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