diff --git a/res/values/strings.xml b/res/values/strings.xml index 5629c927f..076898ba1 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -187,11 +187,11 @@ Combined Inbox - Starred + All Starred - Drafts + All Drafts - Outbox + Combined Outbox Please longpress an account to refresh it diff --git a/src/com/android/email/Utility.java b/src/com/android/email/Utility.java index ec669dcc1..7495ea1ba 100644 --- a/src/com/android/email/Utility.java +++ b/src/com/android/email/Utility.java @@ -35,11 +35,13 @@ import android.database.Cursor; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; +import android.os.Parcelable; import android.security.MessageDigest; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Base64; import android.util.Log; +import android.widget.AbsListView; import android.widget.TextView; import android.widget.Toast; @@ -316,18 +318,24 @@ public class Utility { return selection.toString(); } + // TODO When the UI is settled, cache all strings/drawables + // TODO When the UI is settled, write up tests + // TODO When the UI is settled, remove backward-compatibility methods public static class FolderProperties { private static FolderProperties sInstance; + private final Context mContext; + // Caches for frequently accessed resources. - private String[] mSpecialMailbox = new String[] {}; - private TypedArray mSpecialMailboxDrawable; - private Drawable mDefaultMailboxDrawable; - private Drawable mSummaryStarredMailboxDrawable; - private Drawable mSummaryCombinedInboxDrawable; + private final String[] mSpecialMailbox; + private final TypedArray mSpecialMailboxDrawable; + private final Drawable mDefaultMailboxDrawable; + private final Drawable mSummaryStarredMailboxDrawable; + private final Drawable mSummaryCombinedInboxDrawable; private FolderProperties(Context context) { + mContext = context.getApplicationContext(); mSpecialMailbox = context.getResources().getStringArray(R.array.mailbox_display_names); for (int i = 0; i < mSpecialMailbox.length; ++i) { if ("".equals(mSpecialMailbox[i])) { @@ -352,12 +360,41 @@ public class Utility { return sInstance; } + // For backward compatibility. + public String getDisplayName(int type) { + return getDisplayName(type, -1); + } + + // For backward compatibility. + public Drawable getSummaryMailboxIconIds(long id) { + return getIcon(-1, id); + } + + public Drawable getIconIds(int type) { + return getIcon(type, -1); + } + /** * Lookup names of localized special mailboxes - * @param type - * @return Localized strings */ - public String getDisplayName(int type) { + public String getDisplayName(int type, long mailboxId) { + // Special combined mailboxes + int resId = 0; + + // Can't use long for switch!? + if (mailboxId == Mailbox.QUERY_ALL_INBOXES) { + resId = R.string.account_folder_list_summary_inbox; + } else if (mailboxId == Mailbox.QUERY_ALL_FAVORITES) { + resId = R.string.account_folder_list_summary_starred; + } else if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) { + resId = R.string.account_folder_list_summary_drafts; + } else if (mailboxId == Mailbox.QUERY_ALL_OUTBOX) { + resId = R.string.account_folder_list_summary_outbox; + } + if (resId != 0) { + return mContext.getString(resId); + } + if (type < mSpecialMailbox.length) { return mSpecialMailbox[type]; } @@ -366,26 +403,20 @@ public class Utility { /** * Lookup icons of special mailboxes - * @param type - * @return icon's drawable */ - public Drawable getIconIds(int type) { - if (type < mSpecialMailboxDrawable.length()) { - return mSpecialMailboxDrawable.getDrawable(type); - } - return mDefaultMailboxDrawable; - } - - public Drawable getSummaryMailboxIconIds(long mailboxKey) { - if (mailboxKey == Mailbox.QUERY_ALL_INBOXES) { + public Drawable getIcon(int type, long mailboxId) { + if (mailboxId == Mailbox.QUERY_ALL_INBOXES) { return mSummaryCombinedInboxDrawable; - } else if (mailboxKey == Mailbox.QUERY_ALL_FAVORITES) { + } else if (mailboxId == Mailbox.QUERY_ALL_FAVORITES) { return mSummaryStarredMailboxDrawable; - } else if (mailboxKey == Mailbox.QUERY_ALL_DRAFTS) { + } else if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) { return mSpecialMailboxDrawable.getDrawable(Mailbox.TYPE_DRAFTS); - } else if (mailboxKey == Mailbox.QUERY_ALL_OUTBOX) { + } else if (mailboxId == Mailbox.QUERY_ALL_OUTBOX) { return mSpecialMailboxDrawable.getDrawable(Mailbox.TYPE_OUTBOX); } + if (0 <= type && type < mSpecialMailboxDrawable.length()) { + return mSpecialMailboxDrawable.getDrawable(type); + } return mDefaultMailboxDrawable; } } @@ -773,4 +804,21 @@ public class Utility { return getFirstRowLong(context, uri, projection, selection, selectionArgs, sortOrder, column, null); } + + /** + * A class used to restore ListView state (e.g. scroll position) when changing adapter. + * + * TODO For some reason it doesn't always work. Investigate and fix it. + */ + public static class ListStateSaver { + private final Parcelable mState; + + public ListStateSaver(AbsListView lv) { + mState = lv.onSaveInstanceState(); + } + + public void restore(AbsListView lv) { + lv.onRestoreInstanceState(mState); + } + } } diff --git a/src/com/android/email/activity/MailboxListFragment.java b/src/com/android/email/activity/MailboxListFragment.java index d86ff2745..8c1a0ffec 100644 --- a/src/com/android/email/activity/MailboxListFragment.java +++ b/src/com/android/email/activity/MailboxListFragment.java @@ -17,6 +17,7 @@ package com.android.email.activity; import com.android.email.Email; +import com.android.email.Utility; import android.app.Activity; import android.app.ListFragment; @@ -216,12 +217,17 @@ public class MailboxListFragment extends ListFragment implements OnItemClickList Log.d(Email.LOG_TAG, "MailboxListFragment onLoadFinished"); } + // Save list view state (primarily scroll position) final ListView lv = getListView(); - final Parcelable listState = lv.onSaveInstanceState(); + final Utility.ListStateSaver lss = new Utility.ListStateSaver(lv); + + // Set the adapter. mListAdapter.changeCursor(cursor); setListAdapter(mListAdapter); setListShown(true); - lv.onRestoreInstanceState(listState); + + // Restore the state + lss.restore(lv); } } diff --git a/src/com/android/email/activity/MailboxesAdapter.java b/src/com/android/email/activity/MailboxesAdapter.java index c209a5f2e..b690571bd 100644 --- a/src/com/android/email/activity/MailboxesAdapter.java +++ b/src/com/android/email/activity/MailboxesAdapter.java @@ -21,11 +21,15 @@ import com.android.email.Utility; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.MailboxColumns; +import com.android.email.provider.EmailContent.Message; import android.content.Context; import android.content.CursorLoader; import android.content.Loader; import android.database.Cursor; +import android.database.MatrixCursor; +import android.database.MatrixCursor.RowBuilder; +import android.database.MergeCursor; import android.graphics.Typeface; import android.view.LayoutInflater; import android.view.View; @@ -39,16 +43,18 @@ import android.widget.TextView; * * TODO Add "combined inbox/star/etc.". * TODO Throttle auto-requery. + * TODO New UI will probably not distinguish unread counts from # of messages. + * i.e. we won't need two different viewes for them. * TODO Unit test, when UI is settled. */ /* package */ class MailboxesAdapter extends CursorAdapter { private static final String[] PROJECTION = new String[] { MailboxColumns.ID, - MailboxColumns.DISPLAY_NAME, MailboxColumns.UNREAD_COUNT, MailboxColumns.TYPE, + MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE, MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT}; private static final int COLUMN_ID = 0; private static final int COLUMN_DISPLAY_NAME = 1; - private static final int COLUMN_UNREAD_COUNT = 2; - private static final int COLUMN_TYPE = 3; + private static final int COLUMN_TYPE = 2; + private static final int COLUMN_UNREAD_COUNT = 3; private static final int COLUMN_MESSAGE_COUNT = 4; private static final String MAILBOX_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?" + @@ -65,11 +71,12 @@ import android.widget.TextView; @Override public void bindView(View view, Context context, Cursor cursor) { final int type = cursor.getInt(COLUMN_TYPE); + final long mailboxId = cursor.getLong(COLUMN_ID); // Set mailbox name final TextView nameView = (TextView) view.findViewById(R.id.mailbox_name); String mailboxName = Utility.FolderProperties.getInstance(context) - .getDisplayName(type); + .getDisplayName(type, mailboxId); if (mailboxName == null) { mailboxName = cursor.getString(COLUMN_DISPLAY_NAME); } @@ -110,7 +117,7 @@ import android.widget.TextView; // Set folder icon ((ImageView) view.findViewById(R.id.folder_icon)) .setImageDrawable(Utility.FolderProperties.getInstance(context) - .getIconIds(type)); + .getIcon(type, mailboxId)); } @Override @@ -122,11 +129,70 @@ import android.widget.TextView; * @return mailboxes Loader for an account. */ public static Loader createLoader(Context context, long accountId) { - return new CursorLoader(context, - EmailContent.Mailbox.CONTENT_URI, - MailboxesAdapter.PROJECTION, - MAILBOX_SELECTION, - new String[] { String.valueOf(accountId) }, - MailboxColumns.TYPE + "," + MailboxColumns.DISPLAY_NAME); + return new MailboxesLoader(context, accountId); + } + + /** + * Loader for mailboxes. If there's more than 1 account set up, the result will also include + * special mailboxes. (e.g. combined inbox, etc) + */ + private static class MailboxesLoader extends CursorLoader { + private final Context mContext; + + public MailboxesLoader(Context context, long accountId) { + super(context, EmailContent.Mailbox.CONTENT_URI, + MailboxesAdapter.PROJECTION, + MAILBOX_SELECTION, + new String[] { String.valueOf(accountId) }, + MailboxColumns.TYPE + "," + MailboxColumns.DISPLAY_NAME); + mContext = context; + } + + @Override + public Cursor loadInBackground() { + final Cursor mailboxes = super.loadInBackground(); + return new MergeCursor( + new Cursor[] {getSpecialMailboxesCursor(mContext), mailboxes}); + } + } + + /* package */ static Cursor getSpecialMailboxesCursor(Context context) { + MatrixCursor cursor = new MatrixCursor(PROJECTION); + + // TODO show combined boxes only if # accounts > 1 (wait for UI) but we always need starred. + + // Combined inbox -- show unread count + addSummaryMailboxRow(context, cursor, + Mailbox.QUERY_ALL_INBOXES, Mailbox.TYPE_INBOX, + Mailbox.getUnreadCountByMailboxType(context, Mailbox.TYPE_INBOX)); + + // Favorite -- show # of favorites + addSummaryMailboxRow(context, cursor, + Mailbox.QUERY_ALL_FAVORITES, Mailbox.TYPE_MAIL, + Message.getFavoriteMessageCount(context)); + + // Drafts -- show # of drafts + addSummaryMailboxRow(context, cursor, + Mailbox.QUERY_ALL_DRAFTS, Mailbox.TYPE_DRAFTS, + Mailbox.getMessageCountByMailboxType(context, Mailbox.TYPE_DRAFTS)); + + // Outbox -- # of sent messages + addSummaryMailboxRow(context, cursor, + Mailbox.QUERY_ALL_OUTBOX, Mailbox.TYPE_OUTBOX, + Mailbox.getMessageCountByMailboxType(context, Mailbox.TYPE_OUTBOX)); + + return cursor; + } + + private static void addSummaryMailboxRow(Context context, MatrixCursor cursor, + long id, int type, int count) { + if (count > 0) { + RowBuilder row = cursor.newRow(); + row.add(id); + row.add(""); // Display name. We get it from FolderProperties. + row.add(type); + row.add(count); + row.add(count); + } } } \ No newline at end of file diff --git a/src/com/android/email/activity/MessageListFragment.java b/src/com/android/email/activity/MessageListFragment.java index 0c6d39151..21d4cccd7 100644 --- a/src/com/android/email/activity/MessageListFragment.java +++ b/src/com/android/email/activity/MessageListFragment.java @@ -910,12 +910,12 @@ public class MessageListFragment extends ListFragment implements OnItemClickList } MessageListFragment.this.mAccountId = mAccountKey; - addFooterView(mMailboxKey, mAccountKey); - // TODO changeCursor(null)?? mListAdapter.changeCursor(cursor); setListAdapter(mListAdapter); + addFooterView(mMailboxKey, mAccountKey); + // changeCursor occurs the jumping of position in ListView, so it's need to restore // the position; restoreListPosition(); diff --git a/src/com/android/email/provider/EmailContent.java b/src/com/android/email/provider/EmailContent.java index 211450d36..91bc6feb4 100644 --- a/src/com/android/email/provider/EmailContent.java +++ b/src/com/android/email/provider/EmailContent.java @@ -504,6 +504,9 @@ public abstract class EmailContent { public static final String[] ID_COLUMN_PROJECTION = new String[] { RECORD_ID }; + private static final String FAVORITE_COUNT_SELECTION = + MessageColumns.FLAG_FAVORITE + "= 1"; + // _id field is in AbstractContent public String mDisplayName; public long mTimeStamp; @@ -765,6 +768,15 @@ public abstract class EmailContent { } } } + + /** + * @return number of favorite (starred) messages throughout all accounts. + * + * TODO Add trigger to keep track. (index isn't efficient in this case.) + */ + public static int getFavoriteMessageCount(Context context) { + return count(context, Message.CONTENT_URI, FAVORITE_COUNT_SELECTION, null); + } } public interface AccountColumns { @@ -1996,6 +2008,18 @@ public abstract class EmailContent { MailboxColumns.FLAG_VISIBLE, MailboxColumns.FLAGS, MailboxColumns.VISIBLE_LIMIT, MailboxColumns.SYNC_STATUS, MailboxColumns.MESSAGE_COUNT }; + + private static final String MAILBOX_TYPE_SELECTION = + MailboxColumns.TYPE + " =?"; + private static final String[] MAILBOX_SUM_OF_UNREAD_COUNT_PROJECTION = new String [] { + "sum(" + MailboxColumns.UNREAD_COUNT + ")" + }; + private static final int UNREAD_COUNT_COUNT_COLUMN = 0; + private static final String[] MAILBOX_SUM_OF_MESSAGE_COUNT_PROJECTION = new String [] { + "sum(" + MailboxColumns.MESSAGE_COUNT + ")" + }; + private static final int MESSAGE_COUNT_COUNT_COLUMN = 0; + public static final long NO_MAILBOX = -1; // Sentinel values for the mSyncInterval field of both Mailbox records @@ -2153,6 +2177,22 @@ public abstract class EmailContent { } return null; } + + public static int getUnreadCountByMailboxType(Context context, int type) { + return Utility.getFirstRowLong(context, Mailbox.CONTENT_URI, + MAILBOX_SUM_OF_UNREAD_COUNT_PROJECTION, + MAILBOX_TYPE_SELECTION, + new String[] { String.valueOf(type) }, null, UNREAD_COUNT_COUNT_COLUMN) + .intValue(); + } + + public static int getMessageCountByMailboxType(Context context, int type) { + return Utility.getFirstRowLong(context, Mailbox.CONTENT_URI, + MAILBOX_SUM_OF_MESSAGE_COUNT_PROJECTION, + MAILBOX_TYPE_SELECTION, + new String[] { String.valueOf(type) }, null, MESSAGE_COUNT_COUNT_COLUMN) + .intValue(); + } } public interface HostAuthColumns { diff --git a/tests/src/com/android/email/UtilityUnitTests.java b/tests/src/com/android/email/UtilityUnitTests.java index 7c0d1c5a0..3334a376d 100644 --- a/tests/src/com/android/email/UtilityUnitTests.java +++ b/tests/src/com/android/email/UtilityUnitTests.java @@ -23,12 +23,14 @@ import com.android.email.provider.EmailContent.Mailbox; import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Environment; +import android.os.Parcelable; import android.telephony.TelephonyManager; import android.test.AndroidTestCase; import android.test.MoreAsserts; import android.test.mock.MockCursor; import android.test.suitebuilder.annotation.SmallTest; import android.util.Log; +import android.widget.ListView; import android.widget.TextView; import java.io.File; @@ -401,4 +403,39 @@ public class UtilityUnitTests extends AndroidTestCase { assertEquals(Long.valueOf(-1), actual); assertTrue(cursor.mClosed); } + + public void testListStateSaver() { + MockListView lv = new MockListView(getContext()); + + Utility.ListStateSaver lss = new Utility.ListStateSaver(lv); + assertTrue(lv.mCalledOnSaveInstanceState); + assertFalse(lv.mCalledOnRestoreInstanceState); + + lv.mCalledOnSaveInstanceState = false; + + lss.restore(lv); + assertFalse(lv.mCalledOnSaveInstanceState); + assertTrue(lv.mCalledOnRestoreInstanceState); + } + + private static class MockListView extends ListView { + public boolean mCalledOnSaveInstanceState; + public boolean mCalledOnRestoreInstanceState; + + public MockListView(Context context) { + super(context); + } + + @Override + public Parcelable onSaveInstanceState() { + mCalledOnSaveInstanceState = true; + return super.onSaveInstanceState(); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + mCalledOnRestoreInstanceState = true; + super.onRestoreInstanceState(state); + } + } } diff --git a/tests/src/com/android/email/provider/ProviderTestUtils.java b/tests/src/com/android/email/provider/ProviderTestUtils.java index 020fff72e..df7f288a7 100644 --- a/tests/src/com/android/email/provider/ProviderTestUtils.java +++ b/tests/src/com/android/email/provider/ProviderTestUtils.java @@ -137,20 +137,26 @@ public class ProviderTestUtils extends Assert { /** * Create a message for test purposes - * - * TODO: body - * TODO: attachments */ public static Message setupMessage(String name, long accountId, long mailboxId, boolean addBody, boolean saveIt, Context context) { + // Default starred, read, (backword compatibility) + return setupMessage(name, accountId, mailboxId, addBody, saveIt, context, true, true); + } + + /** + * Create a message for test purposes + */ + public static Message setupMessage(String name, long accountId, long mailboxId, + boolean addBody, boolean saveIt, Context context, boolean starred, boolean read) { Message message = new Message(); message.mDisplayName = name; message.mTimeStamp = 100 + name.length(); message.mSubject = "subject " + name; - message.mFlagRead = true; + message.mFlagRead = read; message.mFlagLoaded = Message.FLAG_LOADED_UNLOADED; - message.mFlagFavorite = true; + message.mFlagFavorite = starred; message.mFlagAttachment = true; message.mFlags = 0; diff --git a/tests/src/com/android/email/provider/ProviderTests.java b/tests/src/com/android/email/provider/ProviderTests.java index 9218b62e4..35793d6cb 100644 --- a/tests/src/com/android/email/provider/ProviderTests.java +++ b/tests/src/com/android/email/provider/ProviderTests.java @@ -1824,8 +1824,13 @@ public class ProviderTests extends ProviderTestCase2 { } /** - * Test for the message count trrigers (insert/delete/move mailbox), and also + * Test for the message count triggers (insert/delete/move mailbox), and also * {@link EmailProvider#recalculateMessageCount}. + * + * It also covers: + * - {@link Mailbox#getMessageCountByMailboxType(Context, int)} + * - {@link Mailbox#getUnreadCountByMailboxType(Context, int)} + * - {@link Message#getFavoriteMessageCount(Context)} */ public void testMessageCount() { final Context c = mMockContext; @@ -1835,10 +1840,10 @@ public class ProviderTests extends ProviderTestCase2 { Account a2 = ProviderTestUtils.setupAccount("holdflag-2", true, c); // Create 2 mailboxes for each account - Mailbox b1 = ProviderTestUtils.setupMailbox("box1", a1.mId, true, c); - Mailbox b2 = ProviderTestUtils.setupMailbox("box2", a1.mId, true, c); - Mailbox b3 = ProviderTestUtils.setupMailbox("box3", a2.mId, true, c); - Mailbox b4 = ProviderTestUtils.setupMailbox("box4", a2.mId, true, c); + Mailbox b1 = ProviderTestUtils.setupMailbox("box1", a1.mId, true, c, Mailbox.TYPE_INBOX); + Mailbox b2 = ProviderTestUtils.setupMailbox("box2", a1.mId, true, c, Mailbox.TYPE_OUTBOX); + Mailbox b3 = ProviderTestUtils.setupMailbox("box3", a2.mId, true, c, Mailbox.TYPE_INBOX); + Mailbox b4 = ProviderTestUtils.setupMailbox("box4", a2.mId, true, c, Mailbox.TYPE_OUTBOX); // 0. Check the initial values, just in case. @@ -1847,20 +1852,26 @@ public class ProviderTests extends ProviderTestCase2 { assertEquals(0, getMessageCount(b3.mId)); assertEquals(0, getMessageCount(b4.mId)); + assertEquals(0, Message.getFavoriteMessageCount(c)); + assertEquals(0, Mailbox.getUnreadCountByMailboxType(c, Mailbox.TYPE_INBOX)); + assertEquals(0, Mailbox.getUnreadCountByMailboxType(c, Mailbox.TYPE_OUTBOX)); + assertEquals(0, Mailbox.getMessageCountByMailboxType(c, Mailbox.TYPE_INBOX)); + assertEquals(0, Mailbox.getMessageCountByMailboxType(c, Mailbox.TYPE_OUTBOX)); + // 1. Test for insert triggers. // Create some messages - Mailbox b = b1; // 1 message - Message m11 = ProviderTestUtils.setupMessage("1", b.mAccountKey, b.mId, true, true, c); + // b1: 1 message + Message m11 = createMessage(c, b1, true, false); - b = b2; // 2 messages - Message m21 = ProviderTestUtils.setupMessage("1", b.mAccountKey, b.mId, true, true, c); - Message m22 = ProviderTestUtils.setupMessage("1", b.mAccountKey, b.mId, true, true, c); + // b2: 2 message + Message m21 = createMessage(c, b2, false, false); + Message m22 = createMessage(c, b2, true, true); - b = b3; // 3 messages - Message m31 = ProviderTestUtils.setupMessage("1", b.mAccountKey, b.mId, true, true, c); - Message m32 = ProviderTestUtils.setupMessage("1", b.mAccountKey, b.mId, true, true, c); - Message m33 = ProviderTestUtils.setupMessage("1", b.mAccountKey, b.mId, true, true, c); + // b3: 3 message + Message m31 = createMessage(c, b3, false, false); + Message m32 = createMessage(c, b3, false, false); + Message m33 = createMessage(c, b3, true, true); // b4 has no messages. @@ -1870,6 +1881,13 @@ public class ProviderTests extends ProviderTestCase2 { assertEquals(3, getMessageCount(b3.mId)); assertEquals(0, getMessageCount(b4.mId)); + // Check the simple counting methods. + assertEquals(3, Message.getFavoriteMessageCount(c)); + assertEquals(3, Mailbox.getUnreadCountByMailboxType(c, Mailbox.TYPE_INBOX)); + assertEquals(1, Mailbox.getUnreadCountByMailboxType(c, Mailbox.TYPE_OUTBOX)); + assertEquals(4, Mailbox.getMessageCountByMailboxType(c, Mailbox.TYPE_INBOX)); + assertEquals(2, Mailbox.getMessageCountByMailboxType(c, Mailbox.TYPE_OUTBOX)); + // 2. test for recalculateMessageCount. // First, invalidate the message counts. @@ -1919,4 +1937,9 @@ public class ProviderTests extends ProviderTestCase2 { assertEquals(2, getMessageCount(b3.mId)); assertEquals(1, getMessageCount(b4.mId)); } + + private static Message createMessage(Context c, Mailbox b, boolean starred, boolean read) { + return ProviderTestUtils.setupMessage("1", b.mAccountKey, b.mId, true, true, c, starred, + read); + } }