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