diff --git a/src/com/android/email/widget/EmailWidget.java b/src/com/android/email/widget/EmailWidget.java index 6f4e50ce8..e60ef0d82 100644 --- a/src/com/android/email/widget/EmailWidget.java +++ b/src/com/android/email/widget/EmailWidget.java @@ -17,28 +17,23 @@ package com.android.email.widget; import com.android.email.Email; -import com.android.email.UiUtilities; import com.android.email.R; import com.android.email.ResourceHelper; +import com.android.email.UiUtilities; import com.android.email.activity.MessageCompose; import com.android.email.activity.Welcome; -import com.android.email.data.ThrottlingCursorLoader; import com.android.email.provider.WidgetProvider.WidgetService; -import com.android.emailcommon.provider.EmailContent; -import com.android.emailcommon.provider.EmailContent.Account; -import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.provider.EmailContent.Mailbox; import com.android.emailcommon.provider.EmailContent.Message; -import com.android.emailcommon.provider.EmailContent.MessageColumns; import com.android.emailcommon.utility.Utility; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; -import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.Loader; +import android.content.Loader.OnLoadCompleteListener; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Typeface; @@ -59,11 +54,17 @@ import android.widget.RemoteViews; import android.widget.RemoteViewsService; import java.util.List; -import java.util.concurrent.ExecutionException; -import junit.framework.Assert; - -public class EmailWidget implements RemoteViewsService.RemoteViewsFactory { +/** + * The email widget. + * + * Threading notes: + * - All methods must be called on the UI thread, except for {@link WidgetUpdater#doInBackground}. + * - {@link WidgetUpdater#doInBackground} must not read/write any members of {@link EmailWidget}. + * - (So no synchronizations are required in this class) + */ +public class EmailWidget implements RemoteViewsService.RemoteViewsFactory, + OnLoadCompleteListener { public static final String TAG = "EmailWidget"; /** @@ -96,16 +97,8 @@ public class EmailWidget implements RemoteViewsService.RemoteViewsFactory { private static final Uri COMMAND_URI_VIEW_MESSAGE = COMMAND_URI.buildUpon().appendPath(COMMAND_NAME_VIEW_MESSAGE).build(); - - private static final int TOTAL_COUNT_UNKNOWN = -1; private static final int MAX_MESSAGE_LIST_COUNT = 25; - private static final String SORT_TIMESTAMP_DESCENDING = MessageColumns.TIMESTAMP + " DESC"; - private static final String SORT_ID_ASCENDING = AccountColumns.ID + " ASC"; - private static final String[] ID_NAME_PROJECTION = {Account.RECORD_ID, Account.DISPLAY_NAME}; - private static final int ID_NAME_COLUMN_ID = 0; - private static final int ID_NAME_COLUMN_NAME = 1; - private static String sSubjectSnippetDivider; private static String sConfigureText; private static int sSenderFontSize; @@ -115,48 +108,25 @@ public class EmailWidget implements RemoteViewsService.RemoteViewsFactory { private static int sLightTextColor; private final Context mContext; - private final ContentResolver mResolver; private final AppWidgetManager mWidgetManager; // The widget identifier private final int mWidgetId; - // The cursor underlying the message list for this widget; this must only be modified while - // holding mCursorLock - private volatile Cursor mCursor; - // A lock on our cursor, which is used in the UI thread while inflating views, and by - // our Loader in the background - private final Object mCursorLock = new Object(); - // Number of records in the cursor - private int mCursorCount = TOTAL_COUNT_UNKNOWN; // The widget's loader (derived from ThrottlingCursorLoader) - private ViewCursorLoader mLoader; + private final EmailWidgetLoader mLoader; private final ResourceHelper mResourceHelper; - // Number of defined accounts - private int mAccountCount = TOTAL_COUNT_UNKNOWN; - // The current view type (all mail, unread, or starred for now) - /*package*/ ViewType mViewType = ViewType.STARRED; + /** + * The cursor for the messages, with some extra info such as the number of accounts. + * + * Note this cursor can be closed any time by the loader. Always use {@link #isCursorValid()} + * before touching its contents. + */ + private EmailWidgetLoader.CursorWithCounts mCursor; - // The projection to be used by the WidgetLoader - private static final String[] WIDGET_PROJECTION = new String[] { - EmailContent.RECORD_ID, MessageColumns.DISPLAY_NAME, MessageColumns.TIMESTAMP, - MessageColumns.SUBJECT, MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, - MessageColumns.FLAG_ATTACHMENT, MessageColumns.MAILBOX_KEY, MessageColumns.SNIPPET, - MessageColumns.ACCOUNT_KEY, MessageColumns.FLAGS - }; - private static final int WIDGET_COLUMN_ID = 0; - private static final int WIDGET_COLUMN_DISPLAY_NAME = 1; - private static final int WIDGET_COLUMN_TIMESTAMP = 2; - private static final int WIDGET_COLUMN_SUBJECT = 3; - private static final int WIDGET_COLUMN_FLAG_READ = 4; - @SuppressWarnings("unused") - private static final int WIDGET_COLUMN_FLAG_FAVORITE = 5; - private static final int WIDGET_COLUMN_FLAG_ATTACHMENT = 6; - private static final int WIDGET_COLUMN_MAILBOX_KEY = 7; - private static final int WIDGET_COLUMN_SNIPPET = 8; - private static final int WIDGET_COLUMN_ACCOUNT_KEY = 9; - private static final int WIDGET_COLUMN_FLAGS = 10; + /** The current view type */ + /* package */ WidgetView mWidgetView = WidgetView.UNINITIALIZED_VIEW; public EmailWidget(Context context, int _widgetId) { super(); @@ -164,11 +134,11 @@ public class EmailWidget implements RemoteViewsService.RemoteViewsFactory { Log.d(TAG, "Creating EmailWidget with id = " + _widgetId); } mContext = context.getApplicationContext(); - mResolver = mContext.getContentResolver(); mWidgetManager = AppWidgetManager.getInstance(mContext); mWidgetId = _widgetId; - mLoader = new ViewCursorLoader(); + mLoader = new EmailWidgetLoader(mContext); + mLoader.registerListener(0, this); if (sSubjectSnippetDivider == null) { // Initialize string, color, dimension resources Resources res = mContext.getResources(); @@ -185,169 +155,38 @@ public class EmailWidget implements RemoteViewsService.RemoteViewsFactory { mResourceHelper = ResourceHelper.getInstance(mContext); } - public void updateWidget(boolean validateView) { - new WidgetUpdateTask().execute(validateView); + public void start() { + // The default view is UNINITIALIZED_VIEW, and we switch to the next one, which should + // be the initial view. (the first view shown to the user.) + switchView(); + } + + private boolean isCursorValid() { + return mCursor != null && !mCursor.isClosed(); } /** - * Task for updating widget data (eg: the header, view list items, etc...) - * If parameter to {@link #execute(Boolean...)} is true, the current - * view is validated against the current set of accounts. And if the current view - * is determined to be invalid, the view will automatically progress to the next - * valid view. + * Called when the loader finished loading data. Update the widget. */ - private final class WidgetUpdateTask extends AsyncTask { - @Override - protected Boolean doInBackground(Boolean... validateView) { - mAccountCount = EmailContent.count(mContext, EmailContent.Account.CONTENT_URI); - // If displaying invalid view, switch to the next view - return !validateView[0] || isViewValid(); - } + @Override + public void onLoadComplete(Loader loader, Cursor cursor) { + // Save away the cursor + mCursor = (EmailWidgetLoader.CursorWithCounts) cursor; + mWidgetView = mLoader.getLoadingWidgetView(); - @Override - protected void onPostExecute(Boolean isValidView) { - updateHeader(); - if (!isValidView) { - switchView(); - } - } + RemoteViews views = new RemoteViews(mContext.getPackageName(), R.layout.widget); + updateHeader(); + setupTitleAndCount(views); + mWidgetManager.partiallyUpdateAppWidget(mWidgetId, views); + mWidgetManager.notifyAppWidgetViewDataChanged(mWidgetId, R.id.message_list); } /** - * The ThrottlingCursorLoader does all of the heavy lifting in managing the data loading - * task; all we need is to register a listener so that we're notified when the load is - * complete. + * Start loading the data. At this point nothing on the widget changes -- the current view + * will remain valid until the loader loads the latest data. */ - private final class ViewCursorLoader extends ThrottlingCursorLoader { - protected ViewCursorLoader() { - super(mContext, Message.CONTENT_URI, WIDGET_PROJECTION, mViewType.selection, - mViewType.selectionArgs, SORT_TIMESTAMP_DESCENDING); - registerListener(0, new OnLoadCompleteListener() { - @Override - public void onLoadComplete(Loader loader, Cursor cursor) { - synchronized (mCursorLock) { - // Save away the cursor - mCursor = cursor; - // Reset the notification Uri to our Message table notifier URI - mCursor.setNotificationUri(mResolver, Message.NOTIFIER_URI); - // Save away the count (for display) - mCursorCount = mCursor.getCount(); - if (Email.DEBUG) { - Log.d(TAG, "onLoadComplete, count = " + cursor.getCount()); - } - } - RemoteViews views = - new RemoteViews(mContext.getPackageName(), R.layout.widget); - setupTitleAndCount(views); - mWidgetManager.partiallyUpdateAppWidget(mWidgetId, views); - mWidgetManager.notifyAppWidgetViewDataChanged(mWidgetId, R.id.message_list); - } - }); - } - - /** - * Stop any pending load, reset selection parameters, and start loading - * Must be called from the UI thread - * @param viewType the current ViewType - */ - private void load(ViewType viewType) { - reset(); - setSelection(viewType.selection); - setSelectionArgs(viewType.selectionArgs); - startLoading(); - } - } - - /** - * Initialize to first appropriate view (depending on the number of accounts) - */ - public void init() { - // Just update the account count & header; no need to validate the view - updateWidget(false); - switchView(); // TODO Do we really need this?? - } - - /** - * Reset cursor and cursor count, notify widget that list data is invalid, and start loading - * with our current ViewType - */ - private void loadView() { - synchronized(mCursorLock) { - mCursorCount = TOTAL_COUNT_UNKNOWN; - mCursor = null; - mWidgetManager.notifyAppWidgetViewDataChanged(mWidgetId, R.id.message_list); - mLoader.load(mViewType); - } - } - - /** - * Switch to the next widget view (all -> account1 -> ... -> account n -> unread -> starred) - * - * This must be called on a background thread. Use {@link #switchView} on the UI thread. - */ - private synchronized void switchToNextView() { - switch(mViewType) { - // If we're in starred and there is more than one account, go to "all mail" - // Otherwise, fall through to the accounts themselves - case STARRED: - if (EmailContent.count(mContext, Account.CONTENT_URI) > 1) { - mViewType = ViewType.ALL_INBOX; - break; - } - //$FALL-THROUGH$ - case ALL_INBOX: - ViewType.ACCOUNT.selectionArgs[0] = "0"; - //$FALL-THROUGH$ - case ACCOUNT: - // Find the next account (or, if none, default to UNREAD) - String idString = ViewType.ACCOUNT.selectionArgs[0]; - Cursor c = mResolver.query(Account.CONTENT_URI, ID_NAME_PROJECTION, "_id>?", - new String[] {idString}, SORT_ID_ASCENDING); - try { - if (c.moveToFirst()) { - mViewType = ViewType.ACCOUNT; - mViewType.selectionArgs[0] = c.getString(ID_NAME_COLUMN_ID); - mViewType.setTitle(c.getString(ID_NAME_COLUMN_NAME)); - } else { - mViewType = ViewType.UNREAD; - } - } finally { - c.close(); - } - break; - case UNREAD: - mViewType = ViewType.STARRED; - break; - } - } - - /** - * Returns whether the current view is valid. The following rules determine if a view is - * considered valid: - * 1. If the view is either {@link ViewType#STARRED} or {@link ViewType#UNREAD}, always - * returns true. - * 2. If the view is {@link ViewType#ALL_INBOX}, returns true if more than - * one account is defined. Otherwise, returns false. - * 3. If the view is {@link ViewType#ACCOUNT}, returns true if the account - * is defined. Otherwise, returns false. - */ - private boolean isViewValid() { - switch(mViewType) { - case ALL_INBOX: - // "all inbox" is valid only if there is more than one account - return (EmailContent.count(mContext, Account.CONTENT_URI) > 1); - case ACCOUNT: - // Ensure current account still exists - String idString = ViewType.ACCOUNT.selectionArgs[0]; - Cursor c = mResolver.query(Account.CONTENT_URI, ID_NAME_PROJECTION, "_id=?", - new String[] {idString}, SORT_ID_ASCENDING); - try { - return c.moveToFirst(); - } finally { - c.close(); - } - } - return true; + private void loadView(WidgetView view) { + mLoader.load(view); } /** @@ -451,11 +290,11 @@ public class EmailWidget implements RemoteViewsService.RemoteViewsFactory { private void setupTitleAndCount(RemoteViews views) { // Set up the title (view type + count of messages) - views.setTextViewText(R.id.widget_title, mViewType.getTitle(mContext)); + views.setTextViewText(R.id.widget_title, mWidgetView.getTitle(mContext)); views.setTextViewText(R.id.widget_tap, sConfigureText); String count = ""; - if (mCursorCount != TOTAL_COUNT_UNKNOWN) { - count = UiUtilities.getMessageCountForUi(mContext, mCursor.getCount(), false); + if (isCursorValid()) { + count = UiUtilities.getMessageCountForUi(mContext, mCursor.getMessageCount(), false); } views.setTextViewText(R.id.widget_count, count); } @@ -463,7 +302,7 @@ public class EmailWidget implements RemoteViewsService.RemoteViewsFactory { * Update the "header" of the widget (i.e. everything that doesn't include the scrolling * message list) */ - public void updateHeader() { + private void updateHeader() { if (Email.DEBUG) { Log.d(TAG, "updateWidget " + mWidgetId); } @@ -480,7 +319,7 @@ public class EmailWidget implements RemoteViewsService.RemoteViewsFactory { setupTitleAndCount(views); - if (mAccountCount == 0) { + if (!isCursorValid() || mCursor.getAccountCount() == 0) { // Hide compose icon & show "touch to configure" text views.setViewVisibility(R.id.widget_compose, View.INVISIBLE); views.setViewVisibility(R.id.message_list, View.GONE); @@ -566,79 +405,78 @@ public class EmailWidget implements RemoteViewsService.RemoteViewsFactory { @Override public RemoteViews getViewAt(int position) { // Use the cursor to set up the widget - synchronized (mCursorLock) { - if (mCursor == null || mCursor.isClosed() || !mCursor.moveToPosition(position)) { - return getLoadingView(); - } - RemoteViews views = - new RemoteViews(mContext.getPackageName(), R.layout.widget_list_item); - boolean isUnread = mCursor.getInt(WIDGET_COLUMN_FLAG_READ) != 1; - int drawableId = R.drawable.widget_read_conversation_selector; - if (isUnread) { - drawableId = R.drawable.widget_unread_conversation_selector; - } - views.setInt(R.id.widget_message, "setBackgroundResource", drawableId); - - // Add style to sender - SpannableStringBuilder from = - new SpannableStringBuilder(mCursor.getString(WIDGET_COLUMN_DISPLAY_NAME)); - from.setSpan( - isUnread ? new StyleSpan(Typeface.BOLD) : new StyleSpan(Typeface.NORMAL), 0, - from.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - CharSequence styledFrom = addStyle(from, sSenderFontSize, sDefaultTextColor); - views.setTextViewText(R.id.widget_from, styledFrom); - - long timestamp = mCursor.getLong(WIDGET_COLUMN_TIMESTAMP); - // Get a nicely formatted date string (relative to today) - String date = DateUtils.getRelativeTimeSpanString(mContext, timestamp).toString(); - // Add style to date - CharSequence styledDate = addStyle(date, sDateFontSize, sDefaultTextColor); - views.setTextViewText(R.id.widget_date, styledDate); - - // Add style to subject/snippet - String subject = mCursor.getString(WIDGET_COLUMN_SUBJECT); - String snippet = mCursor.getString(WIDGET_COLUMN_SNIPPET); - CharSequence subjectAndSnippet = - getStyledSubjectSnippet(subject, snippet, !isUnread); - views.setTextViewText(R.id.widget_subject, subjectAndSnippet); - - int messageFlags = mCursor.getInt(WIDGET_COLUMN_FLAGS); - boolean hasInvite = (messageFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0; - views.setViewVisibility(R.id.widget_invite, hasInvite ? View.VISIBLE : View.GONE); - - boolean hasAttachment = mCursor.getInt(WIDGET_COLUMN_FLAG_ATTACHMENT) != 0; - views.setViewVisibility(R.id.widget_attachment, - hasAttachment ? View.VISIBLE : View.GONE); - - if (mViewType == ViewType.ACCOUNT) { - views.setViewVisibility(R.id.color_chip, View.INVISIBLE); - } else { - long accountId = mCursor.getLong(WIDGET_COLUMN_ACCOUNT_KEY); - int colorId = mResourceHelper.getAccountColorId(accountId); - // Don't show the chip if we have 1 or fewer accounts - if (mAccountCount > 1 && colorId != ResourceHelper.UNDEFINED_RESOURCE_ID) { - // Color defined by resource ID, so, use it - views.setViewVisibility(R.id.color_chip, View.VISIBLE); - views.setImageViewResource(R.id.color_chip, colorId); - } else { - // Color not defined by resource ID, nothing we can do, so, hide the chip - views.setViewVisibility(R.id.color_chip, View.INVISIBLE); - } - } - - // Set button intents for view, reply, and delete - String messageId = mCursor.getString(WIDGET_COLUMN_ID); - String mailboxId = mCursor.getString(WIDGET_COLUMN_MAILBOX_KEY); - setFillInIntent(views, R.id.widget_message, COMMAND_URI_VIEW_MESSAGE, - messageId, mailboxId); - - return views; + if (!isCursorValid() || !mCursor.moveToPosition(position)) { + return getLoadingView(); } + RemoteViews views = + new RemoteViews(mContext.getPackageName(), R.layout.widget_list_item); + boolean isUnread = mCursor.getInt(EmailWidgetLoader.WIDGET_COLUMN_FLAG_READ) != 1; + int drawableId = R.drawable.widget_read_conversation_selector; + if (isUnread) { + drawableId = R.drawable.widget_unread_conversation_selector; + } + views.setInt(R.id.widget_message, "setBackgroundResource", drawableId); + + // Add style to sender + SpannableStringBuilder from = + new SpannableStringBuilder(mCursor.getString( + EmailWidgetLoader.WIDGET_COLUMN_DISPLAY_NAME)); + from.setSpan( + isUnread ? new StyleSpan(Typeface.BOLD) : new StyleSpan(Typeface.NORMAL), 0, + from.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + CharSequence styledFrom = addStyle(from, sSenderFontSize, sDefaultTextColor); + views.setTextViewText(R.id.widget_from, styledFrom); + + long timestamp = mCursor.getLong(EmailWidgetLoader.WIDGET_COLUMN_TIMESTAMP); + // Get a nicely formatted date string (relative to today) + String date = DateUtils.getRelativeTimeSpanString(mContext, timestamp).toString(); + // Add style to date + CharSequence styledDate = addStyle(date, sDateFontSize, sDefaultTextColor); + views.setTextViewText(R.id.widget_date, styledDate); + + // Add style to subject/snippet + String subject = mCursor.getString(EmailWidgetLoader.WIDGET_COLUMN_SUBJECT); + String snippet = mCursor.getString(EmailWidgetLoader.WIDGET_COLUMN_SNIPPET); + CharSequence subjectAndSnippet = + getStyledSubjectSnippet(subject, snippet, !isUnread); + views.setTextViewText(R.id.widget_subject, subjectAndSnippet); + + int messageFlags = mCursor.getInt(EmailWidgetLoader.WIDGET_COLUMN_FLAGS); + boolean hasInvite = (messageFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0; + views.setViewVisibility(R.id.widget_invite, hasInvite ? View.VISIBLE : View.GONE); + + boolean hasAttachment = + mCursor.getInt(EmailWidgetLoader.WIDGET_COLUMN_FLAG_ATTACHMENT) != 0; + views.setViewVisibility(R.id.widget_attachment, + hasAttachment ? View.VISIBLE : View.GONE); + + if (mCursor.getAccountCount() <= 1 || mWidgetView.isPerAccount()) { + views.setViewVisibility(R.id.color_chip, View.INVISIBLE); + } else { + long accountId = mCursor.getLong(EmailWidgetLoader.WIDGET_COLUMN_ACCOUNT_KEY); + int colorId = mResourceHelper.getAccountColorId(accountId); + if (colorId != ResourceHelper.UNDEFINED_RESOURCE_ID) { + // Color defined by resource ID, so, use it + views.setViewVisibility(R.id.color_chip, View.VISIBLE); + views.setImageViewResource(R.id.color_chip, colorId); + } else { + // Color not defined by resource ID, nothing we can do, so, hide the chip + views.setViewVisibility(R.id.color_chip, View.INVISIBLE); + } + } + + // Set button intents for view, reply, and delete + String messageId = mCursor.getString(EmailWidgetLoader.WIDGET_COLUMN_ID); + String mailboxId = mCursor.getString(EmailWidgetLoader.WIDGET_COLUMN_MAILBOX_KEY); + setFillInIntent(views, R.id.widget_message, COMMAND_URI_VIEW_MESSAGE, + messageId, mailboxId); + + return views; } @Override public int getCount() { - if (mCursor == null) return 0; + if (!isCursorValid()) return 0; return Math.min(mCursor.getCount(), MAX_MESSAGE_LIST_COUNT); } @@ -671,7 +509,7 @@ public class EmailWidget implements RemoteViewsService.RemoteViewsFactory { public void onDeleted() { if (mLoader != null) { - mLoader.stopLoading(); + mLoader.reset(); } WidgetManager.getInstance().remove(mWidgetId); } @@ -679,7 +517,7 @@ public class EmailWidget implements RemoteViewsService.RemoteViewsFactory { @Override public void onDestroy() { if (mLoader != null) { - mLoader.stopLoading(); + mLoader.reset(); } WidgetManager.getInstance().remove(mWidgetId); } @@ -688,60 +526,46 @@ public class EmailWidget implements RemoteViewsService.RemoteViewsFactory { public void onCreate() { } + /** + * Update the widget. If the current view is invalid, switch to the next view, then update. + */ + /* package */ void validateAndUpdate() { + new WidgetUpdater(false).execute(); + } + /** * Switch to the next view. */ /* package */ void switchView() { - switchView(false); - } - - private WidgetViewSwitcher switchView(boolean disableLoadAfterSwitchForTest) { - WidgetViewSwitcher switcher = new WidgetViewSwitcher(this, disableLoadAfterSwitchForTest); - switcher.execute(); - return switcher; + new WidgetUpdater(true).execute(); } /** - * Switch views synchronously without loading + * Update the widget. If {@code switchToNextView} is set true, or the current view is invalid, + * switch to the next view. */ - /* package */ void switchViewSyncForTest() { - WidgetViewSwitcher switcher = switchView(true); - try { - switcher.get(); - } catch (InterruptedException e) { - Assert.fail(); - } catch (ExecutionException e) { - Assert.fail(); - } - } + private class WidgetUpdater extends AsyncTask { + private final WidgetView mCurrentView; + private final boolean mSwitchToNextView; - /** - * Utility class to handle switching widget views; in the background, we access the database - * to determine account status, etc. In the foreground, we start up the Loader with new - * parameters - */ - private static class WidgetViewSwitcher extends AsyncTask { - private final EmailWidget mWidget; - private final boolean mDisableLoadAfterSwitchForTest; - - public WidgetViewSwitcher(EmailWidget widget, boolean disableLoadAfterSwitchForTest) { - mWidget = widget; - mDisableLoadAfterSwitchForTest = disableLoadAfterSwitchForTest; + public WidgetUpdater(boolean switchToNextView) { + mCurrentView = mWidgetView; + mSwitchToNextView = switchToNextView; } @Override - protected Void doInBackground(Void... params) { - mWidget.switchToNextView(); - return null; - } - - @Override - protected void onPostExecute(Void param) { - if (isCancelled()) { - return; + protected WidgetView doInBackground(Void... params) { + if (mSwitchToNextView || !mCurrentView.isValid(mContext)) { + return mCurrentView.getNext(mContext); + } else { + return mCurrentView; // Reload the same view. } - if (!mDisableLoadAfterSwitchForTest) { - mWidget.loadView(); + } + + @Override + protected void onPostExecute(WidgetView nextView) { + if (nextView != null) { + loadView(nextView); } } } diff --git a/src/com/android/email/widget/EmailWidgetLoader.java b/src/com/android/email/widget/EmailWidgetLoader.java new file mode 100644 index 000000000..96cb39e0f --- /dev/null +++ b/src/com/android/email/widget/EmailWidgetLoader.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2011 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.widget; + +import com.android.email.data.ThrottlingCursorLoader; +import com.android.emailcommon.provider.EmailContent; +import com.android.emailcommon.provider.EmailContent.Account; +import com.android.emailcommon.provider.EmailContent.Message; +import com.android.emailcommon.provider.EmailContent.MessageColumns; + +import android.content.Context; +import android.database.Cursor; +import android.database.CursorWrapper; + +/** + * Loader for {@link EmailWidget}. + * + * This loader not only loads the messages, but also: + * - The number of accounts. + * - The message count shown in the widget header. + * It's currently just the same as the message count, but this will be updated to the unread + * counts for inboxes. + */ +/* package */ class EmailWidgetLoader extends ThrottlingCursorLoader { + private static final String SORT_TIMESTAMP_DESCENDING = MessageColumns.TIMESTAMP + " DESC"; + + // The projection to be used by the WidgetLoader + private static final String[] WIDGET_PROJECTION = new String[] { + EmailContent.RECORD_ID, MessageColumns.DISPLAY_NAME, MessageColumns.TIMESTAMP, + MessageColumns.SUBJECT, MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, + MessageColumns.FLAG_ATTACHMENT, MessageColumns.MAILBOX_KEY, MessageColumns.SNIPPET, + MessageColumns.ACCOUNT_KEY, MessageColumns.FLAGS + }; + public static final int WIDGET_COLUMN_ID = 0; + public static final int WIDGET_COLUMN_DISPLAY_NAME = 1; + public static final int WIDGET_COLUMN_TIMESTAMP = 2; + public static final int WIDGET_COLUMN_SUBJECT = 3; + public static final int WIDGET_COLUMN_FLAG_READ = 4; + public static final int WIDGET_COLUMN_FLAG_FAVORITE = 5; + public static final int WIDGET_COLUMN_FLAG_ATTACHMENT = 6; + public static final int WIDGET_COLUMN_MAILBOX_KEY = 7; + public static final int WIDGET_COLUMN_SNIPPET = 8; + public static final int WIDGET_COLUMN_ACCOUNT_KEY = 9; + public static final int WIDGET_COLUMN_FLAGS = 10; + + /** + * The actual data returned by this loader. + */ + public static class CursorWithCounts extends CursorWrapper { + private final int mAccountCount; + private final int mMessageCount; + + public CursorWithCounts(Cursor cursor, int accountCount, int messageCount) { + super(cursor); + mAccountCount = accountCount; + mMessageCount = messageCount; + } + + public int getAccountCount() { + return mAccountCount; + } + + /** + * @return The count that should be shown on the widget header. + * Note depending on the view, it may be the unread count, which is different from + * the record count (i.e. {@link #getCount()}}. + */ + public int getMessageCount() { + return mMessageCount; + } + } + + private final Context mContext; + + private WidgetView mLoadingWidgetView; + + public EmailWidgetLoader(Context context) { + super(context, Message.CONTENT_URI, WIDGET_PROJECTION, null, + null, SORT_TIMESTAMP_DESCENDING); + mContext = context; + } + + @Override + public Cursor loadInBackground() { + final Cursor messagesCursor = super.loadInBackground(); + + // Reset the notification Uri to our Message table notifier URI + messagesCursor.setNotificationUri(mContext.getContentResolver(), Message.NOTIFIER_URI); + + final int accountCount = EmailContent.count(mContext, Account.CONTENT_URI); + + // TODO Use correct count -- e.g. unread count for inboxes, not total count. + final int messageCount = messagesCursor.getCount(); + + return new CursorWithCounts(messagesCursor, accountCount, messageCount); + } + + /** + * Stop any pending load, reset selection parameters, and start loading. + * + * Must be called from the UI thread + * + * @param view the current ViewType + */ + public void load(WidgetView view) { + reset(); + mLoadingWidgetView = view; + setSelection(view.getSelection(mContext)); + setSelectionArgs(view.getSelectionArgs()); + startLoading(); + } + + /** + * @return the {@link WidgetView} that is (being) loaded. + * + * Must be called from the UI thread + */ + public WidgetView getLoadingWidgetView() { + return mLoadingWidgetView; + } +} diff --git a/src/com/android/email/widget/WidgetManager.java b/src/com/android/email/widget/WidgetManager.java index 631171acd..7133e0509 100644 --- a/src/com/android/email/widget/WidgetManager.java +++ b/src/com/android/email/widget/WidgetManager.java @@ -49,13 +49,13 @@ public class WidgetManager { public synchronized void updateAllWidgets() { for (EmailWidget widget: mWidgets.values()) { // Anything could have changed; update widget & validate the current view - widget.updateWidget(true); + widget.validateAndUpdate(); } } public synchronized void getOrCreateWidgets(Context context, int[] widgetIds) { for (int widgetId : widgetIds) { - getOrCreateWidget(context, widgetId).updateHeader(); + getOrCreateWidget(context, widgetId).validateAndUpdate(); } } @@ -66,8 +66,8 @@ public class WidgetManager { Log.d(EmailWidget.TAG, "Creating EmailWidget for id #" + widgetId); } widget = new EmailWidget(context, widgetId); - widget.init(); WidgetManager.getInstance().put(widgetId, widget); + widget.start(); } return widget; } @@ -88,7 +88,7 @@ public class WidgetManager { int n = 0; for (EmailWidget widget : mWidgets.values()) { writer.println("Widget #" + (++n)); - writer.println(" ViewType=" + widget.mViewType); + writer.println(" View=" + widget.mWidgetView); } } } diff --git a/src/com/android/email/widget/WidgetView.java b/src/com/android/email/widget/WidgetView.java new file mode 100644 index 000000000..e50f8974e --- /dev/null +++ b/src/com/android/email/widget/WidgetView.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2011 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.widget; + +import com.android.email.R; +import com.android.emailcommon.provider.EmailContent; +import com.android.emailcommon.provider.EmailContent.Account; +import com.android.emailcommon.provider.EmailContent.AccountColumns; +import com.android.emailcommon.provider.EmailContent.Message; +import com.android.emailcommon.utility.Utility; + +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +/** + * Represents the "view" of the widget. + * + * It's a {@link ViewType} + mutable fields. (e.g. account id/name) + */ +/* package */ class WidgetView { + private static final String SORT_ID_ASCENDING = AccountColumns.ID + " ASC"; + private static final String[] ID_NAME_PROJECTION = {Account.RECORD_ID, Account.DISPLAY_NAME}; + private static final int ID_NAME_COLUMN_ID = 0; + private static final int ID_NAME_COLUMN_NAME = 1; + + private static enum ViewType { + TYPE_ALL_UNREAD(false, Message.UNREAD_SELECTION, R.string.widget_unread), + TYPE_ALL_STARRED(false, Message.ALL_FAVORITE_SELECTION, R.string.widget_starred), + TYPE_ALL_INBOX(false, Message.INBOX_SELECTION, R.string.widget_all_mail), + TYPE_ACCOUNT_INBOX(true, Message.PER_ACCOUNT_INBOX_SELECTION, 0) { + @Override public String getTitle(Context context, String accountName) { + return accountName; + } + + @Override public String[] getSelectionArgs(long accountId) { + return new String[]{Long.toString(accountId)}; + } + }; + + private final boolean mIsPerAccount; + private final String mSelection; + private final int mTitleResource; + + ViewType(boolean isPerAccount, String selection, int titleResource) { + mIsPerAccount = isPerAccount; + mSelection = selection; + mTitleResource = titleResource; + } + + public String getTitle(Context context, String accountName) { + return context.getString(mTitleResource); + } + + public String getSelection() { + return mSelection; + } + + public String[] getSelectionArgs(long accountId) { + return null; + } + } + + /* package */ static final WidgetView ALL_UNREAD = new WidgetView(ViewType.TYPE_ALL_UNREAD); + /* package */ static final WidgetView ALL_STARRED = new WidgetView(ViewType.TYPE_ALL_STARRED); + /* package */ static final WidgetView ALL_INBOX = new WidgetView(ViewType.TYPE_ALL_INBOX); + + /** + * The initial view will be the *next* of ALL_STARRED -- see {@link #getNext}. + */ + public static final WidgetView UNINITIALIZED_VIEW = ALL_STARRED; + + private final ViewType mViewType; + /** Account ID -- set only when isPerAccount */ + private final long mAccountId; + /** Account name -- set only when isPerAccount */ + private final String mAccountName; + + private WidgetView(ViewType viewType) { + this(viewType, 0, null); + } + + private WidgetView(ViewType viewType, long accountId, String accountName) { + mViewType = viewType; + mAccountId = accountId; + mAccountName = accountName; + } + + public boolean isPerAccount() { + return mViewType.mIsPerAccount; + } + + public String getTitle(Context context) { + return mViewType.getTitle(context, mAccountName); + } + + public String getSelection(Context context) { + return mViewType.getSelection(); + } + + public String[] getSelectionArgs() { + return mViewType.getSelectionArgs(mAccountId); + } + + /** + * Switch to the "next" view. + * + * Views rotate in this order: + * - {@link #ALL_STARRED} + * - {@link #ALL_INBOX} -- this will be skipped if # of accounts <= 1 + * - Inbox for account 1 + * - Inbox for account 2 + * - : + * - {@link #ALL_UNREAD} + * - Go back to {@link #ALL_STARRED}. + * + * Note the initial view is always the next of {@link #ALL_STARRED}. + */ + public WidgetView getNext(Context context) { + if (mViewType == ViewType.TYPE_ALL_UNREAD) { + return ALL_STARRED; + } + if (mViewType == ViewType.TYPE_ALL_STARRED) { + // If we're in starred and there is more than one account, go to "all mail" + // Otherwise, fall through to the accounts themselves + if (EmailContent.count(context, Account.CONTENT_URI) > 1) { + return ALL_INBOX; + } + } + final long nextAccountIdStart; + if (mViewType == ViewType.TYPE_ALL_INBOX) { + nextAccountIdStart = -1; + } else { // TYPE_ACCOUNT_INBOX + nextAccountIdStart = mAccountId + 1; + } + Cursor c = context.getContentResolver().query(Account.CONTENT_URI, ID_NAME_PROJECTION, + "_id>=?", new String[] {Long.toString(nextAccountIdStart)}, SORT_ID_ASCENDING); + + final long nextAccountId; + final String nextAccountName; + try { + if (c.moveToFirst()) { + return new WidgetView(ViewType.TYPE_ACCOUNT_INBOX, c.getLong(ID_NAME_COLUMN_ID), + c.getString(ID_NAME_COLUMN_NAME)); + } else { + return ALL_UNREAD; + } + } finally { + c.close(); + } + } + + /** + * Returns whether the current view is valid. The following rules determine if a view is + * considered valid: + * 1. {@link ViewType#TYPE_ALL_STARRED} and {@link ViewType#TYPE_ALL_UNREAD} are always + * valid. + * 2. If the view is {@link ViewType#TYPE_ALL_INBOX}, returns true if more than + * one account is defined. Otherwise, returns false. + * 3. If the view is {@link ViewType#TYPE_ACCOUNT_INBOX}, returns true if the + * account is defined. Otherwise, returns false. + */ + public boolean isValid(Context context) { + switch(mViewType) { + case TYPE_ALL_INBOX: + // "all inbox" is valid only if there is more than one account + return (EmailContent.count(context, Account.CONTENT_URI) > 1); + case TYPE_ACCOUNT_INBOX: + // Ensure current account still exists + Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, mAccountId); + return Utility.getFirstRowLong(context, uri, + EmailContent.ID_PROJECTION, null, null, null, + EmailContent.ID_PROJECTION_COLUMN, null) != null; + } + return true; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("WidgetView:type="); + sb.append(mViewType); + sb.append(" account="); + sb.append(mAccountId); + + return sb.toString(); + } +} diff --git a/tests/src/com/android/email/provider/ProviderTestUtils.java b/tests/src/com/android/email/provider/ProviderTestUtils.java index fb17b2c17..8676db947 100644 --- a/tests/src/com/android/email/provider/ProviderTestUtils.java +++ b/tests/src/com/android/email/provider/ProviderTestUtils.java @@ -26,6 +26,7 @@ import com.android.emailcommon.provider.EmailContent.Mailbox; import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.utility.Utility; +import android.content.ContentUris; import android.content.Context; import android.net.Uri; import android.test.MoreAsserts; @@ -72,6 +73,14 @@ public class ProviderTestUtils extends Assert { return account; } + /** + * Lightweight way of deleting an account for testing. + */ + public static void deleteAccount(Context context, long accountId) { + context.getContentResolver().delete(ContentUris.withAppendedId( + EmailContent.Account.CONTENT_URI, accountId), null, null); + } + /** * Create a hostauth record for test purposes */ diff --git a/tests/src/com/android/email/widget/EmailWidgetTests.java b/tests/src/com/android/email/widget/EmailWidgetTests.java deleted file mode 100644 index 8c489f504..000000000 --- a/tests/src/com/android/email/widget/EmailWidgetTests.java +++ /dev/null @@ -1,141 +0,0 @@ -/* Copyright (C) 2011 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.widget; - -import com.android.email.provider.EmailProvider; -import com.android.email.provider.ProviderTestUtils; -import com.android.emailcommon.provider.EmailContent; -import com.android.emailcommon.provider.EmailContent.Account; -import com.android.emailcommon.provider.EmailContent.Mailbox; -import com.android.emailcommon.provider.EmailContent.Message; - -import android.content.Context; -import android.test.ProviderTestCase2; - -/** - * Tests of EmailWidget - * - * You can run this entire test case with: - * runtest -c com.android.email.widget.EmailWidget email - */ -public class EmailWidgetTests extends ProviderTestCase2 { - private Context mMockContext; - - public EmailWidgetTests() { - super(EmailProvider.class, EmailContent.AUTHORITY); - } - - @Override - public void setUp() throws Exception { - super.setUp(); - mMockContext = getMockContext(); - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - } - - private int getMessageCount(ViewType view) { - return EmailContent.count(mMockContext, Message.CONTENT_URI, view.selection, - view.selectionArgs); - } - - private static Message createMessage(Context c, Mailbox b, boolean starred, boolean read, - int flagLoaded) { - Message message = ProviderTestUtils.setupMessage( - "1", b.mAccountKey, b.mId, true, false, c, starred, read); - message.mFlagLoaded = flagLoaded; - message.save(c); - return message; - } - - public void testWidgetSwitcher() { - // Create account - ProviderTestUtils.setupAccount("account1", true, mMockContext); - - // Create a widget - EmailWidget widget = new EmailWidget(mMockContext, 1); - // Since there is one account, this should switch to the ACCOUNT view - widget.switchViewSyncForTest(); - assertEquals(ViewType.ACCOUNT, widget.mViewType); - - // Create account - ProviderTestUtils.setupAccount("account2", true, mMockContext); - // Create a widget - widget = new EmailWidget(mMockContext, 2); - // Since there are two accounts, this should switch to the ALL_INBOX view - widget.switchViewSyncForTest(); - assertEquals(ViewType.ALL_INBOX, widget.mViewType); - - // The next two switches should be to the two accounts - widget.switchViewSyncForTest(); - assertEquals(ViewType.ACCOUNT, widget.mViewType); - widget.switchViewSyncForTest(); - assertEquals(ViewType.ACCOUNT, widget.mViewType); - widget.switchViewSyncForTest(); - assertEquals(ViewType.UNREAD, widget.mViewType); - widget.switchViewSyncForTest(); - assertEquals(ViewType.STARRED, widget.mViewType); - } - - /** - * Test the message counts returned by the ViewType selectors. - */ - public void testCursorCount() { - // Create 2 accounts - Account a1 = ProviderTestUtils.setupAccount("account1", true, mMockContext); - Account a2 = ProviderTestUtils.setupAccount("account2", true, mMockContext); - - // Create 2 mailboxes for each account - Mailbox b1 = ProviderTestUtils.setupMailbox( - "box1", a1.mId, true, mMockContext, Mailbox.TYPE_INBOX); - Mailbox b2 = ProviderTestUtils.setupMailbox( - "box2", a1.mId, true, mMockContext, Mailbox.TYPE_OUTBOX); - Mailbox b3 = ProviderTestUtils.setupMailbox( - "box3", a2.mId, true, mMockContext, Mailbox.TYPE_INBOX); - Mailbox b4 = ProviderTestUtils.setupMailbox( - "box4", a2.mId, true, mMockContext, Mailbox.TYPE_OUTBOX); - Mailbox bt = ProviderTestUtils.setupMailbox( - "boxT", a2.mId, true, mMockContext, Mailbox.TYPE_TRASH); - - // Create some messages - // b1 (account 1, inbox): 2 messages, including 1 starred, 1 unloaded - Message m11 = createMessage(mMockContext, b1, true, false, Message.FLAG_LOADED_COMPLETE); - Message m12 = createMessage(mMockContext, b1, false, false, Message.FLAG_LOADED_UNLOADED); - - // b2 (account 1, outbox): 2 messages, including 1 starred - Message m21 = createMessage(mMockContext, b2, false, false, Message.FLAG_LOADED_COMPLETE); - Message m22 = createMessage(mMockContext, b2, true, true, Message.FLAG_LOADED_COMPLETE); - - // b3 (account 2, inbox): 4 messages, including 1 starred, 1 unloaded - Message m31 = createMessage(mMockContext, b3, false, false, Message.FLAG_LOADED_COMPLETE); - Message m32 = createMessage(mMockContext, b3, false, true, Message.FLAG_LOADED_COMPLETE); - Message m33 = createMessage(mMockContext, b3, true, true, Message.FLAG_LOADED_COMPLETE); - Message m34 = createMessage(mMockContext, b3, true, true, Message.FLAG_LOADED_UNLOADED); - - // b4 (account 2, outbox) has no messages. - - // bt (account 2, trash): 3 messages, including 2 starred - Message mt1 = createMessage(mMockContext, bt, true, false, Message.FLAG_LOADED_COMPLETE); - Message mt2 = createMessage(mMockContext, bt, true, true, Message.FLAG_LOADED_COMPLETE); - Message mt3 = createMessage(mMockContext, bt, false, false, Message.FLAG_LOADED_COMPLETE); - - assertEquals(4, getMessageCount(ViewType.ALL_INBOX)); - assertEquals(3, getMessageCount(ViewType.STARRED)); - assertEquals(2, getMessageCount(ViewType.UNREAD)); - } -} diff --git a/tests/src/com/android/email/widget/WidgetViewTests.java b/tests/src/com/android/email/widget/WidgetViewTests.java new file mode 100644 index 000000000..827c5e9c1 --- /dev/null +++ b/tests/src/com/android/email/widget/WidgetViewTests.java @@ -0,0 +1,215 @@ +/* Copyright (C) 2011 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.widget; + +import com.android.email.provider.EmailProvider; +import com.android.email.provider.ProviderTestUtils; +import com.android.emailcommon.provider.EmailContent; +import com.android.emailcommon.provider.EmailContent.Account; +import com.android.emailcommon.provider.EmailContent.Mailbox; +import com.android.emailcommon.provider.EmailContent.Message; + +import android.content.Context; +import android.test.ProviderTestCase2; + +/** + * Tests of EmailWidget + * + * You can run this entire test case with: + * runtest -c com.android.email.widget.WidgetView email + */ +public class WidgetViewTests extends ProviderTestCase2 { + private Context mMockContext; + + public WidgetViewTests() { + super(EmailProvider.class, EmailContent.AUTHORITY); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + mMockContext = getMockContext(); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + } + + private int getMessageCount(WidgetView view) { + return EmailContent.count(mMockContext, Message.CONTENT_URI, + view.getSelection(mMockContext), view.getSelectionArgs()); + } + + private static Message createMessage(Context c, Mailbox b, boolean starred, boolean read, + int flagLoaded) { + Message message = ProviderTestUtils.setupMessage( + "1", b.mAccountKey, b.mId, true, false, c, starred, read); + message.mFlagLoaded = flagLoaded; + message.save(c); + return message; + } + + public void testGetNext() { + // Test with 1 account. + final Account a1 = ProviderTestUtils.setupAccount("account1", true, mMockContext); + + WidgetView view = WidgetView.ALL_UNREAD; + + // all unread -> all starred + view = view.getNext(mMockContext); + assertEquals(WidgetView.ALL_STARRED, view); + + // all starred -> account 1 inbox + view = view.getNext(mMockContext); + assertTrue(view.isPerAccount()); + assertEquals(Long.toString(a1.mId), view.getSelectionArgs()[0]); + + // account 1 inbox -> all unread + view = view.getNext(mMockContext); + assertEquals(WidgetView.ALL_UNREAD, view); + + // Next, test with 2 accounts. + final Account a2 = ProviderTestUtils.setupAccount("account2", true, mMockContext); + + // Still all unread + assertEquals(WidgetView.ALL_UNREAD, view); + + // all unread -> all starred + view = view.getNext(mMockContext); + assertEquals(WidgetView.ALL_STARRED, view); + + // all starred -> all inboxes, as there are more than 1 account. + view = view.getNext(mMockContext); + assertEquals(WidgetView.ALL_INBOX, view); + + // all inbox -> account 1 inbox + view = view.getNext(mMockContext); + assertTrue(view.isPerAccount()); + assertEquals(Long.toString(a1.mId), view.getSelectionArgs()[0]); + + // account 1 inbox -> account 2 inbox + view = view.getNext(mMockContext); + assertTrue(view.isPerAccount()); + assertEquals(Long.toString(a2.mId), view.getSelectionArgs()[0]); + + // account 2 inbox -> all unread + view = view.getNext(mMockContext); + assertEquals(WidgetView.ALL_UNREAD, view); + } + + public void testIsValid() { + // with 0 accounts + assertTrue(WidgetView.ALL_UNREAD.isValid(mMockContext)); + assertTrue(WidgetView.ALL_STARRED.isValid(mMockContext)); + assertFalse(WidgetView.ALL_INBOX.isValid(mMockContext)); + + // Test with 1 account. + final Account a1 = ProviderTestUtils.setupAccount("account1", true, mMockContext); + assertTrue(WidgetView.ALL_UNREAD.isValid(mMockContext)); + assertTrue(WidgetView.ALL_STARRED.isValid(mMockContext)); + assertFalse(WidgetView.ALL_INBOX.isValid(mMockContext)); // only 1 account -- still invalid + + final WidgetView account1View = WidgetView.ALL_INBOX.getNext(mMockContext); + assertEquals(Long.toString(a1.mId), account1View.getSelectionArgs()[0]); + assertTrue(account1View.isValid(mMockContext)); + + // Test with 2 accounts. + final Account a2 = ProviderTestUtils.setupAccount("account2", true, mMockContext); + assertTrue(WidgetView.ALL_UNREAD.isValid(mMockContext)); + assertTrue(WidgetView.ALL_STARRED.isValid(mMockContext)); + assertTrue(WidgetView.ALL_INBOX.isValid(mMockContext)); // now it's valid + + final WidgetView account2View = account1View.getNext(mMockContext); + assertEquals(Long.toString(a2.mId), account2View.getSelectionArgs()[0]); + assertTrue(account2View.isValid(mMockContext)); + + // Remove account 1 + ProviderTestUtils.deleteAccount(mMockContext, a1.mId); + + assertTrue(WidgetView.ALL_UNREAD.isValid(mMockContext)); + assertTrue(WidgetView.ALL_STARRED.isValid(mMockContext)); + assertFalse(WidgetView.ALL_INBOX.isValid(mMockContext)); // only 1 account -- now invalid + + assertFalse(account1View.isValid(mMockContext)); + assertTrue(account2View.isValid(mMockContext)); + + // Remove account 2 + ProviderTestUtils.deleteAccount(mMockContext, a2.mId); + + assertTrue(WidgetView.ALL_UNREAD.isValid(mMockContext)); + assertTrue(WidgetView.ALL_STARRED.isValid(mMockContext)); + assertFalse(WidgetView.ALL_INBOX.isValid(mMockContext)); // still invalid + + assertFalse(account1View.isValid(mMockContext)); + assertFalse(account2View.isValid(mMockContext)); + } + + /** + * Test the message counts returned by the ViewType selectors. + */ + public void testCursorCount() { + // Create 2 accounts + Account a1 = ProviderTestUtils.setupAccount("account1", true, mMockContext); + Account a2 = ProviderTestUtils.setupAccount("account2", true, mMockContext); + + // Create 2 mailboxes for each account + Mailbox b11 = ProviderTestUtils.setupMailbox( + "box11", a1.mId, true, mMockContext, Mailbox.TYPE_INBOX); + Mailbox b12 = ProviderTestUtils.setupMailbox( + "box12", a1.mId, true, mMockContext, Mailbox.TYPE_OUTBOX); + Mailbox b21 = ProviderTestUtils.setupMailbox( + "box21", a2.mId, true, mMockContext, Mailbox.TYPE_INBOX); + Mailbox b22 = ProviderTestUtils.setupMailbox( + "box22", a2.mId, true, mMockContext, Mailbox.TYPE_OUTBOX); + Mailbox b2t = ProviderTestUtils.setupMailbox( + "box2T", a2.mId, true, mMockContext, Mailbox.TYPE_TRASH); + + // Create some messages + // b11 (account 1, inbox): 2 messages, including 1 starred, 1 unloaded + Message m11a = createMessage(mMockContext, b11, true, false, Message.FLAG_LOADED_COMPLETE); + Message m11b = createMessage(mMockContext, b11, false, false, Message.FLAG_LOADED_UNLOADED); + + // b12 (account 1, outbox): 2 messages, including 1 starred + Message m12a = createMessage(mMockContext, b12, false, false, Message.FLAG_LOADED_COMPLETE); + Message m12b = createMessage(mMockContext, b12, true, true, Message.FLAG_LOADED_COMPLETE); + + // b21 (account 2, inbox): 4 messages, including 1 starred, 1 unloaded + Message m21a = createMessage(mMockContext, b21, false, false, Message.FLAG_LOADED_COMPLETE); + Message m21b = createMessage(mMockContext, b21, false, true, Message.FLAG_LOADED_COMPLETE); + Message m21c = createMessage(mMockContext, b21, true, true, Message.FLAG_LOADED_COMPLETE); + Message m21d = createMessage(mMockContext, b21, true, true, Message.FLAG_LOADED_UNLOADED); + + // b22 (account 2, outbox) has no messages. + + // bt (account 2, trash): 3 messages, including 2 starred + Message mt1 = createMessage(mMockContext, b2t, true, false, Message.FLAG_LOADED_COMPLETE); + Message mt2 = createMessage(mMockContext, b2t, true, true, Message.FLAG_LOADED_COMPLETE); + Message mt3 = createMessage(mMockContext, b2t, false, false, Message.FLAG_LOADED_COMPLETE); + + assertEquals(4, getMessageCount(WidgetView.ALL_INBOX)); + assertEquals(3, getMessageCount(WidgetView.ALL_STARRED)); + assertEquals(2, getMessageCount(WidgetView.ALL_UNREAD)); + + final WidgetView account1View = WidgetView.ALL_INBOX.getNext(mMockContext); + assertEquals(Long.toString(a1.mId), account1View.getSelectionArgs()[0]); + assertEquals(1, getMessageCount(account1View)); + + final WidgetView account2View = account1View.getNext(mMockContext); + assertEquals(Long.toString(a2.mId), account2View.getSelectionArgs()[0]); + assertEquals(3, getMessageCount(account2View)); + } +}