Refactoring widget part 2

- Extracted the loader
- Extracted ViewType and introduced WidgetView.
  WidgetView is ViewType + mutable fields, such as account id.
  WidgetView now owns the method to switch views.

These two are basically in preparation to address the message count bug.
(we're showing total message count where it should be the unread count,
which is a bit tricky because it'll require two different queries.)

- Also simplified the threading model in EmailWidget to fix potential
  theading issues. (now (almost) everything works on the UI thread)

Bug 3431240

Change-Id: I9f8a268210995f1135baabe88b49b274272708d4
This commit is contained in:
Makoto Onuki 2011-02-08 18:53:31 -08:00
parent 3f60e9312b
commit 897a0ea81c
7 changed files with 713 additions and 469 deletions

View File

@ -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<Cursor> {
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 <code>true</code>, 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<Boolean, Void, Boolean> {
@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<Cursor> 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<Cursor>() {
@Override
public void onLoadComplete(Loader<Cursor> 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 <code>true</code>.
* 2. If the view is {@link ViewType#ALL_INBOX}, returns <code>true</code> if more than
* one account is defined. Otherwise, returns <code>false</code>.
* 3. If the view is {@link ViewType#ACCOUNT}, returns <code>true</code> if the account
* is defined. Otherwise, returns <code>false</code>.
*/
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<Void, Void, WidgetView> {
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<Void, Void, Void> {
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);
}
}
}

View File

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

View File

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

View File

@ -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 <code>true</code> if more than
* one account is defined. Otherwise, returns <code>false</code>.
* 3. If the view is {@link ViewType#TYPE_ACCOUNT_INBOX}, returns <code>true</code> if the
* account is defined. Otherwise, returns <code>false</code>.
*/
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();
}
}

View File

@ -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
*/

View File

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

View File

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