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