EmailWidget.java revision a7bc0319a75184ad706bb35c049af107ac3688e6
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.widget;
18
19import com.android.email.Email;
20import com.android.email.R;
21import com.android.email.ResourceHelper;
22import com.android.email.Utility;
23import com.android.email.activity.MessageCompose;
24import com.android.email.activity.Welcome;
25import com.android.email.data.ThrottlingCursorLoader;
26import com.android.email.provider.WidgetProvider.WidgetService;
27import com.android.emailcommon.provider.EmailContent;
28import com.android.emailcommon.provider.EmailContent.Account;
29import com.android.emailcommon.provider.EmailContent.AccountColumns;
30import com.android.emailcommon.provider.EmailContent.Mailbox;
31import com.android.emailcommon.provider.EmailContent.Message;
32import com.android.emailcommon.provider.EmailContent.MessageColumns;
33
34import android.app.PendingIntent;
35import android.appwidget.AppWidgetManager;
36import android.content.ContentResolver;
37import android.content.ContentUris;
38import android.content.Context;
39import android.content.Intent;
40import android.content.Loader;
41import android.content.res.Resources;
42import android.database.Cursor;
43import android.graphics.Typeface;
44import android.net.Uri;
45import android.net.Uri.Builder;
46import android.os.AsyncTask;
47import android.text.Spannable;
48import android.text.SpannableString;
49import android.text.SpannableStringBuilder;
50import android.text.TextUtils;
51import android.text.format.DateUtils;
52import android.text.style.AbsoluteSizeSpan;
53import android.text.style.ForegroundColorSpan;
54import android.text.style.StyleSpan;
55import android.util.Log;
56import android.view.View;
57import android.widget.RemoteViews;
58import android.widget.RemoteViewsService;
59
60import java.util.List;
61import java.util.concurrent.ExecutionException;
62
63import junit.framework.Assert;
64
65public class EmailWidget implements RemoteViewsService.RemoteViewsFactory {
66    public static final String TAG = "EmailWidget";
67
68    /**
69     * When handling clicks in a widget ListView, a single PendingIntent template is provided to
70     * RemoteViews, and the individual "on click" actions are distinguished via a "fillInIntent"
71     * on each list element; when a click is received, this "fillInIntent" is merged with the
72     * PendingIntent using Intent.fillIn().  Since this mechanism does NOT preserve the Extras
73     * Bundle, we instead encode information about the action (e.g. view, reply, etc.) and its
74     * arguments (e.g. messageId, mailboxId, etc.) in an Uri which is added to the Intent via
75     * Intent.setDataAndType()
76     *
77     * The mime type MUST be set in the Intent, even though we do not use it; therefore, it's value
78     * is entirely arbitrary.
79     *
80     * Our "command" Uri is NOT used by the system in any manner, and is therefore constrained only
81     * in the requirement that it be syntactically valid.
82     *
83     * We use the following convention for our commands:
84     *     widget://command/<command>/<arg1>[/<arg2>]
85     */
86    private static final String WIDGET_DATA_MIME_TYPE = "com.android.email/widget_data";
87
88    private static final Uri COMMAND_URI = Uri.parse("widget://command");
89
90    // Command names and Uri's built upon COMMAND_URI
91    private static final String COMMAND_NAME_SWITCH_LIST_VIEW = "switch_list_view";
92    private static final Uri COMMAND_URI_SWITCH_LIST_VIEW =
93            COMMAND_URI.buildUpon().appendPath(COMMAND_NAME_SWITCH_LIST_VIEW).build();
94    private static final String COMMAND_NAME_VIEW_MESSAGE = "view_message";
95    private static final Uri COMMAND_URI_VIEW_MESSAGE =
96            COMMAND_URI.buildUpon().appendPath(COMMAND_NAME_VIEW_MESSAGE).build();
97
98
99    private static final int TOTAL_COUNT_UNKNOWN = -1;
100    private static final int MAX_MESSAGE_LIST_COUNT = 25;
101
102    private static final String SORT_TIMESTAMP_DESCENDING = MessageColumns.TIMESTAMP + " DESC";
103    private static final String SORT_ID_ASCENDING = AccountColumns.ID + " ASC";
104    private static final String[] ID_NAME_PROJECTION = {Account.RECORD_ID, Account.DISPLAY_NAME};
105    private static final int ID_NAME_COLUMN_ID = 0;
106    private static final int ID_NAME_COLUMN_NAME = 1;
107
108    private static String sSubjectSnippetDivider;
109    private static String sConfigureText;
110    private static int sSenderFontSize;
111    private static int sSubjectFontSize;
112    private static int sDateFontSize;
113    private static int sDefaultTextColor;
114    private static int sLightTextColor;
115
116    private final Context mContext;
117    private final ContentResolver mResolver;
118    private final AppWidgetManager mWidgetManager;
119
120    // The widget identifier
121    private final int mWidgetId;
122
123    // The cursor underlying the message list for this widget; this must only be modified while
124    // holding mCursorLock
125    private volatile Cursor mCursor;
126    // A lock on our cursor, which is used in the UI thread while inflating views, and by
127    // our Loader in the background
128    private final Object mCursorLock = new Object();
129    // Number of records in the cursor
130    private int mCursorCount = TOTAL_COUNT_UNKNOWN;
131    // The widget's loader (derived from ThrottlingCursorLoader)
132    private ViewCursorLoader mLoader;
133    private final ResourceHelper mResourceHelper;
134    // Number of defined accounts
135    private int mAccountCount = TOTAL_COUNT_UNKNOWN;
136
137    // The current view type (all mail, unread, or starred for now)
138    /*package*/ ViewType mViewType = ViewType.STARRED;
139
140    // The projection to be used by the WidgetLoader
141    private static final String[] WIDGET_PROJECTION = new String[] {
142            EmailContent.RECORD_ID, MessageColumns.DISPLAY_NAME, MessageColumns.TIMESTAMP,
143            MessageColumns.SUBJECT, MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE,
144            MessageColumns.FLAG_ATTACHMENT, MessageColumns.MAILBOX_KEY, MessageColumns.SNIPPET,
145            MessageColumns.ACCOUNT_KEY, MessageColumns.FLAGS
146            };
147    private static final int WIDGET_COLUMN_ID = 0;
148    private static final int WIDGET_COLUMN_DISPLAY_NAME = 1;
149    private static final int WIDGET_COLUMN_TIMESTAMP = 2;
150    private static final int WIDGET_COLUMN_SUBJECT = 3;
151    private static final int WIDGET_COLUMN_FLAG_READ = 4;
152    @SuppressWarnings("unused")
153    private static final int WIDGET_COLUMN_FLAG_FAVORITE = 5;
154    private static final int WIDGET_COLUMN_FLAG_ATTACHMENT = 6;
155    private static final int WIDGET_COLUMN_MAILBOX_KEY = 7;
156    private static final int WIDGET_COLUMN_SNIPPET = 8;
157    private static final int WIDGET_COLUMN_ACCOUNT_KEY = 9;
158    private static final int WIDGET_COLUMN_FLAGS = 10;
159
160    public EmailWidget(Context context, int _widgetId) {
161        super();
162        if (Email.DEBUG) {
163            Log.d(TAG, "Creating EmailWidget with id = " + _widgetId);
164        }
165        mContext = context.getApplicationContext();
166        mResolver = mContext.getContentResolver();
167        mWidgetManager = AppWidgetManager.getInstance(mContext);
168
169        mWidgetId = _widgetId;
170        mLoader = new ViewCursorLoader();
171        if (sSubjectSnippetDivider == null) {
172            // Initialize string, color, dimension resources
173            Resources res = mContext.getResources();
174            sSubjectSnippetDivider =
175                res.getString(R.string.message_list_subject_snippet_divider);
176            sSenderFontSize = res.getDimensionPixelSize(R.dimen.widget_senders_font_size);
177            sSubjectFontSize = res.getDimensionPixelSize(R.dimen.widget_subject_font_size);
178            sDateFontSize = res.getDimensionPixelSize(R.dimen.widget_date_font_size);
179            sDefaultTextColor = res.getColor(R.color.widget_default_text_color);
180            sDefaultTextColor = res.getColor(R.color.widget_default_text_color);
181            sLightTextColor = res.getColor(R.color.widget_light_text_color);
182            sConfigureText =  res.getString(R.string.widget_other_views);
183        }
184        mResourceHelper = ResourceHelper.getInstance(mContext);
185    }
186
187    public void updateWidget(boolean validateView) {
188        new WidgetUpdateTask().execute(validateView);
189    }
190
191    /**
192     *  Task for updating widget data (eg: the header, view list items, etc...)
193     *  If parameter to {@link #execute(Boolean...)} is <code>true</code>, the current
194     *  view is validated against the current set of accounts. And if the current view
195     *  is determined to be invalid, the view will automatically progress to the next
196     *  valid view.
197     */
198    private final class WidgetUpdateTask extends AsyncTask<Boolean, Void, Boolean> {
199        @Override
200        protected Boolean doInBackground(Boolean... validateView) {
201            mAccountCount = EmailContent.count(mContext, EmailContent.Account.CONTENT_URI);
202            // If displaying invalid view, switch to the next view
203            return !validateView[0] || isViewValid();
204        }
205
206        @Override
207        protected void onPostExecute(Boolean isValidView) {
208            updateHeader();
209            if (!isValidView) {
210                switchView();
211            }
212        }
213    }
214
215    /**
216     * The ThrottlingCursorLoader does all of the heavy lifting in managing the data loading
217     * task; all we need is to register a listener so that we're notified when the load is
218     * complete.
219     */
220    private final class ViewCursorLoader extends ThrottlingCursorLoader {
221        protected ViewCursorLoader() {
222            super(mContext, Message.CONTENT_URI, WIDGET_PROJECTION, mViewType.selection,
223                    mViewType.selectionArgs, SORT_TIMESTAMP_DESCENDING);
224            registerListener(0, new OnLoadCompleteListener<Cursor>() {
225                @Override
226                public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) {
227                    synchronized (mCursorLock) {
228                        // Save away the cursor
229                        mCursor = cursor;
230                        // Reset the notification Uri to our Message table notifier URI
231                        mCursor.setNotificationUri(mResolver, Message.NOTIFIER_URI);
232                        // Save away the count (for display)
233                        mCursorCount = mCursor.getCount();
234                        if (Email.DEBUG) {
235                            Log.d(TAG, "onLoadComplete, count = " + cursor.getCount());
236                        }
237                    }
238                    RemoteViews views =
239                        new RemoteViews(mContext.getPackageName(), R.layout.widget);
240                    setupTitleAndCount(views);
241                    mWidgetManager.partiallyUpdateAppWidget(mWidgetId, views);
242                    mWidgetManager.notifyAppWidgetViewDataChanged(mWidgetId, R.id.message_list);
243                }
244            });
245        }
246
247        /**
248         * Stop any pending load, reset selection parameters, and start loading
249         * Must be called from the UI thread
250         * @param viewType the current ViewType
251         */
252        private void load(ViewType viewType) {
253            reset();
254            setSelection(viewType.selection);
255            setSelectionArgs(viewType.selectionArgs);
256            startLoading();
257        }
258    }
259
260    /**
261     * Initialize to first appropriate view (depending on the number of accounts)
262     */
263    public void init() {
264        // Just update the account count & header; no need to validate the view
265        updateWidget(false);
266        switchView(); // TODO Do we really need this??
267    }
268
269    /**
270     * Reset cursor and cursor count, notify widget that list data is invalid, and start loading
271     * with our current ViewType
272     */
273    private void loadView() {
274        synchronized(mCursorLock) {
275            mCursorCount = TOTAL_COUNT_UNKNOWN;
276            mCursor = null;
277            mWidgetManager.notifyAppWidgetViewDataChanged(mWidgetId, R.id.message_list);
278            mLoader.load(mViewType);
279        }
280    }
281
282    /**
283     * Switch to the next widget view (all -> account1 -> ... -> account n -> unread -> starred)
284     *
285     * This must be called on a background thread.  Use {@link #switchView} on the UI thread.
286     */
287    private synchronized void switchToNextView() {
288        switch(mViewType) {
289            // If we're in starred and there is more than one account, go to "all mail"
290            // Otherwise, fall through to the accounts themselves
291            case STARRED:
292                if (EmailContent.count(mContext, Account.CONTENT_URI) > 1) {
293                    mViewType = ViewType.ALL_INBOX;
294                    break;
295                }
296                //$FALL-THROUGH$
297            case ALL_INBOX:
298                ViewType.ACCOUNT.selectionArgs[0] = "0";
299                //$FALL-THROUGH$
300            case ACCOUNT:
301                // Find the next account (or, if none, default to UNREAD)
302                String idString = ViewType.ACCOUNT.selectionArgs[0];
303                Cursor c = mResolver.query(Account.CONTENT_URI, ID_NAME_PROJECTION, "_id>?",
304                        new String[] {idString}, SORT_ID_ASCENDING);
305                try {
306                    if (c.moveToFirst()) {
307                        mViewType = ViewType.ACCOUNT;
308                        mViewType.selectionArgs[0] = c.getString(ID_NAME_COLUMN_ID);
309                        mViewType.setTitle(c.getString(ID_NAME_COLUMN_NAME));
310                    } else {
311                        mViewType = ViewType.UNREAD;
312                    }
313                } finally {
314                    c.close();
315                }
316                break;
317            case UNREAD:
318                mViewType = ViewType.STARRED;
319                break;
320        }
321    }
322
323    /**
324     * Returns whether the current view is valid. The following rules determine if a view is
325     * considered valid:
326     * 1. If the view is either {@link ViewType#STARRED} or {@link ViewType#UNREAD}, always
327     * returns <code>true</code>.
328     * 2. If the view is {@link ViewType#ALL_INBOX}, returns <code>true</code> if more than
329     * one account is defined. Otherwise, returns <code>false</code>.
330     * 3. If the view is {@link ViewType#ACCOUNT}, returns <code>true</code> if the account
331     * is defined. Otherwise, returns <code>false</code>.
332     */
333    private boolean isViewValid() {
334        switch(mViewType) {
335            case ALL_INBOX:
336                // "all inbox" is valid only if there is more than one account
337                return (EmailContent.count(mContext, Account.CONTENT_URI) > 1);
338            case ACCOUNT:
339                // Ensure current account still exists
340                String idString = ViewType.ACCOUNT.selectionArgs[0];
341                Cursor c = mResolver.query(Account.CONTENT_URI, ID_NAME_PROJECTION, "_id=?",
342                        new String[] {idString}, SORT_ID_ASCENDING);
343                try {
344                    return c.moveToFirst();
345                } finally {
346                    c.close();
347                }
348        }
349        return true;
350    }
351
352    /**
353     * Convenience method for creating an onClickPendingIntent that executes a command via
354     * our command Uri.  Used for the "next view" command; appends the widget id to the command
355     * Uri.
356     *
357     * @param views The RemoteViews we're inflating
358     * @param buttonId the id of the button view
359     * @param data the command Uri
360     */
361    private void setCommandIntent(RemoteViews views, int buttonId, Uri data) {
362        Intent intent = new Intent(mContext, WidgetService.class);
363        intent.setDataAndType(ContentUris.withAppendedId(data, mWidgetId), WIDGET_DATA_MIME_TYPE);
364        PendingIntent pendingIntent = PendingIntent.getService(mContext, 0, intent,
365                PendingIntent.FLAG_UPDATE_CURRENT);
366        views.setOnClickPendingIntent(buttonId, pendingIntent);
367    }
368
369    /**
370     * Convenience method for creating an onClickPendingIntent that launches another activity
371     * directly.
372     *
373     * @param views The RemoteViews we're inflating
374     * @param buttonId the id of the button view
375     * @param intent The intent to be used when launching the activity
376     */
377    private void setActivityIntent(RemoteViews views, int buttonId, Intent intent) {
378        PendingIntent pendingIntent =
379                PendingIntent.getActivity(mContext, 0, intent, 0);
380        views.setOnClickPendingIntent(buttonId, pendingIntent);
381    }
382
383    /**
384     * Convenience method for constructing a fillInIntent for a given list view element.
385     * Appends the command and any arguments to a base Uri.
386     *
387     * @param views the RemoteViews we are inflating
388     * @param viewId the id of the view
389     * @param baseUri the base uri for the command
390     * @param args any arguments to the command
391     */
392    private void setFillInIntent(RemoteViews views, int viewId, Uri baseUri, String ... args) {
393        Intent intent = new Intent();
394        Builder builder = baseUri.buildUpon();
395        for (String arg: args) {
396            builder.appendPath(arg);
397        }
398        intent.setDataAndType(builder.build(), WIDGET_DATA_MIME_TYPE);
399        views.setOnClickFillInIntent(viewId, intent);
400    }
401
402    /**
403     * Called back by {@link com.android.email.provider.WidgetProvider.WidgetService} to
404     * handle intents created by remote views.
405     */
406    public static boolean processIntent(Context context, Intent intent) {
407        final Uri data = intent.getData();
408        if (data == null) {
409            return false;
410        }
411        List<String> pathSegments = data.getPathSegments();
412        // Our path segments are <command>, <arg1> [, <arg2>]
413        // First, a quick check of Uri validity
414        if (pathSegments.size() < 2) {
415            throw new IllegalArgumentException();
416        }
417        String command = pathSegments.get(0);
418        // Ignore unknown action names
419        try {
420            final long arg1 = Long.parseLong(pathSegments.get(1));
421            if (EmailWidget.COMMAND_NAME_VIEW_MESSAGE.equals(command)) {
422                // "view", <message id>, <mailbox id>
423                openMessage(context, Long.parseLong(pathSegments.get(2)), arg1);
424            } else if (EmailWidget.COMMAND_NAME_SWITCH_LIST_VIEW.equals(command)) {
425                // "next_view", <widget id>
426                EmailWidget widget = WidgetManager.getInstance().get((int)arg1);
427                if (widget != null) {
428                    widget.switchView();
429                }
430            }
431        } catch (NumberFormatException e) {
432            // Shouldn't happen as we construct all of the Uri's
433            return false;
434        }
435        return true;
436    }
437
438    private static void openMessage(final Context context, final long mailboxId,
439            final long messageId) {
440        Utility.runAsync(new Runnable() {
441            @Override
442            public void run() {
443                Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
444                if (mailbox == null) return;
445                context.startActivity(Welcome.createOpenMessageIntent(context, mailbox.mAccountKey,
446                        mailboxId, messageId));
447            }
448        });
449    }
450
451    private void setupTitleAndCount(RemoteViews views) {
452        // Set up the title (view type + count of messages)
453        views.setTextViewText(R.id.widget_title, mViewType.getTitle(mContext));
454        views.setTextViewText(R.id.widget_tap, sConfigureText);
455        String count = "";
456        if (mCursorCount != TOTAL_COUNT_UNKNOWN) {
457            count = Utility.getMessageCountForUi(mContext, mCursor.getCount(), false);
458        }
459        views.setTextViewText(R.id.widget_count, count);
460    }
461    /**
462     * Update the "header" of the widget (i.e. everything that doesn't include the scrolling
463     * message list)
464     */
465    public void updateHeader() {
466        if (Email.DEBUG) {
467            Log.d(TAG, "updateWidget " + mWidgetId);
468        }
469
470        // Get the widget layout
471        RemoteViews views =
472                new RemoteViews(mContext.getPackageName(), R.layout.widget);
473
474        // Set up the list with an adapter
475        Intent intent = new Intent(mContext, WidgetService.class);
476        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId);
477        intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
478        views.setRemoteAdapter(mWidgetId, R.id.message_list, intent);
479
480        setupTitleAndCount(views);
481
482        if (mAccountCount == 0) {
483            // Hide compose icon & show "touch to configure" text
484            views.setViewVisibility(R.id.widget_compose, View.INVISIBLE);
485            views.setViewVisibility(R.id.message_list, View.GONE);
486            views.setViewVisibility(R.id.tap_to_configure, View.VISIBLE);
487            // Create click intent for "touch to configure" target
488            intent = Welcome.createOpenAccountInboxIntent(mContext, -1);
489            setActivityIntent(views, R.id.tap_to_configure, intent);
490        } else {
491            // Show compose icon & message list
492            views.setViewVisibility(R.id.widget_compose, View.VISIBLE);
493            views.setViewVisibility(R.id.message_list, View.VISIBLE);
494            views.setViewVisibility(R.id.tap_to_configure, View.GONE);
495            // Create click intent for "compose email" target
496            intent = MessageCompose.getMessageComposeIntent(mContext, -1);
497            setActivityIntent(views, R.id.widget_compose, intent);
498        }
499        // Create click intent for "view rotation" target
500        setCommandIntent(views, R.id.widget_logo, COMMAND_URI_SWITCH_LIST_VIEW);
501
502        // Use a bare intent for our template; we need to fill everything in
503        intent = new Intent(mContext, WidgetService.class);
504        PendingIntent pendingIntent = PendingIntent.getService(mContext, 0, intent,
505                PendingIntent.FLAG_UPDATE_CURRENT);
506        views.setPendingIntentTemplate(R.id.message_list, pendingIntent);
507
508        // And finally update the widget
509        mWidgetManager.updateAppWidget(mWidgetId, views);
510    }
511
512    /**
513     * Add size and color styling to text
514     *
515     * @param text the text to style
516     * @param size the font size for this text
517     * @param color the color for this text
518     * @return a CharSequence quitable for use in RemoteViews.setTextViewText()
519     */
520    private CharSequence addStyle(CharSequence text, int size, int color) {
521        SpannableStringBuilder builder = new SpannableStringBuilder(text);
522        builder.setSpan(
523                new AbsoluteSizeSpan(size), 0, text.length(),
524                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
525        if (color != 0) {
526            builder.setSpan(new ForegroundColorSpan(color), 0, text.length(),
527                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
528        }
529        return builder;
530    }
531
532    /**
533     * Create styled text for our combination subject and snippet
534     *
535     * @param subject the message's subject (or null)
536     * @param snippet the message's snippet (or null)
537     * @param read whether or not the message is read
538     * @return a CharSequence suitable for use in RemoteViews.setTextViewText()
539     */
540    private CharSequence getStyledSubjectSnippet (String subject, String snippet,
541            boolean read) {
542        SpannableStringBuilder ssb = new SpannableStringBuilder();
543        boolean hasSubject = false;
544        if (!TextUtils.isEmpty(subject)) {
545            SpannableString ss = new SpannableString(subject);
546            ss.setSpan(new StyleSpan(read ? Typeface.NORMAL : Typeface.BOLD), 0, ss.length(),
547                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
548            ss.setSpan(new ForegroundColorSpan(sDefaultTextColor), 0, ss.length(),
549                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
550            ssb.append(ss);
551            hasSubject = true;
552        }
553        if (!TextUtils.isEmpty(snippet)) {
554            if (hasSubject) {
555                ssb.append(sSubjectSnippetDivider);
556            }
557            SpannableString ss = new SpannableString(snippet);
558            ss.setSpan(new ForegroundColorSpan(sLightTextColor), 0, snippet.length(),
559                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
560            ssb.append(ss);
561        }
562        return addStyle(ssb, sSubjectFontSize, 0);
563    }
564
565    @Override
566    public RemoteViews getViewAt(int position) {
567        // Use the cursor to set up the widget
568        synchronized (mCursorLock) {
569            if (mCursor == null || mCursor.isClosed() || !mCursor.moveToPosition(position)) {
570                return getLoadingView();
571            }
572            RemoteViews views =
573                new RemoteViews(mContext.getPackageName(), R.layout.widget_list_item);
574            boolean isUnread = mCursor.getInt(WIDGET_COLUMN_FLAG_READ) != 1;
575            int drawableId = R.drawable.widget_read_conversation_selector;
576            if (isUnread) {
577                drawableId = R.drawable.widget_unread_conversation_selector;
578            }
579            views.setInt(R.id.widget_message, "setBackgroundResource", drawableId);
580
581            // Add style to sender
582            SpannableStringBuilder from =
583                new SpannableStringBuilder(mCursor.getString(WIDGET_COLUMN_DISPLAY_NAME));
584            from.setSpan(
585                    isUnread ? new StyleSpan(Typeface.BOLD) : new StyleSpan(Typeface.NORMAL), 0,
586                    from.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
587            CharSequence styledFrom = addStyle(from, sSenderFontSize, sDefaultTextColor);
588            views.setTextViewText(R.id.widget_from, styledFrom);
589
590            long timestamp = mCursor.getLong(WIDGET_COLUMN_TIMESTAMP);
591            // Get a nicely formatted date string (relative to today)
592            String date = DateUtils.getRelativeTimeSpanString(mContext, timestamp).toString();
593            // Add style to date
594            CharSequence styledDate = addStyle(date, sDateFontSize, sDefaultTextColor);
595            views.setTextViewText(R.id.widget_date, styledDate);
596
597            // Add style to subject/snippet
598            String subject = mCursor.getString(WIDGET_COLUMN_SUBJECT);
599            String snippet = mCursor.getString(WIDGET_COLUMN_SNIPPET);
600            CharSequence subjectAndSnippet =
601                getStyledSubjectSnippet(subject, snippet, !isUnread);
602            views.setTextViewText(R.id.widget_subject, subjectAndSnippet);
603
604            int messageFlags = mCursor.getInt(WIDGET_COLUMN_FLAGS);
605            boolean hasInvite = (messageFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0;
606            views.setViewVisibility(R.id.widget_invite, hasInvite ? View.VISIBLE : View.GONE);
607
608            boolean hasAttachment = mCursor.getInt(WIDGET_COLUMN_FLAG_ATTACHMENT) != 0;
609            views.setViewVisibility(R.id.widget_attachment,
610                    hasAttachment ? View.VISIBLE : View.GONE);
611
612            if (mViewType == ViewType.ACCOUNT) {
613                views.setViewVisibility(R.id.color_chip, View.INVISIBLE);
614            } else {
615                long accountId = mCursor.getLong(WIDGET_COLUMN_ACCOUNT_KEY);
616                int colorId = mResourceHelper.getAccountColorId(accountId);
617                if (colorId != ResourceHelper.UNDEFINED_RESOURCE_ID) {
618                    // Color defined by resource ID, so, use it
619                    views.setViewVisibility(R.id.color_chip, View.VISIBLE);
620                    views.setImageViewResource(R.id.color_chip, colorId);
621                } else {
622                    // Color not defined by resource ID, nothing we can do, so, hide the chip
623                    views.setViewVisibility(R.id.color_chip, View.INVISIBLE);
624                }
625            }
626
627            // Set button intents for view, reply, and delete
628            String messageId = mCursor.getString(WIDGET_COLUMN_ID);
629            String mailboxId = mCursor.getString(WIDGET_COLUMN_MAILBOX_KEY);
630            setFillInIntent(views, R.id.widget_message, COMMAND_URI_VIEW_MESSAGE,
631                    messageId, mailboxId);
632
633            return views;
634        }
635    }
636
637    @Override
638    public int getCount() {
639        if (mCursor == null) return 0;
640        return Math.min(mCursor.getCount(), MAX_MESSAGE_LIST_COUNT);
641    }
642
643    @Override
644    public long getItemId(int position) {
645        return position;
646    }
647
648    @Override
649    public RemoteViews getLoadingView() {
650        RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
651        view.setTextViewText(R.id.loading_text, mContext.getString(R.string.widget_loading));
652        return view;
653    }
654
655    @Override
656    public int getViewTypeCount() {
657        // Regular list view and the "loading" view
658        return 2;
659    }
660
661    @Override
662    public boolean hasStableIds() {
663        return true;
664    }
665
666    @Override
667    public void onDataSetChanged() {
668    }
669
670    public void onDeleted() {
671        if (mLoader != null) {
672            mLoader.stopLoading();
673        }
674        WidgetManager.getInstance().remove(mWidgetId);
675    }
676
677    @Override
678    public void onDestroy() {
679        if (mLoader != null) {
680            mLoader.stopLoading();
681        }
682        WidgetManager.getInstance().remove(mWidgetId);
683    }
684
685    @Override
686    public void onCreate() {
687    }
688
689    /**
690     * Switch to the next view.
691     */
692    /* package */ void switchView() {
693        switchView(false);
694    }
695
696    private WidgetViewSwitcher switchView(boolean disableLoadAfterSwitchForTest) {
697        WidgetViewSwitcher switcher = new WidgetViewSwitcher(this, disableLoadAfterSwitchForTest);
698        switcher.execute();
699        return switcher;
700    }
701
702    /**
703     * Switch views synchronously without loading
704     */
705    /* package */ void switchViewSyncForTest() {
706        WidgetViewSwitcher switcher = switchView(true);
707        try {
708            switcher.get();
709        } catch (InterruptedException e) {
710            Assert.fail();
711        } catch (ExecutionException e) {
712            Assert.fail();
713        }
714    }
715
716    /**
717     * Utility class to handle switching widget views; in the background, we access the database
718     * to determine account status, etc.  In the foreground, we start up the Loader with new
719     * parameters
720     */
721    private static class WidgetViewSwitcher extends AsyncTask<Void, Void, Void> {
722        private final EmailWidget mWidget;
723        private final boolean mDisableLoadAfterSwitchForTest;
724
725        public WidgetViewSwitcher(EmailWidget widget, boolean disableLoadAfterSwitchForTest) {
726            mWidget = widget;
727            mDisableLoadAfterSwitchForTest = disableLoadAfterSwitchForTest;
728        }
729
730        @Override
731        protected Void doInBackground(Void... params) {
732            mWidget.switchToNextView();
733            return null;
734        }
735
736        @Override
737        protected void onPostExecute(Void param) {
738            if (isCancelled()) {
739                return;
740            }
741            if (!mDisableLoadAfterSwitchForTest) {
742                mWidget.loadView();
743            }
744        }
745    }
746}