diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 72ddfb3d7..cb4f074d8 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -294,7 +294,7 @@ @@ -339,7 +339,6 @@ - @@ -661,6 +660,30 @@ /> + + + + + + + + + + + + + + + + + + + + + + diff --git a/emailcommon/Android.mk b/emailcommon/Android.mk index 7f1428043..872e6c227 100644 --- a/emailcommon/Android.mk +++ b/emailcommon/Android.mk @@ -26,6 +26,8 @@ apache_src_dir := ../../UnifiedEmail/src/org imported_unified_email_files := \ $(unified_email_unified_src_dir)/com/android/mail/utils/LogTag.java \ + $(unified_email_src_dir)/com/android/mail/preferences/BasePreferenceMigrator.java \ + $(unified_email_unified_src_dir)/com/android/mail/preferences/PreferenceMigrator.java \ $(unified_email_src_dir)/com/android/mail/utils/LogUtils.java \ $(unified_email_src_dir)/com/android/mail/providers/UIProvider.java diff --git a/emailcommon/src/com/android/emailcommon/provider/Account.java b/emailcommon/src/com/android/emailcommon/provider/Account.java index 06f8b70a2..15be6e8fd 100755 --- a/emailcommon/src/com/android/emailcommon/provider/Account.java +++ b/emailcommon/src/com/android/emailcommon/provider/Account.java @@ -57,10 +57,6 @@ public final class Account extends EmailContent implements AccountColumns, Parce */ public static final long NO_ACCOUNT = -1L; - // Whether or not the user has asked for notifications of new mail in this account - public final static int FLAGS_NOTIFY_NEW_MAIL = 1<<0; - // Whether or not the user has asked for vibration notifications with all new mail - public final static int FLAGS_VIBRATE = 1<<1; // Bit mask for the account's deletion policy (see DELETE_POLICY_x below) public static final int FLAGS_DELETE_POLICY_MASK = 1<<2 | 1<<3; public static final int FLAGS_DELETE_POLICY_SHIFT = 2; @@ -125,7 +121,6 @@ public final class Account extends EmailContent implements AccountColumns, Parce public boolean mIsDefault; // note: callers should use getDefaultAccountId() public String mCompatibilityUuid; public String mSenderName; - public String mRingtoneUri; public String mProtocolVersion; public int mNewMessageCount; public String mSecuritySyncKey; @@ -151,12 +146,11 @@ public final class Account extends EmailContent implements AccountColumns, Parce public static final int CONTENT_IS_DEFAULT_COLUMN = 9; public static final int CONTENT_COMPATIBILITY_UUID_COLUMN = 10; public static final int CONTENT_SENDER_NAME_COLUMN = 11; - public static final int CONTENT_RINGTONE_URI_COLUMN = 12; - public static final int CONTENT_PROTOCOL_VERSION_COLUMN = 13; - public static final int CONTENT_NEW_MESSAGE_COUNT_COLUMN = 14; - public static final int CONTENT_SECURITY_SYNC_KEY_COLUMN = 15; - public static final int CONTENT_SIGNATURE_COLUMN = 16; - public static final int CONTENT_POLICY_KEY = 17; + public static final int CONTENT_PROTOCOL_VERSION_COLUMN = 12; + public static final int CONTENT_NEW_MESSAGE_COUNT_COLUMN = 13; + public static final int CONTENT_SECURITY_SYNC_KEY_COLUMN = 14; + public static final int CONTENT_SIGNATURE_COLUMN = 15; + public static final int CONTENT_POLICY_KEY = 16; public static final String[] CONTENT_PROJECTION = new String[] { RECORD_ID, AccountColumns.DISPLAY_NAME, @@ -164,7 +158,7 @@ public final class Account extends EmailContent implements AccountColumns, Parce AccountColumns.SYNC_INTERVAL, AccountColumns.HOST_AUTH_KEY_RECV, AccountColumns.HOST_AUTH_KEY_SEND, AccountColumns.FLAGS, AccountColumns.IS_DEFAULT, AccountColumns.COMPATIBILITY_UUID, AccountColumns.SENDER_NAME, - AccountColumns.RINGTONE_URI, AccountColumns.PROTOCOL_VERSION, + AccountColumns.PROTOCOL_VERSION, AccountColumns.NEW_MESSAGE_COUNT, AccountColumns.SECURITY_SYNC_KEY, AccountColumns.SIGNATURE, AccountColumns.POLICY_KEY }; @@ -205,10 +199,9 @@ public final class Account extends EmailContent implements AccountColumns, Parce mBaseUri = CONTENT_URI; // other defaults (policy) - mRingtoneUri = "content://settings/system/notification_sound"; mSyncInterval = -1; mSyncLookback = -1; - mFlags = FLAGS_NOTIFY_NEW_MAIL; + mFlags = 0; mCompatibilityUuid = UUID.randomUUID().toString(); } @@ -258,7 +251,6 @@ public final class Account extends EmailContent implements AccountColumns, Parce mIsDefault = cursor.getInt(CONTENT_IS_DEFAULT_COLUMN) == 1; mCompatibilityUuid = cursor.getString(CONTENT_COMPATIBILITY_UUID_COLUMN); mSenderName = cursor.getString(CONTENT_SENDER_NAME_COLUMN); - mRingtoneUri = cursor.getString(CONTENT_RINGTONE_URI_COLUMN); mProtocolVersion = cursor.getString(CONTENT_PROTOCOL_VERSION_COLUMN); mNewMessageCount = cursor.getInt(CONTENT_NEW_MESSAGE_COUNT_COLUMN); mSecuritySyncKey = cursor.getString(CONTENT_SECURITY_SYNC_KEY_COLUMN); @@ -360,8 +352,6 @@ public final class Account extends EmailContent implements AccountColumns, Parce /** * @return the flags for this account - * @see #FLAGS_NOTIFY_NEW_MAIL - * @see #FLAGS_VIBRATE */ public int getFlags() { return mFlags; @@ -369,29 +359,12 @@ public final class Account extends EmailContent implements AccountColumns, Parce /** * Set the flags for this account - * @see #FLAGS_NOTIFY_NEW_MAIL - * @see #FLAGS_VIBRATE * @param newFlags the new value for the flags */ public void setFlags(int newFlags) { mFlags = newFlags; } - /** - * @return the ringtone Uri for this account - */ - public String getRingtone() { - return mRingtoneUri; - } - - /** - * Set the ringtone Uri for this account - * @param newUri the new URI string for the ringtone for this account - */ - public void setRingtone(String newUri) { - mRingtoneUri = newUri; - } - /** * Set the "delete policy" as a simple 0,1,2 value set. * @param newPolicy the new delete policy @@ -819,7 +792,6 @@ public final class Account extends EmailContent implements AccountColumns, Parce values.put(AccountColumns.IS_DEFAULT, mIsDefault); values.put(AccountColumns.COMPATIBILITY_UUID, mCompatibilityUuid); values.put(AccountColumns.SENDER_NAME, mSenderName); - values.put(AccountColumns.RINGTONE_URI, mRingtoneUri); values.put(AccountColumns.PROTOCOL_VERSION, mProtocolVersion); values.put(AccountColumns.NEW_MESSAGE_COUNT, mNewMessageCount); values.put(AccountColumns.SECURITY_SYNC_KEY, mSecuritySyncKey); @@ -870,7 +842,6 @@ public final class Account extends EmailContent implements AccountColumns, Parce dest.writeByte(mIsDefault ? (byte)1 : (byte)0); dest.writeString(mCompatibilityUuid); dest.writeString(mSenderName); - dest.writeString(mRingtoneUri); dest.writeString(mProtocolVersion); dest.writeInt(mNewMessageCount); dest.writeString(mSecuritySyncKey); @@ -909,7 +880,6 @@ public final class Account extends EmailContent implements AccountColumns, Parce mIsDefault = in.readByte() == 1; mCompatibilityUuid = in.readString(); mSenderName = in.readString(); - mRingtoneUri = in.readString(); mProtocolVersion = in.readString(); mNewMessageCount = in.readInt(); mSecuritySyncKey = in.readString(); diff --git a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java index 05ba4e28b..2506c695b 100755 --- a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java +++ b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java @@ -60,11 +60,9 @@ import java.util.ArrayList; * */ public abstract class EmailContent { - public static final String[] NOTIFICATION_PROJECTION = - new String[] {MailboxColumns.ID, MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT}; public static final int NOTIFICATION_MAILBOX_ID_COLUMN = 0; public static final int NOTIFICATION_MAILBOX_UNREAD_COUNT_COLUMN = 1; - public static final int NOTIFICATION_MAILBOX_MESSAGE_COUNT_COLUMN = 2; + public static final int NOTIFICATION_MAILBOX_UNSEEN_COUNT_COLUMN = 2; // All classes share this public static final String RECORD_ID = "_id"; @@ -520,11 +518,6 @@ public abstract class EmailContent { mIntroText = cursor.getString(CONTENT_INTRO_TEXT_COLUMN); mQuotedTextStartPos = cursor.getInt(CONTENT_QUOTED_TEXT_START_POS_COLUMN); } - - public boolean update() { - // TODO Auto-generated method stub - return false; - } } public interface MessageColumns { @@ -578,6 +571,9 @@ public abstract class EmailContent { public static final String THREAD_TOPIC = "threadTopic"; // For sync adapter use public static final String SYNC_DATA = "syncData"; + + /** Boolean, unseen = 0, seen = 1 [INDEX] */ + public static final String FLAG_SEEN = "flagSeen"; } public static final class Message extends EmailContent implements SyncColumns, MessageColumns { @@ -636,6 +632,7 @@ public abstract class EmailContent { public static final int CONTENT_PROTOCOL_SEARCH_INFO_COLUMN = 22; public static final int CONTENT_THREAD_TOPIC_COLUMN = 23; public static final int CONTENT_SYNC_DATA_COLUMN = 24; + public static final int CONTENT_FLAG_SEEN_COLUMN = 25; public static final String[] CONTENT_PROJECTION = new String[] { RECORD_ID, @@ -650,7 +647,7 @@ public abstract class EmailContent { MessageColumns.BCC_LIST, MessageColumns.REPLY_TO_LIST, SyncColumns.SERVER_TIMESTAMP, MessageColumns.MEETING_INFO, MessageColumns.SNIPPET, MessageColumns.PROTOCOL_SEARCH_INFO, - MessageColumns.THREAD_TOPIC, MessageColumns.SYNC_DATA + MessageColumns.THREAD_TOPIC, MessageColumns.SYNC_DATA, MessageColumns.FLAG_SEEN }; public static final int LIST_ID_COLUMN = 0; @@ -759,6 +756,7 @@ public abstract class EmailContent { public long mTimeStamp; public String mSubject; public boolean mFlagRead = false; + public boolean mFlagSeen = false; public int mFlagLoaded = FLAG_LOADED_UNLOADED; public boolean mFlagFavorite = false; public boolean mFlagAttachment = false; @@ -878,6 +876,7 @@ public abstract class EmailContent { values.put(MessageColumns.TIMESTAMP, mTimeStamp); values.put(MessageColumns.SUBJECT, mSubject); values.put(MessageColumns.FLAG_READ, mFlagRead); + values.put(MessageColumns.FLAG_SEEN, mFlagSeen); values.put(MessageColumns.FLAG_LOADED, mFlagLoaded); values.put(MessageColumns.FLAG_FAVORITE, mFlagFavorite); values.put(MessageColumns.FLAG_ATTACHMENT, mFlagAttachment); @@ -914,6 +913,7 @@ public abstract class EmailContent { mTimeStamp = cursor.getLong(CONTENT_TIMESTAMP_COLUMN); mSubject = cursor.getString(CONTENT_SUBJECT_COLUMN); mFlagRead = cursor.getInt(CONTENT_FLAG_READ_COLUMN) == 1; + mFlagSeen = cursor.getInt(CONTENT_FLAG_SEEN_COLUMN) == 1; mFlagLoaded = cursor.getInt(CONTENT_FLAG_LOADED_COLUMN); mFlagFavorite = cursor.getInt(CONTENT_FLAG_FAVORITE_COLUMN) == 1; mFlagAttachment = cursor.getInt(CONTENT_FLAG_ATTACHMENT_COLUMN) == 1; @@ -936,11 +936,6 @@ public abstract class EmailContent { mSyncData = cursor.getString(CONTENT_SYNC_DATA_COLUMN); } - public boolean update() { - // TODO Auto-generated method stub - return false; - } - /* * Override this so that we can store the Body first and link it to the Message * Also, attachments when we get there... @@ -1518,7 +1513,12 @@ public abstract class EmailContent { public static final String COMPATIBILITY_UUID = "compatibilityUuid"; // User name (for outgoing messages) public static final String SENDER_NAME = "senderName"; - // Ringtone + /** + * Ringtone + * + * @deprecated This is no longer used by anything except for creating the database. + */ + @Deprecated public static final String RINGTONE_URI = "ringtoneUri"; // Protocol version (arbitrary string, used by EAS currently) public static final String PROTOCOL_VERSION = "protocolVersion"; @@ -1585,9 +1585,19 @@ public abstract class EmailContent { public static final String UI_SYNC_STATUS = "uiSyncStatus"; // The UIProvider last sync result public static final String UI_LAST_SYNC_RESULT = "uiLastSyncResult"; - // The UIProvider sync status + /** + * The UIProvider sync status + * + * @deprecated This is no longer used by anything except for creating the database. + */ + @Deprecated public static final String LAST_NOTIFIED_MESSAGE_KEY = "lastNotifiedMessageKey"; - // The UIProvider last sync result + /** + * The UIProvider last sync result + * + * @deprecated This is no longer used by anything except for creating the database. + */ + @Deprecated public static final String LAST_NOTIFIED_MESSAGE_COUNT = "lastNotifiedMessageCount"; // The total number of messages in the remote mailbox public static final String TOTAL_COUNT = "totalCount"; diff --git a/emailcommon/src/com/android/emailcommon/provider/Mailbox.java b/emailcommon/src/com/android/emailcommon/provider/Mailbox.java index 7307dbd47..ca8d9d6e6 100644 --- a/emailcommon/src/com/android/emailcommon/provider/Mailbox.java +++ b/emailcommon/src/com/android/emailcommon/provider/Mailbox.java @@ -63,8 +63,6 @@ public class Mailbox extends EmailContent implements SyncColumns, MailboxColumns public long mLastTouchedTime; public int mUiSyncStatus; public int mUiLastSyncResult; - public long mLastNotifiedMessageKey; - public int mLastNotifiedMessageCount; public int mTotalCount; public String mHierarchicalName; @@ -87,10 +85,8 @@ public class Mailbox extends EmailContent implements SyncColumns, MailboxColumns public static final int CONTENT_LAST_TOUCHED_TIME_COLUMN = 16; public static final int CONTENT_UI_SYNC_STATUS_COLUMN = 17; public static final int CONTENT_UI_LAST_SYNC_RESULT_COLUMN = 18; - public static final int CONTENT_LAST_NOTIFIED_MESSAGE_KEY_COLUMN = 19; - public static final int CONTENT_LAST_NOTIFIED_MESSAGE_COUNT_COLUMN = 20; - public static final int CONTENT_TOTAL_COUNT_COLUMN = 21; - public static final int CONTENT_HIERARCHICAL_NAME_COLUMN = 22; + public static final int CONTENT_TOTAL_COUNT_COLUMN = 19; + public static final int CONTENT_HIERARCHICAL_NAME_COLUMN = 20; /** * NOTE: If fields are added or removed, the method {@link #getHashes()} @@ -104,7 +100,6 @@ public class Mailbox extends EmailContent implements SyncColumns, MailboxColumns MailboxColumns.FLAG_VISIBLE, MailboxColumns.FLAGS, MailboxColumns.VISIBLE_LIMIT, MailboxColumns.SYNC_STATUS, MailboxColumns.PARENT_KEY, MailboxColumns.LAST_TOUCHED_TIME, MailboxColumns.UI_SYNC_STATUS, MailboxColumns.UI_LAST_SYNC_RESULT, - MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY, MailboxColumns.LAST_NOTIFIED_MESSAGE_COUNT, MailboxColumns.TOTAL_COUNT, MailboxColumns.HIERARCHICAL_NAME }; @@ -334,8 +329,6 @@ public class Mailbox extends EmailContent implements SyncColumns, MailboxColumns mLastTouchedTime = cursor.getLong(CONTENT_LAST_TOUCHED_TIME_COLUMN); mUiSyncStatus = cursor.getInt(CONTENT_UI_SYNC_STATUS_COLUMN); mUiLastSyncResult = cursor.getInt(CONTENT_UI_LAST_SYNC_RESULT_COLUMN); - mLastNotifiedMessageKey = cursor.getLong(CONTENT_LAST_NOTIFIED_MESSAGE_KEY_COLUMN); - mLastNotifiedMessageCount = cursor.getInt(CONTENT_LAST_NOTIFIED_MESSAGE_COUNT_COLUMN); mTotalCount = cursor.getInt(CONTENT_TOTAL_COUNT_COLUMN); mHierarchicalName = cursor.getString(CONTENT_HIERARCHICAL_NAME_COLUMN); } @@ -361,8 +354,6 @@ public class Mailbox extends EmailContent implements SyncColumns, MailboxColumns values.put(MailboxColumns.LAST_TOUCHED_TIME, mLastTouchedTime); values.put(MailboxColumns.UI_SYNC_STATUS, mUiSyncStatus); values.put(MailboxColumns.UI_LAST_SYNC_RESULT, mUiLastSyncResult); - values.put(MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY, mLastNotifiedMessageKey); - values.put(MailboxColumns.LAST_NOTIFIED_MESSAGE_COUNT, mLastNotifiedMessageCount); values.put(MailboxColumns.TOTAL_COUNT, mTotalCount); values.put(MailboxColumns.HIERARCHICAL_NAME, mHierarchicalName); return values; @@ -554,10 +545,6 @@ public class Mailbox extends EmailContent implements SyncColumns, MailboxColumns = mUiSyncStatus; hash[CONTENT_UI_LAST_SYNC_RESULT_COLUMN] = mUiLastSyncResult; - hash[CONTENT_LAST_NOTIFIED_MESSAGE_KEY_COLUMN] - = mLastNotifiedMessageKey; - hash[CONTENT_LAST_NOTIFIED_MESSAGE_COUNT_COLUMN] - = mLastNotifiedMessageCount; hash[CONTENT_TOTAL_COUNT_COLUMN] = mTotalCount; hash[CONTENT_HIERARCHICAL_NAME_COLUMN] @@ -594,8 +581,6 @@ public class Mailbox extends EmailContent implements SyncColumns, MailboxColumns dest.writeLong(mLastTouchedTime); dest.writeInt(mUiSyncStatus); dest.writeInt(mUiLastSyncResult); - dest.writeLong(mLastNotifiedMessageKey); - dest.writeInt(mLastNotifiedMessageCount); dest.writeInt(mTotalCount); dest.writeString(mHierarchicalName); } @@ -621,8 +606,6 @@ public class Mailbox extends EmailContent implements SyncColumns, MailboxColumns mLastTouchedTime = in.readLong(); mUiSyncStatus = in.readInt(); mUiLastSyncResult = in.readInt(); - mLastNotifiedMessageKey = in.readLong(); - mLastNotifiedMessageCount = in.readInt(); mTotalCount = in.readInt(); mHierarchicalName = in.readString(); } diff --git a/emailcommon/src/com/android/emailcommon/service/ServiceProxy.java b/emailcommon/src/com/android/emailcommon/service/ServiceProxy.java index e4ac3001a..e5496de9a 100644 --- a/emailcommon/src/com/android/emailcommon/service/ServiceProxy.java +++ b/emailcommon/src/com/android/emailcommon/service/ServiceProxy.java @@ -23,6 +23,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.Debug; import android.os.IBinder; +import android.os.Looper; import android.os.RemoteException; import android.util.Log; @@ -181,6 +182,16 @@ public abstract class ServiceProxy { } public void waitForCompletion() { + /* + * onServiceConnected() is always called on the main thread, and we block the current thread + * for up to 10 seconds as a timeout. If we're currently on the main thread, + * onServiceConnected() is not called until our timeout elapses (and the UI is frozen for + * the duration). + */ + if (Looper.myLooper() == Looper.getMainLooper()) { + throw new IllegalStateException("This cannot be called on the main thread."); + } + synchronized (mConnection) { long time = System.currentTimeMillis(); try { diff --git a/emailcommon/src/com/android/emailcommon/utility/Utility.java b/emailcommon/src/com/android/emailcommon/utility/Utility.java index 28d0b41e9..f6d3e2df7 100644 --- a/emailcommon/src/com/android/emailcommon/utility/Utility.java +++ b/emailcommon/src/com/android/emailcommon/utility/Utility.java @@ -50,10 +50,8 @@ import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.provider.EmailContent.Attachment; import com.android.emailcommon.provider.EmailContent.AttachmentColumns; import com.android.emailcommon.provider.EmailContent.HostAuthColumns; -import com.android.emailcommon.provider.EmailContent.MailboxColumns; import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.provider.HostAuth; -import com.android.emailcommon.provider.Mailbox; import com.android.emailcommon.provider.ProviderUnavailableException; import java.io.ByteArrayInputStream; @@ -903,72 +901,6 @@ public class Utility { } } - /** - * Updates the last seen message key in the mailbox data base for the INBOX of the currently - * shown account. If the account is {@link Account#ACCOUNT_ID_COMBINED_VIEW}, the INBOX for - * all accounts are updated. - * @return an {@link EmailAsyncTask} for test only. - */ - public static EmailAsyncTask updateLastNotifiedMessageKey( - final Context context, final long mailboxId) { - return EmailAsyncTask.runAsyncParallel(new Runnable() { - private void updateLastSeenMessageKeyForMailbox(long mailboxId) { - ContentResolver resolver = context.getContentResolver(); - if (mailboxId == Mailbox.QUERY_ALL_INBOXES) { - Cursor c = resolver.query( - Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION, Mailbox.TYPE + "=?", - new String[] { Integer.toString(Mailbox.TYPE_INBOX) }, null); - if (c == null) throw new ProviderUnavailableException(); - try { - while (c.moveToNext()) { - final long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN); - updateLastSeenMessageKeyForMailbox(id); - } - } finally { - c.close(); - } - } else if (mailboxId > 0L) { - Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); - // mailbox has been removed - if (mailbox == null) { - return; - } - // We use the highest _id for the account the mailbox table as the "last seen - // message key". We don't care if the message has been read or not. We only - // need a point at which we can compare against in the future. By setting this - // value, we are claiming that every message before this has potentially been - // seen by the user. - long mostRecentMessageId = Utility.getFirstRowLong(context, - ContentUris.withAppendedId( - EmailContent.MAILBOX_MOST_RECENT_MESSAGE_URI, mailboxId), - Message.ID_COLUMN_PROJECTION, null, null, null, - Message.ID_MAILBOX_COLUMN_ID, -1L); - long lastNotifiedMessageId = mailbox.mLastNotifiedMessageKey; - // Only update the db if the value has changed - if (mostRecentMessageId != lastNotifiedMessageId) { - Log.d(Logging.LOG_TAG, "Most recent = " + mostRecentMessageId + - ", last notified: " + lastNotifiedMessageId + - "; updating last notified"); - ContentValues values = mailbox.toContentValues(); - values.put(MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY, mostRecentMessageId); - resolver.update( - Mailbox.CONTENT_URI, - values, - EmailContent.ID_SELECTION, - new String[] { Long.toString(mailbox.mId) }); - } else { - Log.d(Logging.LOG_TAG, "Most recent = last notified; no change"); - } - } - } - - @Override - public void run() { - updateLastSeenMessageKeyForMailbox(mailboxId); - } - }); - } - public static long[] toPrimitiveLongArray(Collection collection) { // Need to do this manually because we're converting to a primitive long array, not // a Long array. diff --git a/res/drawable-hdpi/stat_notify_email_generic.png b/res/drawable-hdpi/stat_notify_email_generic.png deleted file mode 100644 index d40e2fe1e..000000000 Binary files a/res/drawable-hdpi/stat_notify_email_generic.png and /dev/null differ diff --git a/res/drawable-xhdpi/stat_notify_email_generic.png b/res/drawable-xhdpi/stat_notify_email_generic.png deleted file mode 100644 index 0317760ee..000000000 Binary files a/res/drawable-xhdpi/stat_notify_email_generic.png and /dev/null differ diff --git a/res/xml/account_settings_preferences.xml b/res/xml/account_settings_preferences.xml index 2c6b83026..04fa921bb 100755 --- a/res/xml/account_settings_preferences.xml +++ b/res/xml/account_settings_preferences.xml @@ -94,22 +94,19 @@ android:title="@string/account_settings_notifications"> - + diff --git a/res/xml/authenticator_alternate.xml b/res/xml/authenticator_alternate.xml index 003e7f720..1403535c8 100644 --- a/res/xml/authenticator_alternate.xml +++ b/res/xml/authenticator_alternate.xml @@ -25,7 +25,7 @@ diff --git a/res/xml/authenticator_eas.xml b/res/xml/authenticator_eas.xml index 4ee83d2c1..eed4c1019 100644 --- a/res/xml/authenticator_eas.xml +++ b/res/xml/authenticator_eas.xml @@ -23,7 +23,7 @@ diff --git a/res/xml/authenticator_imap.xml b/res/xml/authenticator_imap.xml index 72bcf0431..75445645c 100644 --- a/res/xml/authenticator_imap.xml +++ b/res/xml/authenticator_imap.xml @@ -23,7 +23,7 @@ diff --git a/res/xml/authenticator_legacy_eas.xml b/res/xml/authenticator_legacy_eas.xml index b9e31c3aa..041ad2011 100644 --- a/res/xml/authenticator_legacy_eas.xml +++ b/res/xml/authenticator_legacy_eas.xml @@ -23,7 +23,7 @@ diff --git a/res/xml/authenticator_legacy_email.xml b/res/xml/authenticator_legacy_email.xml index 5aad049c8..532cc058b 100644 --- a/res/xml/authenticator_legacy_email.xml +++ b/res/xml/authenticator_legacy_email.xml @@ -23,7 +23,7 @@ diff --git a/res/xml/authenticator_legacy_imap.xml b/res/xml/authenticator_legacy_imap.xml index 8182cef3c..43e0e5d17 100644 --- a/res/xml/authenticator_legacy_imap.xml +++ b/res/xml/authenticator_legacy_imap.xml @@ -23,7 +23,7 @@ diff --git a/res/xml/authenticator_pop3.xml b/res/xml/authenticator_pop3.xml index 47e022106..6bced4fce 100644 --- a/res/xml/authenticator_pop3.xml +++ b/res/xml/authenticator_pop3.xml @@ -23,7 +23,7 @@ diff --git a/res/xml/general_preferences.xml b/res/xml/general_preferences.xml index 3bc17df1f..f3c0a642f 100644 --- a/res/xml/general_preferences.xml +++ b/res/xml/general_preferences.xml @@ -56,8 +56,8 @@ diff --git a/src/com/android/email/EmailIntentService.java b/src/com/android/email/EmailIntentService.java new file mode 100644 index 000000000..005df2f71 --- /dev/null +++ b/src/com/android/email/EmailIntentService.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.email; + +import android.content.Intent; + +import com.android.mail.MailIntentService; +import com.android.mail.utils.LogTag; +import com.android.mail.utils.LogUtils; + +/** + * A service to handle various intents asynchronously. + */ +public class EmailIntentService extends MailIntentService { + private static final String LOG_TAG = LogTag.getLogTag(); + + public EmailIntentService() { + super("EmailIntentService"); + } + + @Override + protected void onHandleIntent(final Intent intent) { + super.onHandleIntent(intent); + + LogUtils.v(LOG_TAG, "Handling intent %s", intent); + } +} diff --git a/src/com/android/email/NotificationController.java b/src/com/android/email/NotificationController.java index c46d8a034..7aacbe800 100644 --- a/src/com/android/email/NotificationController.java +++ b/src/com/android/email/NotificationController.java @@ -22,54 +22,45 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.content.ContentResolver; import android.content.ContentUris; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.res.Resources; import android.database.ContentObserver; import android.database.Cursor; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.AudioManager; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.Process; -import android.text.SpannableString; +import android.provider.Settings; +import android.support.v4.app.NotificationCompat; import android.text.TextUtils; -import android.text.style.TextAppearanceSpan; -import android.util.Log; -import com.android.email.activity.ContactStatusLoader; import com.android.email.activity.setup.AccountSecurity; import com.android.email.activity.setup.AccountSettings; import com.android.email.provider.EmailProvider; -import com.android.email.service.EmailBroadcastProcessorService; -import com.android.email2.ui.MailActivityEmail; -import com.android.emailcommon.Logging; -import com.android.emailcommon.mail.Address; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.Attachment; -import com.android.emailcommon.provider.EmailContent.MailboxColumns; import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.provider.Mailbox; import com.android.emailcommon.utility.EmailAsyncTask; -import com.android.emailcommon.utility.Utility; -import com.android.mail.providers.Conversation; +import com.android.mail.preferences.FolderPreferences; import com.android.mail.providers.Folder; import com.android.mail.providers.UIProvider; -import com.android.mail.utils.Utils; -import com.google.common.annotations.VisibleForTesting; +import com.android.mail.utils.LogTag; +import com.android.mail.utils.LogUtils; +import com.android.mail.utils.NotificationUtils; import java.util.HashMap; import java.util.HashSet; +import java.util.Map; +import java.util.Set; /** * Class that manages notifications. */ public class NotificationController { - private static final String TAG = "NotificationController"; + private static final String LOG_TAG = LogTag.getLogTag(); /** Reserved for {@link com.android.exchange.CalendarSyncEnabler} */ @SuppressWarnings("unused") @@ -84,56 +75,24 @@ public class NotificationController { private static final int NOTIFICATION_ID_BASE_SECURITY_NEEDED = 0x30000000; private static final int NOTIFICATION_ID_BASE_SECURITY_CHANGED = 0x40000000; - /** Selection to retrieve accounts that should we notify user for changes */ - private final static String NOTIFIED_ACCOUNT_SELECTION = - Account.FLAGS + "&" + Account.FLAGS_NOTIFY_NEW_MAIL + " != 0"; - - private static final String NEW_MAIL_MAILBOX_ID = "com.android.email.new_mail.mailboxId"; - private static final String NEW_MAIL_MESSAGE_ID = "com.android.email.new_mail.messageId"; - private static final String NEW_MAIL_MESSAGE_COUNT = "com.android.email.new_mail.messageCount"; - private static final String NEW_MAIL_UNREAD_COUNT = "com.android.email.new_mail.unreadCount"; - private static NotificationThread sNotificationThread; private static Handler sNotificationHandler; private static NotificationController sInstance; private final Context mContext; private final NotificationManager mNotificationManager; - private final AudioManager mAudioManager; - private final Bitmap mGenericSenderIcon; - private final Bitmap mGenericMultipleSenderIcon; private final Clock mClock; /** Maps account id to its observer */ - private final HashMap mNotificationMap; + private final Map mNotificationMap = + new HashMap(); private ContentObserver mAccountObserver; - /** - * Timestamp indicating when the last message notification sound was played. - * Used for throttling. - */ - private long mLastMessageNotifyTime; - - /** - * Minimum interval between notification sounds. - * Since a long sync (either on account setup or after a long period of being offline) can cause - * several notifications consecutively, it can be pretty overwhelming to get a barrage of - * notification sounds. Throttle them using this value. - */ - private static final long MIN_SOUND_INTERVAL_MS = 15 * 1000; // 15 seconds - /** Constructor */ - @VisibleForTesting - NotificationController(Context context, Clock clock) { + private NotificationController(Context context, Clock clock) { mContext = context.getApplicationContext(); EmailContent.init(context); mNotificationManager = (NotificationManager) context.getSystemService( Context.NOTIFICATION_SERVICE); - mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - mGenericSenderIcon = BitmapFactory.decodeResource(mContext.getResources(), - R.drawable.ic_contact_picture); - mGenericMultipleSenderIcon = BitmapFactory.decodeResource(mContext.getResources(), - R.drawable.ic_notification_multiple_mail_holo_dark); mClock = clock; - mNotificationMap = new HashMap(); } /** Singleton access */ @@ -149,16 +108,16 @@ public class NotificationController { * @param notificationId the notification id to check * @return whether or not the notification must be "ongoing" */ - private boolean needsOngoingNotification(int notificationId) { + private static boolean needsOngoingNotification(int notificationId) { // "Security needed" must be ongoing so that the user doesn't close it; otherwise, sync will // be prevented until a reboot. Consider also doing this for password expired. return (notificationId & NOTIFICATION_ID_BASE_MASK) == NOTIFICATION_ID_BASE_SECURITY_NEEDED; } /** - * Returns a {@link Notification.Builder}} for an event with the given account. The account - * contains specific rules on ring tone usage and these will be used to modify the notification - * behaviour. + * Returns a {@link android.support.v4.app.NotificationCompat.Builder} for an event with the + * given account. The account contains specific rules on ring tone usage and these will be used + * to modify the notification behaviour. * * @param accountId The id of the account this notification is being built for. * @param ticker Text displayed when the notification is first shown. May be {@code null}. @@ -166,14 +125,13 @@ public class NotificationController { * @param contentText The second line of text. May NOT be {@code null}. * @param intent The intent to start if the user clicks on the notification. * @param largeIcon A large icon. May be {@code null} - * @param number A number to display using {@link Builder#setNumber(int)}. May - * be {@code null}. + * @param number A number to display using {@link Builder#setNumber(int)}. May be {@code null}. * @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according * to the settings for the given account. * @return A {@link Notification} that can be sent to the notification service. */ - private Notification.Builder createBaseAccountNotificationBuilder(long accountId, String ticker, - CharSequence title, String contentText, Intent intent, Bitmap largeIcon, + private NotificationCompat.Builder createBaseAccountNotificationBuilder(long accountId, + String ticker, CharSequence title, String contentText, Intent intent, Bitmap largeIcon, Integer number, boolean enableAudio, boolean ongoing) { // Pending Intent PendingIntent pending = null; @@ -183,13 +141,13 @@ public class NotificationController { } // NOTE: the ticker is not shown for notifications in the Holo UX - final Notification.Builder builder = new Notification.Builder(mContext) + final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext) .setContentTitle(title) .setContentText(contentText) .setContentIntent(pending) .setLargeIcon(largeIcon) .setNumber(number == null ? 0 : number) - .setSmallIcon(R.drawable.stat_notify_email_generic) + .setSmallIcon(R.drawable.stat_notify_email) .setWhen(mClock.getTime()) .setTicker(ticker) .setOngoing(ongoing); @@ -214,16 +172,16 @@ public class NotificationController { */ private void showNotification(long accountId, String ticker, String title, String contentText, Intent intent, int notificationId) { - final Notification.Builder builder = createBaseAccountNotificationBuilder(accountId, ticker, - title, contentText, intent, null, null, true, + final NotificationCompat.Builder builder = createBaseAccountNotificationBuilder(accountId, + ticker, title, contentText, intent, null, null, true, needsOngoingNotification(notificationId)); - mNotificationManager.notify(notificationId, builder.getNotification()); + mNotificationManager.notify(notificationId, builder.build()); } /** * Returns a notification ID for new message notifications for the given account. */ - private int getNewMessageNotificationId(long mailboxId) { + private static int getNewMessageNotificationId(long mailboxId) { // We assume accountId will always be less than 0x0FFFFFFF; is there a better way? return (int) (NOTIFICATION_ID_BASE_NEW_MESSAGES + mailboxId); } @@ -234,42 +192,20 @@ public class NotificationController { * database changes, we save the state [e.g. message ID and count] of the most recent * notification shown to the user. And, when we start observing database changes, we restore * the saved state. - * @param watch If {@code true}, we register observers for all accounts whose settings have - * notifications enabled. Otherwise, all observers are unregistered. */ - public void watchForMessages(final boolean watch) { - if (MailActivityEmail.DEBUG) { - Log.d(Logging.LOG_TAG, "Notifications being toggled: " + watch); - } - // Don't create the thread if we're only going to stop watching - if (!watch && sNotificationThread == null) return; - + public void watchForMessages() { ensureHandlerExists(); // Run this on the message notification handler sNotificationHandler.post(new Runnable() { @Override public void run() { ContentResolver resolver = mContext.getContentResolver(); - if (!watch) { - unregisterMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW); - if (mAccountObserver != null) { - resolver.unregisterContentObserver(mAccountObserver); - mAccountObserver = null; - } - - // tear down the event loop - sNotificationThread.quit(); - sNotificationThread = null; - return; - } // otherwise, start new observers for all notified accounts registerMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW); // If we're already observing account changes, don't do anything else if (mAccountObserver == null) { - if (MailActivityEmail.DEBUG) { - Log.i(Logging.LOG_TAG, "Observing account changes for notifications"); - } + LogUtils.i(LOG_TAG, "Observing account changes for notifications"); mAccountObserver = new AccountContentObserver(sNotificationHandler, mContext); resolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver); } @@ -294,12 +230,12 @@ public class NotificationController { * {@link Account#ACCOUNT_ID_COMBINED_VIEW} to register observers for all * accounts that allow for user notification. */ - private void registerMessageNotification(long accountId) { + private void registerMessageNotification(final long accountId) { ContentResolver resolver = mContext.getContentResolver(); if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { Cursor c = resolver.query( Account.CONTENT_URI, EmailContent.ID_PROJECTION, - NOTIFIED_ACCOUNT_SELECTION, null, null); + null, null, null); try { while (c.moveToNext()) { long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN); @@ -311,9 +247,7 @@ public class NotificationController { } else { ContentObserver obs = mNotificationMap.get(accountId); if (obs != null) return; // we're already observing; nothing to do - if (MailActivityEmail.DEBUG) { - Log.i(Logging.LOG_TAG, "Registering for notifications for account " + accountId); - } + LogUtils.i(LOG_TAG, "Registering for notifications for account " + accountId); ContentObserver observer = new MessageContentObserver( sNotificationHandler, mContext, accountId); resolver.registerContentObserver(Message.NOTIFIER_URI, true, observer); @@ -331,21 +265,17 @@ public class NotificationController { * @param accountId The ID of the account to unregister from. To unregister all accounts that * have observers, specify an ID of {@link Account#ACCOUNT_ID_COMBINED_VIEW}. */ - private void unregisterMessageNotification(long accountId) { + private void unregisterMessageNotification(final long accountId) { ContentResolver resolver = mContext.getContentResolver(); if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { - if (MailActivityEmail.DEBUG) { - Log.i(Logging.LOG_TAG, "Unregistering notifications for all accounts"); - } + LogUtils.i(LOG_TAG, "Unregistering notifications for all accounts"); // cancel all existing message observers for (ContentObserver observer : mNotificationMap.values()) { resolver.unregisterContentObserver(observer); } mNotificationMap.clear(); } else { - if (MailActivityEmail.DEBUG) { - Log.i(Logging.LOG_TAG, "Unregistering notifications for account " + accountId); - } + LogUtils.i(LOG_TAG, "Unregistering notifications for account " + accountId); ContentObserver observer = mNotificationMap.remove(accountId); if (observer != null) { resolver.unregisterContentObserver(observer); @@ -353,411 +283,60 @@ public class NotificationController { } } - /** - * Returns a picture of the sender of the given message. If no picture is available, returns - * {@code null}. - * - * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) - */ - private Bitmap getSenderPhoto(Message message) { - Address sender = Address.unpackFirst(message.mFrom); - if (sender == null) { - return null; - } - String email = sender.getAddress(); - if (TextUtils.isEmpty(email)) { - return null; - } - Bitmap photo = ContactStatusLoader.getContactInfo(mContext, email).mPhoto; - - if (photo != null) { - final Resources res = mContext.getResources(); - final int idealIconHeight = - res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); - final int idealIconWidth = - res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width); - - if (photo.getHeight() < idealIconHeight) { - // We should scale this image to fit the intended size - photo = Bitmap.createScaledBitmap( - photo, idealIconWidth, idealIconHeight, true); - } - } - return photo; - } - public static final String EXTRA_ACCOUNT = "account"; public static final String EXTRA_CONVERSATION = "conversationUri"; public static final String EXTRA_FOLDER = "folder"; - private Intent createViewConversationIntent(Conversation conversation, Folder folder, - com.android.mail.providers.Account account) { - final Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - intent.putExtra(EXTRA_ACCOUNT, account.serialize()); - if (folder != null) { - intent.setDataAndType(folder.uri, account.mimeType); - intent.putExtra(EXTRA_FOLDER, Folder.toString(folder)); - } - intent.putExtra(EXTRA_CONVERSATION, conversation); - return intent; - } - - private Intent createViewMailboxIntent(com.android.mail.providers.Account account, - Folder folder) { - final Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - intent.putExtra(EXTRA_ACCOUNT, account.serialize()); - if (folder != null) { - intent.setDataAndType(folder.uri, account.mimeType); - intent.putExtra(EXTRA_FOLDER, Folder.toString(folder)); - } - return intent; - } - - private Cursor getUiCursor(Uri uri, String[] projection) { - Cursor c = mContext.getContentResolver().query(uri, projection, null, null, null); - if (c == null) return null; - if (c.moveToFirst()) { - return c; - } else { - c.close(); - return null; - } - } - - private Intent createViewConversationIntent(Message message) { - Cursor c = getUiCursor(EmailProvider.uiUri("uiaccount", message.mAccountKey), - UIProvider.ACCOUNTS_PROJECTION); - if (c == null) { - Log.w(TAG, "Can't find account for message " + message.mId); - return null; - } - com.android.mail.providers.Account acct = new com.android.mail.providers.Account(c); - c.close(); - c = getUiCursor(EmailProvider.uiUri("uifolder", message.mMailboxKey), - UIProvider.FOLDERS_PROJECTION); - if (c == null) { - Log.w(TAG, "Can't find folder for message " + message.mId + ", folder " + - message.mMailboxKey); - return null; - } - Folder folder = new Folder(c); - c.close(); - c = getUiCursor(EmailProvider.uiUri("uiconversation", message.mId), - UIProvider.CONVERSATION_PROJECTION); - if (c == null) { - Log.w(TAG, "Can't find conversation for message " + message.mId); - return null; - } - Conversation conv = new Conversation(c); - c.close(); - return createViewConversationIntent(conv, folder, acct); - } - - private Intent createViewMailboxIntentForMessage(Message message) { - Cursor c = null; - com.android.mail.providers.Account acct = null; - try { - c = getUiCursor(EmailProvider.uiUri("uiaccount", message.mAccountKey), - UIProvider.ACCOUNTS_PROJECTION); - if (c == null) { - Log.w(TAG, "Can't find account for message " + message.mId); - return null; - } - acct = new com.android.mail.providers.Account(c); - } finally { - if (c != null) { - c.close(); - c = null; - } - } - - Folder folder = null; - try { - c = getUiCursor(EmailProvider.uiUri("uifolder", message.mMailboxKey), - UIProvider.FOLDERS_PROJECTION); - if (c == null) { - Log.w(TAG, "Can't find folder for message " + message.mId + ", folder " + - message.mMailboxKey); - return null; - } - folder = new Folder(c); - } finally { - if (c != null) { - c.close(); - c = null; - } - } - return createViewMailboxIntent(acct, folder); - } - - /** - * Returns a "new message" notification for the given account. - * - * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) - */ - @VisibleForTesting - Notification createNewMessageNotification(long mailboxId, long newMessageId, - int unseenMessageCount, int unreadCount) { - final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); - if (mailbox == null) { - return null; - } - final Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey); - if (account == null) { - return null; - } - // Get the latest message - final Message message = Message.restoreMessageWithId(mContext, newMessageId); - if (message == null) { - return null; // no message found??? - } - - String senderName = Address.toFriendly(Address.unpack(message.mFrom)); - if (senderName == null) { - senderName = ""; // Happens when a message has no from. - } - final boolean multipleUnseen = unseenMessageCount > 1; - final Bitmap senderPhoto = multipleUnseen - ? mGenericMultipleSenderIcon - : getSenderPhoto(message); - final SpannableString title = getNewMessageTitle(senderName, unseenMessageCount); - // TODO: add in display name on the second line for the text, once framework supports - // multiline texts. - // Show account name if an inbox; otherwise mailbox name - final String text = multipleUnseen - ? ((mailbox.mType == Mailbox.TYPE_INBOX) ? account.mDisplayName : - mailbox.mDisplayName) - : message.mSubject; - final Bitmap largeIcon = senderPhoto != null ? senderPhoto : mGenericSenderIcon; - final Integer number = unreadCount > 1 ? unreadCount : null; - final Intent intent; - if (multipleUnseen) { - intent = createViewMailboxIntentForMessage(message); - } else { - intent = createViewConversationIntent(message); - } - if (intent == null) { - return null; - } - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | - Intent.FLAG_ACTIVITY_TASK_ON_HOME); - long now = mClock.getTime(); - boolean enableAudio = (now - mLastMessageNotifyTime) > MIN_SOUND_INTERVAL_MS; - final Notification.Builder builder = createBaseAccountNotificationBuilder( - mailbox.mAccountKey, title.toString(), title, text, - intent, largeIcon, number, enableAudio, false); - if (Utils.isRunningJellybeanOrLater()) { - // For a new-style notification - if (multipleUnseen) { - final Cursor messageCursor = - mContext.getContentResolver().query(ContentUris.withAppendedId( - EmailContent.MAILBOX_NOTIFICATION_URI, mailbox.mAccountKey), - EmailContent.NOTIFICATION_PROJECTION, null, null, null); - - try { - if (messageCursor != null && messageCursor.getCount() > 0) { - final int maxNumDigestItems = mContext.getResources().getInteger( - R.integer.max_num_notification_digest_items); - // The body of the notification is the account name, or the label name. - builder.setSubText(text); - - Notification.InboxStyle digest = new Notification.InboxStyle(builder); - - digest.setBigContentTitle(title); - - int numDigestItems = 0; - // We can assume that the current position of the cursor is on the - // newest message - messageCursor.moveToFirst(); - do { - final long messageId = - messageCursor.getLong(EmailContent.ID_PROJECTION_COLUMN); - - // Get the latest message - final Message digestMessage = - Message.restoreMessageWithId(mContext, messageId); - if (digestMessage != null) { - final CharSequence digestLine = - getSingleMessageInboxLine(mContext, digestMessage); - digest.addLine(digestLine); - numDigestItems++; - } - } while (numDigestItems <= maxNumDigestItems && messageCursor.moveToNext()); - - // We want to clear the content text in this case. The content text would - // have been set in createBaseAccountNotificationBuilder, but since the - // same string was set in as the subtext, we don't want to show a - // duplicate string. - builder.setContentText(null); - } - } finally { - if (messageCursor != null) { - messageCursor.close(); - } - } - } else { - // The notification content will be the subject of the conversation. - builder.setContentText(getSingleMessageLittleText(mContext, message.mSubject)); - - // The notification subtext will be the subject of the conversation for inbox - // notifications, or will based on the the label name for user label notifications. - builder.setSubText(account.mDisplayName); - - final Notification.BigTextStyle bigText = new Notification.BigTextStyle(builder); - bigText.bigText(getSingleMessageBigText(mContext, message)); - } - } - - mLastMessageNotifyTime = now; - return builder.getNotification(); - } - - /** - * Sets the bigtext for a notification for a single new conversation - * @param context - * @param message New message that triggered the notification. - * @return a {@link CharSequence} suitable for use in {@link Notification.BigTextStyle} - */ - private static CharSequence getSingleMessageInboxLine(Context context, Message message) { - final String subject = message.mSubject; - final String snippet = message.mSnippet; - final String senders = Address.toFriendly(Address.unpack(message.mFrom)); - - final String subjectSnippet = !TextUtils.isEmpty(subject) ? subject : snippet; - - final TextAppearanceSpan notificationPrimarySpan = - new TextAppearanceSpan(context, R.style.NotificationPrimaryText); - - if (TextUtils.isEmpty(senders)) { - // If the senders are empty, just use the subject/snippet. - return subjectSnippet; - } - else if (TextUtils.isEmpty(subjectSnippet)) { - // If the subject/snippet is empty, just use the senders. - final SpannableString spannableString = new SpannableString(senders); - spannableString.setSpan(notificationPrimarySpan, 0, senders.length(), 0); - - return spannableString; - } else { - final String formatString = context.getResources().getString( - R.string.multiple_new_message_notification_item); - final TextAppearanceSpan notificationSecondarySpan = - new TextAppearanceSpan(context, R.style.NotificationSecondaryText); - - final String instantiatedString = String.format(formatString, senders, subjectSnippet); - - final SpannableString spannableString = new SpannableString(instantiatedString); - - final boolean isOrderReversed = formatString.indexOf("%2$s") < - formatString.indexOf("%1$s"); - final int primaryOffset = - (isOrderReversed ? instantiatedString.lastIndexOf(senders) : - instantiatedString.indexOf(senders)); - final int secondaryOffset = - (isOrderReversed ? instantiatedString.lastIndexOf(subjectSnippet) : - instantiatedString.indexOf(subjectSnippet)); - spannableString.setSpan(notificationPrimarySpan, - primaryOffset, primaryOffset + senders.length(), 0); - spannableString.setSpan(notificationSecondarySpan, - secondaryOffset, secondaryOffset + subjectSnippet.length(), 0); - return spannableString; - } - } - - /** - * Sets the bigtext for a notification for a single new conversation - * @param context - * @param subject Subject of the new message that triggered the notification - * @return a {@link CharSequence} suitable for use in {@link Notification.ContentText} - */ - private static CharSequence getSingleMessageLittleText(Context context, String subject) { - if (subject == null) { - return null; - } - final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan( - context, R.style.NotificationPrimaryText); - - final SpannableString spannableString = new SpannableString(subject); - spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0); - - return spannableString; - } - - - /** - * Sets the bigtext for a notification for a single new conversation - * @param context - * @param message New message that triggered the notification - * @return a {@link CharSequence} suitable for use in {@link Notification.BigTextStyle} - */ - private static CharSequence getSingleMessageBigText(Context context, Message message) { - final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan( - context, R.style.NotificationPrimaryText); - - final String subject = message.mSubject; - final String snippet = message.mSnippet; - - if (TextUtils.isEmpty(subject)) { - // If the subject is empty, just use the snippet. - return snippet; - } - else if (TextUtils.isEmpty(snippet)) { - // If the snippet is empty, just use the subject. - final SpannableString spannableString = new SpannableString(subject); - spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0); - - return spannableString; - } else { - final String notificationBigTextFormat = context.getResources().getString( - R.string.single_new_message_notification_big_text); - - // Localizers may change the order of the parameters, look at how the format - // string is structured. - final boolean isSubjectFirst = notificationBigTextFormat.indexOf("%2$s") > - notificationBigTextFormat.indexOf("%1$s"); - final String bigText = String.format(notificationBigTextFormat, subject, snippet); - final SpannableString spannableString = new SpannableString(bigText); - - final int subjectOffset = - (isSubjectFirst ? bigText.indexOf(subject) : bigText.lastIndexOf(subject)); - spannableString.setSpan(notificationSubjectSpan, - subjectOffset, subjectOffset + subject.length(), 0); - - return spannableString; - } - } - - /** - * Creates a notification title for a new message. If there is only a single message, - * show the sender name. Otherwise, show "X new messages". - */ - @VisibleForTesting - SpannableString getNewMessageTitle(String sender, int unseenCount) { - String title; - if (unseenCount > 1) { - title = String.format( - mContext.getString(R.string.notification_multiple_new_messages_fmt), - unseenCount); - } else { - title = sender; - } - return new SpannableString(title); - } - - /** Returns the system's current ringer mode */ - @VisibleForTesting - int getRingerMode() { - return mAudioManager.getRingerMode(); - } - /** Sets up the notification's sound and vibration based upon account details. */ - @VisibleForTesting - void setupSoundAndVibration(Notification.Builder builder, Account account) { - final int flags = account.mFlags; - final String ringtoneUri = account.mRingtoneUri; - final boolean vibrate = (flags & Account.FLAGS_VIBRATE) != 0; + private void setupSoundAndVibration( + NotificationCompat.Builder builder, Account account) { + String ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI.toString(); + boolean vibrate = false; + + // Use the Inbox notification preferences + final Cursor accountCursor = mContext.getContentResolver().query(EmailProvider.uiUri( + "uiaccount", account.mId), UIProvider.ACCOUNTS_PROJECTION, null, null, null); + + com.android.mail.providers.Account uiAccount = null; + try { + if (accountCursor.moveToFirst()) { + uiAccount = new com.android.mail.providers.Account(accountCursor); + } + } finally { + accountCursor.close(); + } + + if (uiAccount != null) { + final Cursor folderCursor = + mContext.getContentResolver().query(uiAccount.settings.defaultInbox, + UIProvider.FOLDERS_PROJECTION, null, null, null); + + if (folderCursor == null) { + LogUtils.e(LOG_TAG, "Null folder cursor for mailbox %s", + uiAccount.settings.defaultInbox); + } + + Folder folder = null; + try { + if (folderCursor.moveToFirst()) { + folder = new Folder(folderCursor); + } + } finally { + folderCursor.close(); + } + + if (folder != null) { + final FolderPreferences folderPreferences = + new FolderPreferences(mContext, uiAccount.name, folder, true /* inbox */); + + ringtoneUri = folderPreferences.getNotificationRingtoneUri(); + vibrate = folderPreferences.isNotificationVibrateEnabled(); + } else { + LogUtils.e(LOG_TAG, "Null folder for mailbox %s", uiAccount.settings.defaultInbox); + } + } else { + LogUtils.e(LOG_TAG, "Null uiAccount for account id %d", account.mId); + } int defaults = Notification.DEFAULT_LIGHTS; if (vibrate) { @@ -790,7 +369,7 @@ public class NotificationController { /** * Returns a notification ID for login failed notifications for the given account account. */ - private int getLoginFailedNotificationId(long accountId) { + private static int getLoginFailedNotificationId(long accountId) { return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId; } @@ -953,109 +532,100 @@ public class NotificationController { private final long mAccountId; public MessageContentObserver( - Handler handler, Context context, long accountId) { + final Handler handler, final Context context, final long accountId) { super(handler); mContext = context; mAccountId = accountId; } @Override - public void onChange(boolean selfChange) { - ContentObserver observer = sInstance.mNotificationMap.get(mAccountId); - Account account = Account.restoreAccountWithId(mContext, mAccountId); - if (observer == null || account == null) { - Log.w(Logging.LOG_TAG, "Couldn't find account for changed message notification"); + public void onChange(final boolean selfChange) { + final ContentResolver contentResolver = mContext.getContentResolver(); + + final Cursor accountCursor = contentResolver.query( + EmailProvider.uiUri("uiaccount", mAccountId), UIProvider.ACCOUNTS_PROJECTION, + null, null, null); + + if (accountCursor == null) { + LogUtils.e(LOG_TAG, "Null account cursor for mAccountId %d", mAccountId); return; } - ContentResolver resolver = mContext.getContentResolver(); - Cursor c = resolver.query(ContentUris.withAppendedId( - EmailContent.MAILBOX_NOTIFICATION_URI, mAccountId), - EmailContent.NOTIFICATION_PROJECTION, null, null, null); + com.android.mail.providers.Account account = null; try { - while (c.moveToNext()) { - long mailboxId = c.getLong(EmailContent.NOTIFICATION_MAILBOX_ID_COLUMN); - if (mailboxId == 0) continue; - int messageCount = - c.getInt(EmailContent.NOTIFICATION_MAILBOX_MESSAGE_COUNT_COLUMN); - int unreadCount = - c.getInt(EmailContent.NOTIFICATION_MAILBOX_UNREAD_COUNT_COLUMN); - - Mailbox m = Mailbox.restoreMailboxWithId(mContext, mailboxId); - long newMessageId = Utility.getFirstRowLong(mContext, - ContentUris.withAppendedId( - EmailContent.MAILBOX_MOST_RECENT_MESSAGE_URI, mailboxId), - Message.ID_COLUMN_PROJECTION, null, null, null, - Message.ID_MAILBOX_COLUMN_ID, -1L); - Log.d(Logging.LOG_TAG, "Changes to " + account.mDisplayName + "/" + - m.mDisplayName + ", count: " + messageCount + ", lastNotified: " + - m.mLastNotifiedMessageKey + ", mostRecent: " + newMessageId); - // Broadcast intent here - Intent i = new Intent(EmailBroadcastProcessorService.ACTION_NOTIFY_NEW_MAIL); - // Required by UIProvider - i.setType(EmailProvider.EMAIL_APP_MIME_TYPE); - i.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_FOLDER, - Uri.parse(EmailProvider.uiUriString("uifolder", mailboxId))); - i.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_ACCOUNT, - Uri.parse(EmailProvider.uiUriString("uiaccount", m.mAccountKey))); - i.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNREAD_COUNT, - unreadCount); - // Required by our notification controller - i.putExtra(NEW_MAIL_MAILBOX_ID, mailboxId); - i.putExtra(NEW_MAIL_MESSAGE_ID, newMessageId); - i.putExtra(NEW_MAIL_MESSAGE_COUNT, messageCount); - i.putExtra(NEW_MAIL_UNREAD_COUNT, unreadCount); - mContext.sendOrderedBroadcast(i, null); + if (accountCursor.moveToFirst()) { + account = new com.android.mail.providers.Account(accountCursor); } } finally { - c.close(); + accountCursor.close(); + } + + final Cursor mailboxCursor = contentResolver.query( + ContentUris.withAppendedId(EmailContent.MAILBOX_NOTIFICATION_URI, mAccountId), + null, null, null, null); + try { + while (mailboxCursor.moveToNext()) { + final long mailboxId = + mailboxCursor.getLong(EmailContent.NOTIFICATION_MAILBOX_ID_COLUMN); + if (mailboxId == 0) continue; + + final int unreadCount = mailboxCursor.getInt( + EmailContent.NOTIFICATION_MAILBOX_UNREAD_COUNT_COLUMN); + final int unseenCount = mailboxCursor.getInt( + EmailContent.NOTIFICATION_MAILBOX_UNSEEN_COUNT_COLUMN); + + final Cursor folderCursor = contentResolver.query( + EmailProvider.uiUri("uifolder", mailboxId), + UIProvider.FOLDERS_PROJECTION, null, null, null); + + if (folderCursor == null) { + LogUtils.e(LOG_TAG, "Null folder cursor for mMailboxId %d", mailboxId); + return; + } + + Folder folder = null; + try { + if (folderCursor.moveToFirst()) { + folder = new Folder(folderCursor); + } + } finally { + folderCursor.close(); + } + + LogUtils.d(LOG_TAG, "Changes to account " + account.name + ", folder: " + + folder.name + ", unreadCount: " + unreadCount + ", unseenCount: " + + unseenCount); + + NotificationUtils.setNewEmailIndicator(mContext, unreadCount, unseenCount, + account, folder, true); + } + } finally { + mailboxCursor.close(); } } } - public static void notifyNewMail(Context context, Intent i) { - Log.d(Logging.LOG_TAG, "Sending notification to system..."); - NotificationController nc = NotificationController.getInstance(context); - ContentResolver resolver = context.getContentResolver(); - long mailboxId = i.getLongExtra(NEW_MAIL_MAILBOX_ID, -1); - long newMessageId = i.getLongExtra(NEW_MAIL_MESSAGE_ID, -1); - int messageCount = i.getIntExtra(NEW_MAIL_MESSAGE_COUNT, 0); - int unreadCount = i.getIntExtra(NEW_MAIL_UNREAD_COUNT, 0); - Notification n = nc.createNewMessageNotification(mailboxId, newMessageId, - messageCount, unreadCount); - if (n != null) { - // Make the notification visible - nc.mNotificationManager.notify(nc.getNewMessageNotificationId(mailboxId), n); - } - // Save away the new values - ContentValues cv = new ContentValues(); - cv.put(MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY, newMessageId); - cv.put(MailboxColumns.LAST_NOTIFIED_MESSAGE_COUNT, messageCount); - resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId), cv, - null, null); - } - /** * Observer invoked whenever an account is modified. This could mean the user changed the * notification settings. */ private static class AccountContentObserver extends ContentObserver { private final Context mContext; - public AccountContentObserver(Handler handler, Context context) { + public AccountContentObserver(final Handler handler, final Context context) { super(handler); mContext = context; } @Override - public void onChange(boolean selfChange) { + public void onChange(final boolean selfChange) { final ContentResolver resolver = mContext.getContentResolver(); final Cursor c = resolver.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION, - NOTIFIED_ACCOUNT_SELECTION, null, null); - final HashSet newAccountList = new HashSet(); - final HashSet removedAccountList = new HashSet(); + null, null, null); + final Set newAccountList = new HashSet(); + final Set removedAccountList = new HashSet(); if (c == null) { // Suspender time ... theoretically, this will never happen - Log.wtf(Logging.LOG_TAG, "#onChange(); NULL response for account id query"); + LogUtils.wtf(LOG_TAG, "#onChange(); NULL response for account id query"); return; } try { @@ -1071,22 +641,22 @@ public class NotificationController { // NOTE: Looping over three lists is not necessarily the most efficient. However, the // account lists are going to be very small, so, this will not be necessarily bad. // Cycle through existing notification list and adjust as necessary - for (long accountId : sInstance.mNotificationMap.keySet()) { + for (final long accountId : sInstance.mNotificationMap.keySet()) { if (!newAccountList.remove(accountId)) { // account id not in the current set of notifiable accounts removedAccountList.add(accountId); } } // A new account was added to the notification list - for (long accountId : newAccountList) { + for (final long accountId : newAccountList) { sInstance.registerMessageNotification(accountId); } // An account was removed from the notification list - for (long accountId : removedAccountList) { + for (final long accountId : removedAccountList) { sInstance.unregisterMessageNotification(accountId); - int notificationId = sInstance.getNewMessageNotificationId(accountId); - sInstance.mNotificationManager.cancel(notificationId); } + + NotificationUtils.resendNotifications(mContext, false); } } @@ -1099,7 +669,7 @@ public class NotificationController { /** The {@link Looper} that handles messages for this thread */ private Looper mLooper; - NotificationThread() { + public NotificationThread() { new Thread(null, this, "EmailNotification").start(); synchronized (mLock) { while (mLooper == null) { @@ -1121,10 +691,8 @@ public class NotificationController { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); Looper.loop(); } - void quit() { - mLooper.quit(); - } - Looper getLooper() { + + public Looper getLooper() { return mLooper; } } diff --git a/src/com/android/email/Preferences.java b/src/com/android/email/Preferences.java index 079d5b200..4d1042c0f 100644 --- a/src/com/android/email/Preferences.java +++ b/src/com/android/email/Preferences.java @@ -75,13 +75,6 @@ public class Preferences { // "normal" will be the default public static final int TEXT_ZOOM_DEFAULT = TEXT_ZOOM_NORMAL; - // Starting something new here: - // REPLY_ALL is saved by the framework (CheckBoxPreference's parent, Preference). - // i.e. android:persistent=true in general_preferences.xml - public static final String REPLY_ALL = "reply_all"; - // Reply All Default - when changing this, be sure to update general_preferences.xml - public static final boolean REPLY_ALL_DEFAULT = false; - private static Preferences sPreferences; private final SharedPreferences mSharedPreferences; diff --git a/src/com/android/email/activity/setup/AccountSettingsFragment.java b/src/com/android/email/activity/setup/AccountSettingsFragment.java index bad4c15e1..8a8c60c81 100644 --- a/src/com/android/email/activity/setup/AccountSettingsFragment.java +++ b/src/com/android/email/activity/setup/AccountSettingsFragment.java @@ -21,8 +21,10 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.content.res.Resources; +import android.database.Cursor; +import android.media.Ringtone; +import android.media.RingtoneManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; @@ -32,14 +34,16 @@ import android.preference.EditTextPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceCategory; -import android.preference.RingtonePreference; +import android.preference.Preference.OnPreferenceClickListener; import android.provider.CalendarContract; import android.provider.ContactsContract; +import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import com.android.email.R; import com.android.email.SecurityPolicy; +import com.android.email.provider.EmailProvider; import com.android.email.provider.FolderPickerActivity; import com.android.email.service.EmailServiceUtils; import com.android.email.service.EmailServiceUtils.EmailServiceInfo; @@ -51,6 +55,11 @@ import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.provider.Mailbox; import com.android.emailcommon.provider.Policy; import com.android.emailcommon.utility.Utility; +import com.android.mail.preferences.AccountPreferences; +import com.android.mail.preferences.FolderPreferences; +import com.android.mail.providers.Folder; +import com.android.mail.providers.UIProvider; +import com.android.mail.utils.NotificationUtils; import java.util.ArrayList; @@ -79,10 +88,6 @@ public class AccountSettingsFragment extends EmailPreferenceFragment private static final String PREFERENCE_DEFAULT = "account_default"; private static final String PREFERENCE_CATEGORY_DATA_USAGE = "data_usage"; private static final String PREFERENCE_CATEGORY_NOTIFICATIONS = "account_notifications"; - private static final String PREFERENCE_NOTIFY = "account_notify"; - private static final String PREFERENCE_VIBRATE = "account_settings_vibrate"; - private static final String PREFERENCE_VIBRATE_OLD = "account_settings_vibrate_when"; - private static final String PREFERENCE_RINGTONE = "account_ringtone"; private static final String PREFERENCE_CATEGORY_SERVER = "account_servers"; private static final String PREFERENCE_CATEGORY_POLICIES = "account_policies"; private static final String PREFERENCE_POLICIES_ENFORCED = "policies_enforced"; @@ -98,6 +103,9 @@ public class AccountSettingsFragment extends EmailPreferenceFragment private static final String PREFERENCE_SYSTEM_FOLDERS_TRASH = "system_folders_trash"; private static final String PREFERENCE_SYSTEM_FOLDERS_SENT = "system_folders_sent"; + // Request code to start different activities. + private static final int RINGTONE_REQUEST_CODE = 0; + private EditTextPreference mAccountDescription; private EditTextPreference mAccountName; private EditTextPreference mAccountSignature; @@ -105,9 +113,9 @@ public class AccountSettingsFragment extends EmailPreferenceFragment private ListPreference mSyncWindow; private CheckBoxPreference mAccountBackgroundAttachments; private CheckBoxPreference mAccountDefault; - private CheckBoxPreference mAccountNotify; - private CheckBoxPreference mAccountVibrate; - private RingtonePreference mAccountRingtone; + private CheckBoxPreference mInboxNotify; + private CheckBoxPreference mInboxVibrate; + private Preference mInboxRingtone; private CheckBoxPreference mSyncContacts; private CheckBoxPreference mSyncCalendar; private CheckBoxPreference mSyncEmail; @@ -121,6 +129,11 @@ public class AccountSettingsFragment extends EmailPreferenceFragment private boolean mLoaded; private boolean mSaveOnExit; + private Ringtone mRingtone; + + private AccountPreferences mAccountPreferences; + private FolderPreferences mInboxFolderPreferences; + /** The e-mail of the account being edited. */ private String mAccountEmail; @@ -178,8 +191,6 @@ public class AccountSettingsFragment extends EmailPreferenceFragment } super.onCreate(savedInstanceState); - upgradeVibrateSetting(); - // Load the preferences from an XML resource addPreferencesFromResource(R.xml.account_settings_preferences); @@ -197,20 +208,6 @@ public class AccountSettingsFragment extends EmailPreferenceFragment mAccountDirty = false; } - /** - * Upgrades the old tri-state vibrate setting to the new boolean value. - */ - private void upgradeVibrateSetting() { - final SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences(); - - if (!sharedPreferences.contains(PREFERENCE_VIBRATE)) { - // Try to migrate the old one - final boolean vibrate = - "always".equals(sharedPreferences.getString(PREFERENCE_VIBRATE_OLD, "")); - sharedPreferences.edit().putBoolean(PREFERENCE_VIBRATE, vibrate); - } - } - @Override public void onActivityCreated(Bundle savedInstanceState) { if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) { @@ -293,6 +290,41 @@ public class AccountSettingsFragment extends EmailPreferenceFragment mStarted = false; } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case RINGTONE_REQUEST_CODE: + if (resultCode == Activity.RESULT_OK && data != null) { + Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); + setRingtone(uri); + } + break; + } + } + + /** + * Sets the current ringtone. + */ + private void setRingtone(Uri ringtone) { + if (ringtone != null) { + mInboxFolderPreferences.setNotificationRingtoneUri(ringtone.toString()); + mRingtone = RingtoneManager.getRingtone(getActivity(), ringtone); + } else { + // Null means silent was selected. + mInboxFolderPreferences.setNotificationRingtoneUri(""); + mRingtone = null; + } + + setRingtoneSummary(); + } + + private void setRingtoneSummary() { + final String summary = mRingtone != null ? mRingtone.getTitle(mContext) + : mContext.getString(R.string.silent_ringtone); + + mInboxRingtone.setSummary(summary); + } + /** * Listen to all preference changes in this class. * @param preference @@ -338,11 +370,16 @@ public class AccountSettingsFragment extends EmailPreferenceFragment preferenceChanged(PREFERENCE_NAME, summary); } return false; - } else if (key.equals(PREFERENCE_VIBRATE)) { + } else if (FolderPreferences.PreferenceKeys.NOTIFICATION_VIBRATE.equals(key)) { final boolean vibrateSetting = (Boolean) newValue; - mAccountVibrate.setChecked(vibrateSetting); - preferenceChanged(PREFERENCE_VIBRATE, newValue); - return false; + mInboxVibrate.setChecked(vibrateSetting); + mInboxFolderPreferences.setNotificationVibrateEnabled(vibrateSetting); + preferenceChanged(FolderPreferences.PreferenceKeys.NOTIFICATION_VIBRATE, newValue); + return true; + } else if (FolderPreferences.PreferenceKeys.NOTIFICATIONS_ENABLED.equals(key)) { + mInboxFolderPreferences.setNotificationsEnabled((Boolean) newValue); + preferenceChanged(FolderPreferences.PreferenceKeys.NOTIFICATIONS_ENABLED, newValue); + return true; } else { // Default behavior, just indicate that the preferences were written preferenceChanged(key, newValue); @@ -474,7 +511,72 @@ public class AccountSettingsFragment extends EmailPreferenceFragment } /** - * Load account data into preference UI + * Loads settings that are dependent on a {@link com.android.mail.providers.Account}, which + * must be obtained off the main thread. This will also call {@link #loadMainThreadSettings()}. + */ + private void loadSettingsOffMainThread() { + new Thread(new Runnable() { + @Override + public void run() { + final Cursor accountCursor = mContext.getContentResolver().query(EmailProvider + .uiUri("uiaccount", mAccount.mId), UIProvider.ACCOUNTS_PROJECTION, null, + null, null); + + com.android.mail.providers.Account account = null; + try { + if (accountCursor.moveToFirst()) { + account = new com.android.mail.providers.Account(accountCursor); + } + } finally { + accountCursor.close(); + } + + final Cursor folderCursor = mContext.getContentResolver().query( + account.settings.defaultInbox, UIProvider.FOLDERS_PROJECTION, null, null, + null); + + Folder folder = null; + try { + if (folderCursor.moveToFirst()) { + folder = new Folder(folderCursor); + } + } finally { + folderCursor.close(); + } + + mAccountPreferences = new AccountPreferences(mContext, account.name); + mInboxFolderPreferences = + new FolderPreferences(mContext, account.name, folder, true); + + NotificationUtils.moveNotificationSetting( + mAccountPreferences, mInboxFolderPreferences); + + final String ringtoneUri = mInboxFolderPreferences.getNotificationRingtoneUri(); + if (!TextUtils.isEmpty(ringtoneUri)) { + mRingtone = RingtoneManager.getRingtone(getActivity(), Uri.parse(ringtoneUri)); + } + + final Activity activity = getActivity(); + if (activity != null) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + mInboxNotify.setChecked( + mInboxFolderPreferences.areNotificationsEnabled()); + + mInboxVibrate.setChecked( + mInboxFolderPreferences.isNotificationVibrateEnabled()); + + setRingtoneSummary(); + } + }); + } + } + }).start(); + } + + /** + * Load account data into preference UI. This must be called on the main thread. */ private void loadSettings() { // We can only do this once, so prevent repeat @@ -482,6 +584,8 @@ public class AccountSettingsFragment extends EmailPreferenceFragment // Once loaded the data is ready to be saved, as well mSaveOnExit = false; + loadSettingsOffMainThread(); + mAccountDescription = (EditTextPreference) findPreference(PREFERENCE_DESCRIPTION); mAccountDescription.setSummary(mAccount.getDisplayName()); mAccountDescription.setText(mAccount.getDisplayName()); @@ -549,8 +653,7 @@ public class AccountSettingsFragment extends EmailPreferenceFragment PreferenceCategory folderPrefs = (PreferenceCategory) findPreference(PREFERENCE_SYSTEM_FOLDERS); if (info.requiresSetup) { - Preference trashPreference = - (Preference) findPreference(PREFERENCE_SYSTEM_FOLDERS_TRASH); + Preference trashPreference = findPreference(PREFERENCE_SYSTEM_FOLDERS_TRASH); Intent i = new Intent(mContext, FolderPickerActivity.class); Uri uri = EmailContent.CONTENT_URI.buildUpon().appendQueryParameter( "account", Long.toString(mAccount.mId)).build(); @@ -558,8 +661,7 @@ public class AccountSettingsFragment extends EmailPreferenceFragment i.putExtra(FolderPickerActivity.MAILBOX_TYPE_EXTRA, Mailbox.TYPE_TRASH); trashPreference.setIntent(i); - Preference sentPreference = - (Preference) findPreference(PREFERENCE_SYSTEM_FOLDERS_SENT); + Preference sentPreference = findPreference(PREFERENCE_SYSTEM_FOLDERS_SENT); i = new Intent(mContext, FolderPickerActivity.class); i.setData(uri); i.putExtra(FolderPickerActivity.MAILBOX_TYPE_EXTRA, Mailbox.TYPE_SENT); @@ -582,33 +684,35 @@ public class AccountSettingsFragment extends EmailPreferenceFragment mAccountDefault.setChecked(mAccount.mId == mDefaultAccountId); mAccountDefault.setOnPreferenceChangeListener(this); - mAccountNotify = (CheckBoxPreference) findPreference(PREFERENCE_NOTIFY); - mAccountNotify.setChecked(0 != (mAccount.getFlags() & Account.FLAGS_NOTIFY_NEW_MAIL)); - mAccountNotify.setOnPreferenceChangeListener(this); + mInboxNotify = (CheckBoxPreference) findPreference( + FolderPreferences.PreferenceKeys.NOTIFICATIONS_ENABLED); + mInboxNotify.setOnPreferenceChangeListener(this); - mAccountRingtone = (RingtonePreference) findPreference(PREFERENCE_RINGTONE); - mAccountRingtone.setOnPreferenceChangeListener(this); + mInboxRingtone = findPreference(FolderPreferences.PreferenceKeys.NOTIFICATION_RINGTONE); + mInboxRingtone.setOnPreferenceChangeListener(this); + mInboxRingtone.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(final Preference preference) { + showRingtonePicker(); - // The following two lines act as a workaround for the RingtonePreference - // which does not let us set/get the value programmatically - SharedPreferences prefs = mAccountRingtone.getPreferenceManager().getSharedPreferences(); - prefs.edit().putString(PREFERENCE_RINGTONE, mAccount.getRingtone()).apply(); + return true; + } + }); // Set the vibrator value, or hide it on devices w/o a vibrator - mAccountVibrate = (CheckBoxPreference) findPreference(PREFERENCE_VIBRATE); + mInboxVibrate = (CheckBoxPreference) findPreference( + FolderPreferences.PreferenceKeys.NOTIFICATION_VIBRATE); Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); if (vibrator.hasVibrator()) { - // Calculate the value to set based on the choices, and set the value. - final boolean vibrateSetting = 0 != (mAccount.getFlags() & Account.FLAGS_VIBRATE); - mAccountVibrate.setChecked(vibrateSetting); + // Checked state will be set when we obtain it in #loadSettingsOffMainThread() // When the value is changed, update the setting. - mAccountVibrate.setOnPreferenceChangeListener(this); + mInboxVibrate.setOnPreferenceChangeListener(this); } else { // No vibrator present. Remove the preference altogether. PreferenceCategory notificationsCategory = (PreferenceCategory) findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS); - notificationsCategory.removePreference(mAccountVibrate); + notificationsCategory.removePreference(mInboxVibrate); } final Preference retryAccount = findPreference(PREFERENCE_POLICIES_RETRY_ACCOUNT); @@ -725,10 +829,7 @@ public class AccountSettingsFragment extends EmailPreferenceFragment */ private void saveSettings() { // Turn off all controlled flags - will turn them back on while checking UI elements - int newFlags = mAccount.getFlags() & - ~(Account.FLAGS_NOTIFY_NEW_MAIL | - Account.FLAGS_VIBRATE | - Account.FLAGS_BACKGROUND_ATTACHMENTS); + int newFlags = mAccount.getFlags() & ~(Account.FLAGS_BACKGROUND_ATTACHMENTS); newFlags |= mAccountBackgroundAttachments.isChecked() ? Account.FLAGS_BACKGROUND_ATTACHMENTS : 0; @@ -738,16 +839,10 @@ public class AccountSettingsFragment extends EmailPreferenceFragment // The sender name must never be empty (this is enforced by the preference editor) mAccount.setSenderName(mAccountName.getText().trim()); mAccount.setSignature(mAccountSignature.getText()); - newFlags |= mAccountNotify.isChecked() ? Account.FLAGS_NOTIFY_NEW_MAIL : 0; mAccount.setSyncInterval(Integer.parseInt(mCheckFrequency.getValue())); if (mSyncWindow != null) { mAccount.setSyncLookback(Integer.parseInt(mSyncWindow.getValue())); } - if (mAccountVibrate.isChecked()) { - newFlags |= Account.FLAGS_VIBRATE; - } - SharedPreferences prefs = mAccountRingtone.getPreferenceManager().getSharedPreferences(); - mAccount.setRingtone(prefs.getString(PREFERENCE_RINGTONE, null)); mAccount.setFlags(newFlags); EmailServiceInfo info = @@ -779,4 +874,21 @@ public class AccountSettingsFragment extends EmailPreferenceFragment // Get the e-mail address of the account being editted, if this is for an existing account. return mAccountEmail; } + + /** + * Shows the system ringtone picker. + */ + private void showRingtonePicker() { + Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); + final String ringtoneUri = mInboxFolderPreferences.getNotificationRingtoneUri(); + if (!TextUtils.isEmpty(ringtoneUri)) { + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, Uri.parse(ringtoneUri)); + } + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, + Settings.System.DEFAULT_NOTIFICATION_URI); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); + startActivityForResult(intent, RINGTONE_REQUEST_CODE); + } } diff --git a/src/com/android/email/activity/setup/AccountSettingsUtils.java b/src/com/android/email/activity/setup/AccountSettingsUtils.java index 2613a53f6..d1644d974 100644 --- a/src/com/android/email/activity/setup/AccountSettingsUtils.java +++ b/src/com/android/email/activity/setup/AccountSettingsUtils.java @@ -92,7 +92,6 @@ public class AccountSettingsUtils { cv.put(AccountColumns.SENDER_NAME, account.getSenderName()); cv.put(AccountColumns.SIGNATURE, account.getSignature()); cv.put(AccountColumns.SYNC_INTERVAL, account.mSyncInterval); - cv.put(AccountColumns.RINGTONE_URI, account.mRingtoneUri); cv.put(AccountColumns.FLAGS, account.mFlags); cv.put(AccountColumns.SYNC_LOOKBACK, account.mSyncLookback); cv.put(AccountColumns.SECURITY_SYNC_KEY, account.mSecuritySyncKey); diff --git a/src/com/android/email/activity/setup/AccountSetupOptions.java b/src/com/android/email/activity/setup/AccountSetupOptions.java index 6a8ca3536..215d31f04 100644 --- a/src/com/android/email/activity/setup/AccountSetupOptions.java +++ b/src/com/android/email/activity/setup/AccountSetupOptions.java @@ -28,6 +28,7 @@ import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.database.Cursor; import android.os.Bundle; import android.os.RemoteException; import android.util.Log; @@ -40,6 +41,7 @@ import android.widget.Spinner; import com.android.email.R; import com.android.email.activity.ActivityHelper; import com.android.email.activity.UiUtilities; +import com.android.email.provider.EmailProvider; import com.android.email.service.EmailServiceUtils; import com.android.email.service.EmailServiceUtils.EmailServiceInfo; import com.android.email.service.MailService; @@ -50,10 +52,17 @@ import com.android.emailcommon.provider.Policy; import com.android.emailcommon.service.EmailServiceProxy; import com.android.emailcommon.service.SyncWindow; import com.android.emailcommon.utility.Utility; +import com.android.mail.preferences.AccountPreferences; +import com.android.mail.preferences.FolderPreferences; +import com.android.mail.providers.Folder; +import com.android.mail.providers.UIProvider; +import com.android.mail.utils.LogTag; +import com.android.mail.utils.LogUtils; import java.io.IOException; public class AccountSetupOptions extends AccountSetupActivity implements OnClickListener { + private static final String LOG_TAG = LogTag.getLogTag(); private Spinner mCheckFrequencyView; private Spinner mSyncWindowView; @@ -126,8 +135,8 @@ public class AccountSetupOptions extends AccountSetupActivity implements OnClick if (account.mIsDefault || SetupData.isDefault()) { mDefaultView.setChecked(true); } - mNotifyView.setChecked( - (account.getFlags() & Account.FLAGS_NOTIFY_NEW_MAIL) != 0); + + mNotifyView.setChecked(true); // By default, we want notifications on SpinnerOption.setSpinnerOptionValue(mCheckFrequencyView, account.getSyncInterval()); if (mServiceInfo.syncContacts) { @@ -210,11 +219,7 @@ public class AccountSetupOptions extends AccountSetupActivity implements OnClick return; } account.setDisplayName(account.getEmailAddress()); - int newFlags = account.getFlags() & - ~(Account.FLAGS_NOTIFY_NEW_MAIL | Account.FLAGS_BACKGROUND_ATTACHMENTS); - if (mNotifyView.isChecked()) { - newFlags |= Account.FLAGS_NOTIFY_NEW_MAIL; - } + int newFlags = account.getFlags() & ~(Account.FLAGS_BACKGROUND_ATTACHMENTS); if (mServiceInfo.offerAttachmentPreload && mBackgroundAttachmentsView.isChecked()) { newFlags |= Account.FLAGS_BACKGROUND_ATTACHMENTS; } @@ -269,6 +274,12 @@ public class AccountSetupOptions extends AccountSetupActivity implements OnClick AccountSettingsUtils.commitSettings(context, account); MailService.setupAccountManagerAccount(context, account, email2, calendar2, contacts2, mAccountManagerCallback); + + // We can move the notification setting to the inbox FolderPreferences later, once + // we know what the inbox is + final AccountPreferences accountPreferences = + new AccountPreferences(context, account.mEmailAddress); + accountPreferences.setDefaultInboxNotificationsEnabled(mNotifyView.isChecked()); } }); } diff --git a/src/com/android/email/activity/setup/GeneralPreferences.java b/src/com/android/email/activity/setup/GeneralPreferences.java index 12f9680c8..084353824 100644 --- a/src/com/android/email/activity/setup/GeneralPreferences.java +++ b/src/com/android/email/activity/setup/GeneralPreferences.java @@ -29,6 +29,7 @@ import com.android.email.Preferences; import com.android.email.R; import com.android.email.provider.EmailProvider; +import com.android.mail.preferences.MailPrefs; import com.android.mail.utils.Utils; public class GeneralPreferences extends EmailPreferenceFragment implements @@ -41,8 +42,8 @@ public class GeneralPreferences extends EmailPreferenceFragment implements private static final String PREFERENCE_KEY_SWIPE_DELETE = "swipe_delete"; private static final String PREFERENCE_KEY_SHOW_CHECKBOXES = "show_checkboxes"; private static final String PREFERENCE_KEY_CLEAR_TRUSTED_SENDERS = "clear_trusted_senders"; - private static final String PREFERNECE_REPLY_ALL = "reply_all"; + private MailPrefs mMailPrefs; private Preferences mPreferences; private ListPreference mAutoAdvance; /** @@ -64,6 +65,7 @@ public class GeneralPreferences extends EmailPreferenceFragment implements public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mMailPrefs = MailPrefs.get(getActivity()); getPreferenceManager().setSharedPreferencesName(Preferences.PREFERENCES_FILE); // Load the preferences from an XML resource @@ -75,7 +77,7 @@ public class GeneralPreferences extends EmailPreferenceFragment implements // Disabling reply-all on tablets, as this setting is just for phones if (Utils.useTabletUI(getActivity().getResources())) { - ps.removePreference(findPreference(PREFERNECE_REPLY_ALL)); + ps.removePreference(findPreference(MailPrefs.PreferenceKeys.DEFAULT_REPLY_ALL)); } } @@ -108,6 +110,9 @@ public class GeneralPreferences extends EmailPreferenceFragment implements mPreferences.setTextZoom(mTextZoom.findIndexOfValue((String) newValue)); reloadDynamicSummaries(); return true; + } else if (MailPrefs.PreferenceKeys.DEFAULT_REPLY_ALL.equals(key)) { + mMailPrefs.setDefaultReplyAll((Boolean) newValue); + return true; } return false; } @@ -158,6 +163,13 @@ public class GeneralPreferences extends EmailPreferenceFragment implements mShowCheckboxes = (CheckBoxPreference) findPreference(PREFERENCE_KEY_SHOW_CHECKBOXES); mSwipeDelete = (CheckBoxPreference) findPreference(PREFERENCE_KEY_SWIPE_DELETE); + final Preference replyAllPreference = + findPreference(MailPrefs.PreferenceKeys.DEFAULT_REPLY_ALL); + // This preference is removed on tablets + if (replyAllPreference != null) { + replyAllPreference.setOnPreferenceChangeListener(this); + } + reloadDynamicSummaries(); } diff --git a/src/com/android/email/provider/DBHelper.java b/src/com/android/email/provider/DBHelper.java index bfba3f6a2..bba9be001 100644 --- a/src/com/android/email/provider/DBHelper.java +++ b/src/com/android/email/provider/DBHelper.java @@ -131,8 +131,9 @@ public final class DBHelper { // Version 102&103: Add hierarchicalName to Mailbox // Version 104&105: add syncData to Message // Version 106: Add certificate to HostAuth + // Version 107: Add a SEEN column to the message table - public static final int DATABASE_VERSION = 106; + public static final int DATABASE_VERSION = 107; // Any changes to the database format *must* include update-in-place code. // Original version: 2 @@ -178,7 +179,8 @@ public final class DBHelper { + MessageColumns.SNIPPET + " text, " + MessageColumns.PROTOCOL_SEARCH_INFO + " text, " + MessageColumns.THREAD_TOPIC + " text, " - + MessageColumns.SYNC_DATA + " text" + + MessageColumns.SYNC_DATA + " text, " + + MessageColumns.FLAG_SEEN + " integer" + ");"; // This String and the following String MUST have the same columns, except for the type @@ -1012,6 +1014,20 @@ public final class DBHelper { } oldVersion = 106; } + if (oldVersion == 106) { + try { + db.execSQL("alter table " + Message.TABLE_NAME + + " add " + MessageColumns.FLAG_SEEN + " integer"); + db.execSQL("alter table " + Message.UPDATED_TABLE_NAME + + " add " + MessageColumns.FLAG_SEEN + " integer"); + db.execSQL("alter table " + Message.DELETED_TABLE_NAME + + " add " + MessageColumns.FLAG_SEEN + " integer"); + } catch (SQLException e) { + // Shouldn't be needed unless we're debugging and interrupt the process + Log.w(TAG, "Exception upgrading EmailProvider.db from v106 to v107", e); + } + oldVersion = 107; + } } @Override diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index 3117f3d13..3c3476830 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -93,7 +93,6 @@ import com.android.mail.utils.MatrixCursorWithExtra; import com.android.mail.utils.Utils; import com.android.mail.widget.BaseWidgetProvider; import com.android.mail.widget.WidgetProvider; -import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -119,33 +118,29 @@ public class EmailProvider extends ContentProvider { public static String EMAIL_APP_MIME_TYPE; - protected static final String DATABASE_NAME = "EmailProvider.db"; - protected static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; - protected static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db"; - - public static final String ACTION_ATTACHMENT_UPDATED = "com.android.email.ATTACHMENT_UPDATED"; - public static final String ATTACHMENT_UPDATED_EXTRA_FLAGS = - "com.android.email.ATTACHMENT_UPDATED_FLAGS"; + private static final String DATABASE_NAME = "EmailProvider.db"; + private static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; + private static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db"; /** * Notifies that changes happened. Certain UI components, e.g., widgets, can register for this * {@link android.content.Intent} and update accordingly. However, this can be very broad and * is NOT the preferred way of getting notification. */ - public static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED = + private static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED = "com.android.email.MESSAGE_LIST_DATASET_CHANGED"; - public static final String EMAIL_MESSAGE_MIME_TYPE = + private static final String EMAIL_MESSAGE_MIME_TYPE = "vnd.android.cursor.item/email-message"; - public static final String EMAIL_ATTACHMENT_MIME_TYPE = + private static final String EMAIL_ATTACHMENT_MIME_TYPE = "vnd.android.cursor.item/email-attachment"; /** Appended to the notification URI for delete operations */ - public static final String NOTIFICATION_OP_DELETE = "delete"; + private static final String NOTIFICATION_OP_DELETE = "delete"; /** Appended to the notification URI for insert operations */ - public static final String NOTIFICATION_OP_INSERT = "insert"; + private static final String NOTIFICATION_OP_INSERT = "insert"; /** Appended to the notification URI for update operations */ - public static final String NOTIFICATION_OP_UPDATE = "update"; + private static final String NOTIFICATION_OP_UPDATE = "update"; // Definitions for our queries looking for orphaned messages private static final String[] ORPHANS_PROJECTION @@ -339,7 +334,7 @@ public class EmailProvider extends ContentProvider { private static ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; private static final ContentValues EMPTY_CONTENT_VALUES = new ContentValues(); - public static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId"; + private static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId"; // For undo handling private int mLastSequence = -1; @@ -368,10 +363,9 @@ public class EmailProvider extends ContentProvider { return match; } - public static Uri INTEGRITY_CHECK_URI; + private static Uri INTEGRITY_CHECK_URI; public static Uri ACCOUNT_BACKUP_URI; - public static Uri FOLDER_STATUS_URI; - public static Uri FOLDER_REFRESH_URI; + private static Uri FOLDER_STATUS_URI; private SQLiteDatabase mDatabase; private SQLiteDatabase mBodyDatabase; @@ -399,9 +393,8 @@ public class EmailProvider extends ContentProvider { * @param foreignColumn the column in the foreign table whose absence will trigger the deletion * @param foreignTable the foreign table */ - @VisibleForTesting - void deleteUnlinked(SQLiteDatabase db, String table, String column, String foreignColumn, - String foreignTable) { + private static void deleteUnlinked(SQLiteDatabase db, String table, String column, + String foreignColumn, String foreignTable) { int count = db.delete(table, column + " not in (select " + foreignColumn + " from " + foreignTable + ")", null); if (count > 0) { @@ -409,8 +402,7 @@ public class EmailProvider extends ContentProvider { } } - @VisibleForTesting - synchronized SQLiteDatabase getDatabase(Context context) { + private synchronized SQLiteDatabase getDatabase(Context context) { // Always return the cached database, if we've got one if (mDatabase != null) { return mDatabase; @@ -542,15 +534,10 @@ public class EmailProvider extends ContentProvider { } } - /*package*/ static SQLiteDatabase getReadableDatabase(Context context) { - DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME); - return helper.getReadableDatabase(); - } - /** * Restore user Account and HostAuth data from our backup database */ - public static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) { + private static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) { if (MailActivityEmail.DEBUG) { Log.w(TAG, "restoreIfNeeded..."); } @@ -596,7 +583,7 @@ public class EmailProvider extends ContentProvider { } } - /*package*/ static void deleteMessageOrphans(SQLiteDatabase database, String tableName) { + private static void deleteMessageOrphans(SQLiteDatabase database, String tableName) { if (database != null) { // We'll look at all of the items in the table; there won't be many typically Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null); @@ -1061,8 +1048,6 @@ public class EmailProvider extends ContentProvider { Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup"); FOLDER_STATUS_URI = Uri.parse("content://" + EmailContent.AUTHORITY + "/status"); - FOLDER_REFRESH_URI = - Uri.parse("content://" + EmailContent.AUTHORITY + "/refresh"); EMAIL_APP_MIME_TYPE = context.getString(R.string.application_mime_type); } checkDatabases(); @@ -1303,7 +1288,16 @@ public class EmailProvider extends ContentProvider { if (selection != null || selectionArgs != null) { throw new IllegalArgumentException("UI queries can't have selection/args"); } - c = uiQuery(match, uri, projection); + + final String seenParam = uri.getQueryParameter(UIProvider.SEEN_QUERY_PARAMETER); + final boolean unseenOnly; + if (seenParam != null && Boolean.FALSE.toString().equals(seenParam)) { + unseenOnly = true; + } else { + unseenOnly = false; + } + + c = uiQuery(match, uri, projection, unseenOnly); return c; case UI_FOLDERS: c = uiFolders(uri, projection); @@ -1451,7 +1445,7 @@ public class EmailProvider extends ContentProvider { return c; } - private String whereWithId(String id, String selection) { + private static String whereWithId(String id, String selection) { StringBuilder sb = new StringBuilder(256); sb.append("_id="); sb.append(id); @@ -1473,7 +1467,7 @@ public class EmailProvider extends ContentProvider { * @param selection user-provided selection, may be null * @return a single selection string */ - private String whereWith(String where, String selection) { + private static String whereWith(String where, String selection) { if (selection == null) { return where; } @@ -1689,7 +1683,7 @@ outer: case ACCOUNT_PICK_SENT_FOLDER: return pickSentFolder(uri); case UI_FOLDER: - return uiUpdateFolder(uri, values); + return uiUpdateFolder(context, uri, values); case UI_RECENT_FOLDERS: return uiUpdateRecentFolders(uri, values); case UI_DEFAULT_RECENT_FOLDERS: @@ -1933,7 +1927,7 @@ outer: * * @param match The type of content that was modified. */ - private Uri getBaseNotificationUri(int match) { + private static Uri getBaseNotificationUri(int match) { Uri baseUri = null; switch (match) { case MESSAGE: @@ -2013,22 +2007,6 @@ outer: } } - /** - * For testing purposes, check whether a given row is cached - * @param baseUri the base uri of the EmailContent - * @param id the row id of the EmailContent - * @return whether or not the row is currently cached - */ - @VisibleForTesting - protected boolean isCached(Uri baseUri, long id) { - int match = findMatch(baseUri, "isCached"); - int table = match >> BASE_SHIFT; - ContentCache cache = mContentCaches[table]; - if (cache == null) return false; - Cursor cc = cache.get(Long.toString(id)); - return (cc != null); - } - public static interface AttachmentService { /** * Notify the service that an attachment has changed. @@ -2045,44 +2023,31 @@ outer: }; private AttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE; - /** - * Injects a custom attachment service handler. If null is specified, will reset to the - * default service. - */ - public void injectAttachmentService(AttachmentService as) { - mAttachmentService = (as == null) ? DEFAULT_ATTACHMENT_SERVICE : as; + private Cursor notificationQuery(final Uri uri) { + final SQLiteDatabase db = getDatabase(getContext()); + final String accountId = uri.getLastPathSegment(); + + final StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + sqlBuilder.append(MessageColumns.MAILBOX_KEY).append(", "); + sqlBuilder.append("SUM(CASE ") + .append(MessageColumns.FLAG_READ).append(" WHEN 0 THEN 1 ELSE 0 END), "); + sqlBuilder.append("SUM(CASE ") + .append(MessageColumns.FLAG_SEEN).append(" WHEN 0 THEN 1 ELSE 0 END)\n"); + sqlBuilder.append("FROM "); + sqlBuilder.append(Message.TABLE_NAME).append('\n'); + sqlBuilder.append("WHERE "); + sqlBuilder.append(MessageColumns.ACCOUNT_KEY).append(" = ?\n"); + sqlBuilder.append("GROUP BY "); + sqlBuilder.append(MessageColumns.MAILBOX_KEY); + + final String sql = sqlBuilder.toString(); + + final String[] selectionArgs = {accountId}; + + return db.rawQuery(sql, selectionArgs); } - // SELECT DISTINCT Boxes._id, Boxes.unreadCount count(Message._id) from Message, - // (SELECT _id, unreadCount, messageCount, lastNotifiedMessageCount, lastNotifiedMessageKey - // FROM Mailbox WHERE accountKey=6 AND ((type = 0) OR (syncInterval!=0 AND syncInterval!=-1))) - // AS Boxes - // WHERE Boxes.messageCount!=Boxes.lastNotifiedMessageCount - // OR (Boxes._id=Message.mailboxKey AND Message._id>Boxes.lastNotifiedMessageKey) - // TODO: This query can be simplified a bit - private static final String NOTIFICATION_QUERY = - "SELECT DISTINCT Boxes." + MailboxColumns.ID + ", Boxes." + MailboxColumns.UNREAD_COUNT + - ", count(" + Message.TABLE_NAME + "." + MessageColumns.ID + ")" + - " FROM " + - Message.TABLE_NAME + "," + - "(SELECT " + MailboxColumns.ID + "," + MailboxColumns.UNREAD_COUNT + "," + - MailboxColumns.MESSAGE_COUNT + "," + MailboxColumns.LAST_NOTIFIED_MESSAGE_COUNT + - "," + MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY + " FROM " + Mailbox.TABLE_NAME + - " WHERE " + MailboxColumns.ACCOUNT_KEY + "=?" + - " AND (" + MailboxColumns.TYPE + "=" + Mailbox.TYPE_INBOX + " OR (" - + MailboxColumns.SYNC_INTERVAL + "!=0 AND " + - MailboxColumns.SYNC_INTERVAL + "!=-1))) AS Boxes " + - "WHERE Boxes." + MailboxColumns.ID + '=' + Message.TABLE_NAME + "." + - MessageColumns.MAILBOX_KEY + " AND " + Message.TABLE_NAME + "." + - MessageColumns.ID + ">Boxes." + MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY + - " AND " + MessageColumns.FLAG_READ + "=0 AND " + MessageColumns.TIMESTAMP + "!=0"; - - public Cursor notificationQuery(Uri uri) { - SQLiteDatabase db = getDatabase(getContext()); - String accountId = uri.getLastPathSegment(); - return db.rawQuery(NOTIFICATION_QUERY, new String[] {accountId}); - } - public Cursor mostRecentMessageQuery(Uri uri) { SQLiteDatabase db = getDatabase(getContext()); String mailboxId = uri.getLastPathSegment(); @@ -2145,7 +2110,7 @@ outer: * Mapping of UIProvider columns to EmailProvider columns for the message list (called the * conversation list in UnifiedEmail) */ - private ProjectionMap getMessageListMap() { + private static ProjectionMap getMessageListMap() { if (sMessageListMap == null) { sMessageListMap = ProjectionMap.builder() .add(BaseColumns._ID, MessageColumns.ID) @@ -2163,6 +2128,7 @@ outer: .add(UIProvider.ConversationColumns.PRIORITY, Integer.toString(ConversationPriority.LOW)) .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ) + .add(UIProvider.ConversationColumns.SEEN, MessageColumns.FLAG_SEEN) .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE) .add(UIProvider.ConversationColumns.FLAGS, CONVERSATION_FLAGS) .add(UIProvider.ConversationColumns.ACCOUNT_URI, @@ -2197,7 +2163,7 @@ outer: * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in * UnifiedEmail */ - private ProjectionMap getMessageViewMap() { + private static ProjectionMap getMessageViewMap() { if (sMessageViewMap == null) { sMessageViewMap = ProjectionMap.builder() .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID) @@ -2233,6 +2199,7 @@ outer: uriWithColumn("account", MessageColumns.ACCOUNT_KEY)) .add(UIProvider.MessageColumns.STARRED, EmailContent.MessageColumns.FLAG_FAVORITE) .add(UIProvider.MessageColumns.READ, EmailContent.MessageColumns.FLAG_READ) + .add(UIProvider.MessageColumns.SEEN, EmailContent.MessageColumns.FLAG_SEEN) .add(UIProvider.MessageColumns.SPAM_WARNING_STRING, null) .add(UIProvider.MessageColumns.SPAM_WARNING_LEVEL, Integer.toString(UIProvider.SpamWarningLevel.NO_WARNING)) @@ -2274,10 +2241,11 @@ outer: + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + R.drawable.ic_menu_star_holo_light + " ELSE -1 END"; - private ProjectionMap getFolderListMap() { + private static ProjectionMap getFolderListMap() { if (sFolderListMap == null) { sFolderListMap = ProjectionMap.builder() .add(BaseColumns._ID, MailboxColumns.ID) + .add(UIProvider.FolderColumns.PERSISTENT_ID, MailboxColumns.SERVER_ID) .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) .add(UIProvider.FolderColumns.NAME, "displayName") .add(UIProvider.FolderColumns.HAS_CHILDREN, @@ -2304,7 +2272,7 @@ outer: * Constructs the map of default entries for accounts. These values can be overridden in * {@link #genQueryAccount(String[], String)}. */ - private ProjectionMap getAccountListMap() { + private static ProjectionMap getAccountListMap() { if (sAccountListMap == null) { sAccountListMap = ProjectionMap.builder() .add(BaseColumns._ID, AccountColumns.ID) @@ -2355,7 +2323,7 @@ outer: /** * Mapping of UIProvider columns to EmailProvider columns for a message's attachments */ - private ProjectionMap getAttachmentMap() { + private static ProjectionMap getAttachmentMap() { if (sAttachmentMap == null) { sAttachmentMap = ProjectionMap.builder() .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME) @@ -2377,14 +2345,14 @@ outer: * Generate the SELECT clause using a specified mapping and the original UI projection * @param map the ProjectionMap to use for this projection * @param projection the projection as sent by UnifiedEmail - * @param values ContentValues to be used if the ProjectionMap entry is null * @return a StringBuilder containing the SELECT expression for a SQLite query */ - private StringBuilder genSelect(ProjectionMap map, String[] projection) { + private static StringBuilder genSelect(ProjectionMap map, String[] projection) { return genSelect(map, projection, EMPTY_CONTENT_VALUES); } - private StringBuilder genSelect(ProjectionMap map, String[] projection, ContentValues values) { + private static StringBuilder genSelect(ProjectionMap map, String[] projection, + ContentValues values) { StringBuilder sb = new StringBuilder("SELECT "); boolean first = true; for (String column: projection) { @@ -2545,12 +2513,16 @@ outer: * Generate the "message list" SQLite query, given a projection from UnifiedEmail * * @param uiProjection as passed from UnifiedEmail + * @param unseenOnly true to only return unseen messages * @return the SQLite query to be executed on the EmailProvider database */ - private String genQueryMailboxMessages(String[] uiProjection) { + private static String genQueryMailboxMessages(String[] uiProjection, final boolean unseenOnly) { StringBuilder sb = genSelect(getMessageListMap(), uiProjection); - sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.MAILBOX_KEY + "=? ORDER BY " + - MessageColumns.TIMESTAMP + " DESC"); + sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.MAILBOX_KEY + "=? "); + if (unseenOnly) { + sb.append("AND ").append(MessageColumns.FLAG_SEEN).append(" = 0 "); + } + sb.append("ORDER BY " + MessageColumns.TIMESTAMP + " DESC"); return sb.toString(); } @@ -2558,11 +2530,12 @@ outer: * Generate various virtual mailbox SQLite queries, given a projection from UnifiedEmail * * @param uiProjection as passed from UnifiedEmail - * @param id the id of the virtual mailbox + * @param mailboxId the id of the virtual mailbox + * @param unseenOnly true to only return unseen messages * @return the SQLite query to be executed on the EmailProvider database */ - private Cursor getVirtualMailboxMessagesCursor(SQLiteDatabase db, String[] uiProjection, - long mailboxId) { + private static Cursor getVirtualMailboxMessagesCursor(SQLiteDatabase db, String[] uiProjection, + long mailboxId, final boolean unseenOnly) { ContentValues values = new ContentValues(); values.put(UIProvider.ConversationColumns.COLOR, CONVERSATION_COLOR); StringBuilder sb = genSelect(getMessageListMap(), uiProjection, values); @@ -2572,13 +2545,19 @@ outer: sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + - "=" + Mailbox.TYPE_INBOX + ") ORDER BY " + MessageColumns.TIMESTAMP + - " DESC"); + "=" + Mailbox.TYPE_INBOX + ") "); + if (unseenOnly) { + sb.append("AND ").append(MessageColumns.FLAG_SEEN).append(" = 0 "); + } + sb.append("ORDER BY " + MessageColumns.TIMESTAMP + " DESC"); break; case Mailbox.TYPE_STARRED: sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + - MessageColumns.FLAG_FAVORITE + "=1 ORDER BY " + - MessageColumns.TIMESTAMP + " DESC"); + MessageColumns.FLAG_FAVORITE + "=1 "); + if (unseenOnly) { + sb.append("AND ").append(MessageColumns.FLAG_SEEN).append(" = 0 "); + } + sb.append("ORDER BY " + MessageColumns.TIMESTAMP + " DESC"); break; default: throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); @@ -2606,7 +2585,7 @@ outer: * @param uiProjection as passed from UnifiedEmail * @return the SQLite query to be executed on the EmailProvider database */ - private String genQueryConversation(String[] uiProjection) { + private static String genQueryConversation(String[] uiProjection) { StringBuilder sb = genSelect(getMessageListMap(), uiProjection); sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.RECORD_ID + "=?"); return sb.toString(); @@ -2618,7 +2597,7 @@ outer: * @param uiProjection as passed from UnifiedEmail * @return the SQLite query to be executed on the EmailProvider database */ - private String genQueryAccountMailboxes(String[] uiProjection) { + private static String genQueryAccountMailboxes(String[] uiProjection) { StringBuilder sb = genSelect(getFolderListMap(), uiProjection); sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + @@ -2634,7 +2613,7 @@ outer: * @param uiProjection as passed from UnifiedEmail * @return the SQLite query to be executed on the EmailProvider database */ - private String genQueryAccountAllMailboxes(String[] uiProjection) { + private static String genQueryAccountAllMailboxes(String[] uiProjection) { StringBuilder sb = genSelect(getFolderListMap(), uiProjection); // Use a derived column to choose either hierarchicalName or displayName sb.append(", case when " + MailboxColumns.HIERARCHICAL_NAME + " is null then " + @@ -2653,7 +2632,7 @@ outer: * @param uiProjection as passed from UnifiedEmail * @return the SQLite query to be executed on the EmailProvider database */ - private String genQueryRecentMailboxes(String[] uiProjection) { + private static String genQueryRecentMailboxes(String[] uiProjection) { StringBuilder sb = genSelect(getFolderListMap(), uiProjection); sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + @@ -2663,7 +2642,8 @@ outer: return sb.toString(); } - private int getFolderCapabilities(EmailServiceInfo info, int flags, int type, long mailboxId) { + private static int getFolderCapabilities(EmailServiceInfo info, int flags, int type, + long mailboxId) { // All folders support delete int caps = UIProvider.FolderCapabilities.DELETE; if (info != null && info.offerLookback) { @@ -2739,7 +2719,7 @@ outer: .appendQueryParameter("account", account).build().toString(); } - private String getBits(int bitField) { + private static String getBits(int bitField) { StringBuilder sb = new StringBuilder(" "); for (int i = 0; i < 32; i++, bitField >>= 1) { if ((bitField & 1) != 0) { @@ -2833,7 +2813,7 @@ outer: values.put(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE, textZoomToUiValue(textZoom)); } - // Set default inbox, if we've got an inbox; otherwise, say initial sync needed + // Set default inbox, if we've got an inbox; otherwise, say initial sync needed long mailboxId = Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_INBOX); if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX) && mailboxId != Mailbox.NO_MAILBOX) { @@ -2878,7 +2858,7 @@ outer: return sb.toString(); } - private int autoAdvanceToUiValue(int autoAdvance) { + private static int autoAdvanceToUiValue(int autoAdvance) { switch(autoAdvance) { case Preferences.AUTO_ADVANCE_OLDER: return UIProvider.AutoAdvance.OLDER; @@ -2890,7 +2870,7 @@ outer: } } - private int textZoomToUiValue(int textZoom) { + private static int textZoomToUiValue(int textZoom) { switch(textZoom) { case Preferences.TEXT_ZOOM_HUGE: return UIProvider.MessageTextSize.HUGE; @@ -3130,7 +3110,7 @@ outer: * or null if there are no query parameters * @return the SQLite query to be executed on the EmailProvider database */ - private String genQueryAttachments(String[] uiProjection, + private static String genQueryAttachments(String[] uiProjection, List contentTypeQueryParameters) { StringBuilder sb = genSelect(getAttachmentMap(), uiProjection); sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.MESSAGE_KEY + @@ -3180,7 +3160,7 @@ outer: * @param uiProjection as passed from UnifiedEmail * @return the SQLite query to be executed on the EmailProvider database */ - private String genQuerySubfolders(String[] uiProjection) { + private static String genQuerySubfolders(String[] uiProjection) { StringBuilder sb = genSelect(getFolderListMap(), uiProjection); sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY + " =? ORDER BY "); @@ -3324,12 +3304,21 @@ outer: if (params.containsKey( UIProvider.ConversationCursorCommand.COMMAND_KEY_ENTERED_FOLDER)) { Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId); - if (mailbox != null && mailbox.mVisibleLimit > 0) { - ContentValues values = new ContentValues(); - values.put(MailboxColumns.VISIBLE_LIMIT, 0); - mContext.getContentResolver().update( - ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId), - values, null, null); + if (mailbox != null) { + if (mailbox.mVisibleLimit > 0) { + ContentValues values = new ContentValues(); + values.put(MailboxColumns.VISIBLE_LIMIT, 0); + mContext.getContentResolver().update(ContentUris.withAppendedId( + Mailbox.CONTENT_URI, mMailboxId), values, null, null); + } + + // Mark all messages as seen + final ContentValues contentValues = new ContentValues(1); + contentValues.put(MessageColumns.FLAG_SEEN, true); + final Uri uri = EmailContent.Message.CONTENT_URI; + mContext.getContentResolver().update(uri, contentValues, + MessageColumns.MAILBOX_KEY + " = ?", + new String[] {String.valueOf(mailbox.mId)}); } } } @@ -3388,7 +3377,7 @@ outer: * We need to do individual queries for the mailboxes in order to get correct * folder capabilities. */ - Cursor getFolderListCursor(SQLiteDatabase db, Cursor c, String[] uiProjection) { + private Cursor getFolderListCursor(SQLiteDatabase db, Cursor c, String[] uiProjection) { final MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection); Object[] values = new Object[uiProjection.length]; String[] args = new String[1]; @@ -3417,9 +3406,10 @@ outer: * @param match the UriMatcher match for the original uri passed in from UnifiedEmail * @param uri the original uri passed in from UnifiedEmail * @param uiProjection the projection passed in from UnifiedEmail + * @param unseenOnly true to only return unseen messages (where supported) * @return the result Cursor */ - private Cursor uiQuery(int match, Uri uri, String[] uiProjection) { + private Cursor uiQuery(int match, Uri uri, String[] uiProjection, final boolean unseenOnly) { Context context = getContext(); ContentResolver resolver = context.getContentResolver(); SQLiteDatabase db = getDatabase(context); @@ -3443,9 +3433,10 @@ outer: case UI_MESSAGES: long mailboxId = Long.parseLong(id); if (isVirtualMailbox(mailboxId)) { - c = getVirtualMailboxMessagesCursor(db, uiProjection, mailboxId); + c = getVirtualMailboxMessagesCursor(db, uiProjection, mailboxId, unseenOnly); } else { - c = db.rawQuery(genQueryMailboxMessages(uiProjection), new String[] {id}); + c = db.rawQuery( + genQueryMailboxMessages(uiProjection, unseenOnly), new String[] {id}); } notifyUri = UIPROVIDER_CONVERSATION_NOTIFIER.buildUpon().appendPath(id).build(); c = new VisibilityCursor(context, c, mailboxId); @@ -3508,7 +3499,7 @@ outer: * @param uiAtt the UIProvider attachment to convert * @return the EmailProvider attachment */ - private Attachment convertUiAttachmentToAttachment( + private static Attachment convertUiAttachmentToAttachment( com.android.mail.providers.Attachment uiAtt) { Attachment att = new Attachment(); att.setContentUri(uiAtt.contentUri.toString()); @@ -3572,7 +3563,7 @@ outer: /** * Given an account name and a mailbox type, return that mailbox, creating it if necessary - * @param accountName the account name to use + * @param accountId the account id to use * @param mailboxType the type of mailbox we're trying to find * @return the mailbox of the given type for the account in the uri, or null if not found */ @@ -3619,6 +3610,7 @@ outer: msg.mDisplayName = msg.mTo; msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; msg.mFlagRead = true; + msg.mFlagSeen = true; Integer quoteStartPos = values.getAsInteger(UIProvider.MessageColumns.QUOTE_START_POS); msg.mQuotedTextStartPos = quoteStartPos == null ? 0 : quoteStartPos; int flags = 0; @@ -3629,7 +3621,7 @@ outer: break; case DraftType.REPLY_ALL: flags |= Message.FLAG_TYPE_REPLY_ALL; - // Fall through + //$FALL-THROUGH$ case DraftType.REPLY: flags |= Message.FLAG_TYPE_REPLY; break; @@ -3818,7 +3810,8 @@ outer: return 1; } - private void putIntegerLongOrBoolean(ContentValues values, String columnName, Object value) { + private static void putIntegerLongOrBoolean(ContentValues values, String columnName, + Object value) { if (value instanceof Integer) { Integer intValue = (Integer)value; values.put(columnName, intValue); @@ -3836,7 +3829,7 @@ outer: * @param folders * @return number of folders updated */ - private int updateTimestamp(final Context context, String id, Uri[] folders){ + private static int updateTimestamp(final Context context, String id, Uri[] folders){ int updated = 0; final long now = System.currentTimeMillis(); final ContentResolver resolver = context.getContentResolver(); @@ -3941,8 +3934,23 @@ outer: return 0; } - private int uiUpdateFolder(Uri uri, ContentValues uiValues) { - Uri ourUri = convertToEmailProviderUri(uri, Mailbox.CONTENT_URI, true); + private int uiUpdateFolder(final Context context, Uri uri, ContentValues uiValues) { + // We need to mark seen separately + if (uiValues.containsKey(UIProvider.ConversationColumns.SEEN)) { + final int seenValue = uiValues.getAsInteger(UIProvider.ConversationColumns.SEEN); + + if (seenValue == 1) { + final String mailboxId = uri.getLastPathSegment(); + final int rows = markAllSeen(context, mailboxId); + + if (uiValues.size() == 1) { + // Nothing else to do, so return this value + return rows; + } + } + } + + final Uri ourUri = convertToEmailProviderUri(uri, Mailbox.CONTENT_URI, true); if (ourUri == null) return 0; ContentValues ourValues = new ContentValues(); // This should only be called via update to "recent folders" @@ -3954,6 +3962,17 @@ outer: return update(ourUri, ourValues, null, null); } + private int markAllSeen(final Context context, final String mailboxId) { + final SQLiteDatabase db = getDatabase(context); + final String table = Message.TABLE_NAME; + final ContentValues values = new ContentValues(1); + values.put(MessageColumns.FLAG_SEEN, 1); + final String whereClause = MessageColumns.MAILBOX_KEY + " = ?"; + final String[] whereArgs = new String[] {mailboxId}; + + return db.update(table, values, whereClause, whereArgs); + } + private ContentValues convertUiMessageValues(Message message, ContentValues values) { ContentValues ourValues = new ContentValues(); for (String columnName : values.keySet()) { @@ -3962,6 +3981,8 @@ outer: putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val); } else if (columnName.equals(UIProvider.ConversationColumns.READ)) { putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val); + } else if (columnName.equals(UIProvider.ConversationColumns.SEEN)) { + putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_SEEN, val); } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) { putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val); } else if (columnName.equals(UIProvider.ConversationColumns.RAW_FOLDERS)) { @@ -3994,7 +4015,7 @@ outer: return ourValues; } - private Uri convertToEmailProviderUri(Uri uri, Uri newBaseUri, boolean asProvider) { + private static Uri convertToEmailProviderUri(Uri uri, Uri newBaseUri, boolean asProvider) { String idString = uri.getLastPathSegment(); try { long id = Long.parseLong(idString); @@ -4034,7 +4055,7 @@ outer: } // TODO: This should depend on flags on the mailbox... - private boolean uploadsToServer(Context context, Mailbox m) { + private static boolean uploadsToServer(Context context, Mailbox m) { if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX || m.mType == Mailbox.TYPE_SEARCH) { return false; @@ -4084,6 +4105,8 @@ outer: undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey); } else if (columnName.equals(MessageColumns.FLAG_READ)) { undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead); + } else if (columnName.equals(MessageColumns.FLAG_SEEN)) { + undoValues.put(MessageColumns.FLAG_SEEN, msg.mFlagSeen); } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) { undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite); } @@ -4403,7 +4426,7 @@ outer: // This will look just like a "normal" folder return uiQuery(UI_FOLDER, ContentUris.withAppendedId(Mailbox.CONTENT_URI, - searchMailbox.mId), projection); + searchMailbox.mId), projection, false); } private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?"; @@ -4454,7 +4477,7 @@ outer: return 1; } - private void deleteAccountData(Context context, long accountId) { + private static void deleteAccountData(Context context, long accountId) { // Delete synced attachments AttachmentUtilities.deleteAllAccountAttachmentFiles(context, accountId); diff --git a/src/com/android/email/provider/WidgetProvider.java b/src/com/android/email/provider/WidgetProvider.java index 5920ee944..f94a25da2 100644 --- a/src/com/android/email/provider/WidgetProvider.java +++ b/src/com/android/email/provider/WidgetProvider.java @@ -16,21 +16,16 @@ package com.android.email.provider; -import android.appwidget.AppWidgetManager; import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; -import android.database.CursorWrapper; import android.net.Uri; -import android.provider.BaseColumns; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.Mailbox; import com.android.mail.providers.Folder; import com.android.mail.providers.UIProvider; -import com.android.mail.providers.UIProvider.AccountColumns; import com.android.mail.utils.LogTag; import com.android.mail.utils.LogUtils; import com.android.mail.widget.BaseWidgetProvider; @@ -43,56 +38,6 @@ public class WidgetProvider extends BaseWidgetProvider { private static final String LOG_TAG = LogTag.getLogTag(); - // This projection is needed, as if we were to request the capabilities of the account, - // that provider attempts to bind to the email service to get this information. It is not - // valid to bind to a service in a broadcast receiver, as the bind just blocks, for the amount - // of time specified in the timeout. - // Instead, this projection doesn't include the capabilities column. The cursor wrapper then - // makes sure that the Account objects can find all of the columns it expects. - private static final String[] WIDGET_ACCOUNTS_PROJECTION = { - BaseColumns._ID, - AccountColumns.NAME, - AccountColumns.PROVIDER_VERSION, - AccountColumns.URI, - AccountColumns.FOLDER_LIST_URI, - AccountColumns.FULL_FOLDER_LIST_URI, - AccountColumns.SEARCH_URI, - AccountColumns.ACCOUNT_FROM_ADDRESSES, - AccountColumns.SAVE_DRAFT_URI, - AccountColumns.SEND_MAIL_URI, - AccountColumns.EXPUNGE_MESSAGE_URI, - AccountColumns.UNDO_URI, - AccountColumns.SETTINGS_INTENT_URI, - AccountColumns.SYNC_STATUS, - AccountColumns.HELP_INTENT_URI, - AccountColumns.SEND_FEEDBACK_INTENT_URI, - AccountColumns.REAUTHENTICATION_INTENT_URI, - AccountColumns.COMPOSE_URI, - AccountColumns.MIME_TYPE, - AccountColumns.RECENT_FOLDER_LIST_URI, - AccountColumns.COLOR, - AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI, - AccountColumns.MANUAL_SYNC_URI, - AccountColumns.VIEW_INTENT_PROXY_URI, - AccountColumns.SettingsColumns.SIGNATURE, - AccountColumns.SettingsColumns.AUTO_ADVANCE, - AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE, - AccountColumns.SettingsColumns.SNAP_HEADERS, - AccountColumns.SettingsColumns.REPLY_BEHAVIOR, - AccountColumns.SettingsColumns.SHOW_CHECKBOXES, - AccountColumns.SettingsColumns.CONFIRM_DELETE, - AccountColumns.SettingsColumns.CONFIRM_ARCHIVE, - AccountColumns.SettingsColumns.CONFIRM_SEND, - AccountColumns.SettingsColumns.DEFAULT_INBOX, - AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME, - AccountColumns.SettingsColumns.FORCE_REPLY_FROM_DEFAULT, - AccountColumns.SettingsColumns.MAX_ATTACHMENT_SIZE, - AccountColumns.SettingsColumns.SWIPE, - AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED, - AccountColumns.SettingsColumns.SETUP_INTENT_URI - }; - - /** * Remove preferences when deleting widget */ @@ -115,10 +60,10 @@ public class WidgetProvider extends BaseWidgetProvider { protected com.android.mail.providers.Account getAccountObject( Context context, String accountUri) { final ContentResolver resolver = context.getContentResolver(); - final Cursor sparseAccountCursor = resolver.query(Uri.parse(accountUri), - WIDGET_ACCOUNTS_PROJECTION, null, null, null); + final Cursor accountCursor = resolver.query(Uri.parse(accountUri), + UIProvider.ACCOUNTS_PROJECTION_NO_CAPABILITIES, null, null, null); - return getPopulatedAccountObject(sparseAccountCursor); + return getPopulatedAccountObject(accountCursor); } @@ -126,13 +71,13 @@ public class WidgetProvider extends BaseWidgetProvider { protected boolean isAccountValid(Context context, com.android.mail.providers.Account account) { if (account != null) { final ContentResolver resolver = context.getContentResolver(); - final Cursor sparseAccountCursor = resolver.query(account.uri, - WIDGET_ACCOUNTS_PROJECTION, null, null, null); - if (sparseAccountCursor != null) { + final Cursor accountCursor = resolver.query(account.uri, + UIProvider.ACCOUNTS_PROJECTION_NO_CAPABILITIES, null, null, null); + if (accountCursor != null) { try { - return sparseAccountCursor.getCount() > 0; + return accountCursor.getCount() > 0; } finally { - sparseAccountCursor.close(); + accountCursor.close(); } } } @@ -169,24 +114,23 @@ public class WidgetProvider extends BaseWidgetProvider { editor.apply(); } - private com.android.mail.providers.Account getAccount(Context context, long accountId) { + private static com.android.mail.providers.Account getAccount(Context context, long accountId) { final ContentResolver resolver = context.getContentResolver(); final Cursor ac = resolver.query(EmailProvider.uiUri("uiaccount", accountId), - WIDGET_ACCOUNTS_PROJECTION, null, null, null); + UIProvider.ACCOUNTS_PROJECTION_NO_CAPABILITIES, null, null, null); com.android.mail.providers.Account uiAccount = getPopulatedAccountObject(ac); return uiAccount; } - private com.android.mail.providers.Account getPopulatedAccountObject(final Cursor ac) { - if (ac == null) { + private static com.android.mail.providers.Account getPopulatedAccountObject( + final Cursor accountCursor) { + if (accountCursor == null) { LogUtils.e(LOG_TAG, "Null account cursor"); return null; } - final Cursor accountCursor = new SparseAccountCursorWrapper(ac); - com.android.mail.providers.Account uiAccount = null; try { if (accountCursor.moveToFirst()) { @@ -198,7 +142,7 @@ public class WidgetProvider extends BaseWidgetProvider { return uiAccount; } - private Folder getFolder(Context context, long mailboxId) { + private static Folder getFolder(Context context, long mailboxId) { final ContentResolver resolver = context.getContentResolver(); final Cursor fc = resolver.query(EmailProvider.uiUri("uifolder", mailboxId), UIProvider.FOLDERS_PROJECTION, null, null, null); @@ -240,64 +184,4 @@ public class WidgetProvider extends BaseWidgetProvider { long mailboxId = prefs.getLong(LEGACY_MAILBOX_ID_PREFIX + appWidgetId, Mailbox.NO_MAILBOX); return mailboxId; } - - private class SparseAccountCursorWrapper extends CursorWrapper { - public SparseAccountCursorWrapper(Cursor cursor) { - super(cursor); - } - - @Override - public int getColumnCount () { - return UIProvider.ACCOUNTS_PROJECTION.length; - } - - @Override - public int getColumnIndex (String columnName) { - for (int i = 0; i < UIProvider.ACCOUNTS_PROJECTION.length; i++) { - if (UIProvider.ACCOUNTS_PROJECTION[i].equals(columnName)) { - return i; - } - } - return -1; - } - - @Override - public String getColumnName (int columnIndex) { - return UIProvider.ACCOUNTS_PROJECTION[columnIndex]; - } - - @Override - public String[] getColumnNames () { - return UIProvider.ACCOUNTS_PROJECTION; - } - - @Override - public int getInt (int columnIndex) { - if (columnIndex == UIProvider.ACCOUNT_CAPABILITIES_COLUMN) { - return 0; - } - return super.getInt(convertColumnIndex(columnIndex)); - } - - @Override - public long getLong (int columnIndex) { - return super.getLong(convertColumnIndex(columnIndex)); - } - - @Override - public String getString (int columnIndex) { - return super.getString(convertColumnIndex(columnIndex)); - } - - private int convertColumnIndex(int columnIndex) { - // Since this sparse cursor doesn't have the capabilities column, - // we need to adjust all of the column indexes that come after where the - // capabilities column should be - if (columnIndex > UIProvider.ACCOUNT_CAPABILITIES_COLUMN) { - return columnIndex - 1; - } - return columnIndex; - } - } - } diff --git a/src/com/android/email/service/EmailBroadcastProcessorService.java b/src/com/android/email/service/EmailBroadcastProcessorService.java index 442feac57..e5d3d3755 100644 --- a/src/com/android/email/service/EmailBroadcastProcessorService.java +++ b/src/com/android/email/service/EmailBroadcastProcessorService.java @@ -29,7 +29,6 @@ import android.database.Cursor; import android.net.Uri; import android.util.Log; -import com.android.email.NotificationController; import com.android.email.Preferences; import com.android.email.R; import com.android.email.SecurityPolicy; @@ -65,10 +64,6 @@ public class EmailBroadcastProcessorService extends IntentService { private static final String ACTION_DEVICE_POLICY_ADMIN = "com.android.email.devicepolicy"; private static final String EXTRA_DEVICE_POLICY_ADMIN = "message_code"; - // Broadcast received to initiate new message notification updates - public static final String ACTION_NOTIFY_NEW_MAIL = - "com.android.mail.action.update_notification"; - public EmailBroadcastProcessorService() { // Class name will be the thread name. super(EmailBroadcastProcessorService.class.getName()); @@ -116,8 +111,6 @@ public class EmailBroadcastProcessorService extends IntentService { AccountSettings.actionSettingsWithDebug(this); } else if (AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(broadcastAction)) { onSystemAccountChanged(); - } else if (ACTION_NOTIFY_NEW_MAIL.equals(broadcastAction)) { - NotificationController.notifyNewMail(this, broadcastIntent); } } else if (ACTION_DEVICE_POLICY_ADMIN.equals(action)) { int message = intent.getIntExtra(EXTRA_DEVICE_POLICY_ADMIN, -1); diff --git a/src/com/android/email2/ui/MailActivityEmail.java b/src/com/android/email2/ui/MailActivityEmail.java index 7a0870c3a..c9c7612a7 100644 --- a/src/com/android/email2/ui/MailActivityEmail.java +++ b/src/com/android/email2/ui/MailActivityEmail.java @@ -144,7 +144,7 @@ public class MailActivityEmail extends com.android.mail.ui.MailActivity { // Start/stop the various services depending on whether there are any accounts startOrStopService(enabled, context, new Intent(context, AttachmentDownloadService.class)); - NotificationController.getInstance(context).watchForMessages(enabled); + NotificationController.getInstance(context).watchForMessages(); } /** diff --git a/src/com/android/mail/preferences/PreferenceMigrator.java b/src/com/android/mail/preferences/PreferenceMigrator.java new file mode 100644 index 000000000..d5ef3688c --- /dev/null +++ b/src/com/android/mail/preferences/PreferenceMigrator.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.mail.preferences; + +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.preference.PreferenceManager; + +import com.android.emailcommon.provider.EmailContent; +import com.android.mail.providers.Account; +import com.android.mail.providers.Folder; +import com.android.mail.providers.UIProvider; +import com.android.mail.utils.LogTag; +import com.android.mail.utils.LogUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Migrates Email settings to UnifiedEmail + */ +public class PreferenceMigrator extends BasePreferenceMigrator { + private static final String LOG_TAG = LogTag.getLogTag(); + + @Override + protected void migrate(final Context context, final int oldVersion, final int newVersion) { + final List accounts = new ArrayList(); + + final Cursor accountCursor = context.getContentResolver().query(Uri.parse( + EmailContent.CONTENT_URI + "/uiaccts"), + UIProvider.ACCOUNTS_PROJECTION_NO_CAPABILITIES, null, null, null); + try { + while (accountCursor.moveToNext()) { + accounts.add(new Account(accountCursor)); + } + } finally { + accountCursor.close(); + } + + migrate(context, oldVersion, newVersion, accounts); + } + + public static final String REPLY_ALL = "reply_all"; + + private static final String PREFERENCE_NOTIFY = "account_notify"; + private static final String PREFERENCE_VIBRATE = "account_settings_vibrate"; + private static final String PREFERENCE_VIBRATE_OLD = "account_settings_vibrate_when"; + private static final String PREFERENCE_RINGTONE = "account_ringtone"; + + protected static void migrate(final Context context, final int oldVersion, final int newVersion, + final List accounts) { + if (oldVersion < 1) { + // No global settings to move yet + + // Move folder notification settings + for (final Account account : accounts) { + // The only setting in AccountPreferences so far is a global notification toggle, + // but we only allow Inbox notifications, so it will remain unused + final Cursor folderCursor = + context.getContentResolver().query(account.settings.defaultInbox, + UIProvider.FOLDERS_PROJECTION, null, null, null); + + if (folderCursor == null) { + LogUtils.e(LOG_TAG, "Null folder cursor for mailbox %s", + account.settings.defaultInbox); + continue; + } + + Folder folder = null; + try { + if (folderCursor.moveToFirst()) { + folder = new Folder(folderCursor); + } + } finally { + folderCursor.close(); + } + + final FolderPreferences folderPreferences = + new FolderPreferences(context, account.name, folder, true /* inbox */); + + final SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(context); + + if (sharedPreferences.contains(PREFERENCE_NOTIFY)) { + final boolean notify = sharedPreferences.getBoolean(PREFERENCE_NOTIFY, true); + folderPreferences.setNotificationsEnabled(notify); + } + + if (sharedPreferences.contains(PREFERENCE_RINGTONE)) { + final String ringtoneUri = + sharedPreferences.getString(PREFERENCE_RINGTONE, null); + folderPreferences.setNotificationRingtoneUri(ringtoneUri); + } + + if (sharedPreferences.contains(PREFERENCE_VIBRATE)) { + final boolean vibrate = sharedPreferences.getBoolean(PREFERENCE_VIBRATE, false); + folderPreferences.setNotificationVibrateEnabled(vibrate); + } else if (sharedPreferences.contains(PREFERENCE_VIBRATE_OLD)) { + final boolean vibrate = "always".equals( + sharedPreferences.getString(PREFERENCE_VIBRATE_OLD, "")); + folderPreferences.setNotificationVibrateEnabled(vibrate); + } + } + } + } +}