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