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