WidgetService.java revision c197d4c5bfe62255ca664d7029367ef45f946219
1/*
2 * Copyright (C) 2012 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 */
16package com.android.mail.widget;
17
18import android.app.PendingIntent;
19import android.appwidget.AppWidgetManager;
20import android.content.Context;
21import android.content.CursorLoader;
22import android.content.Intent;
23import android.content.Loader;
24import android.content.Loader.OnLoadCompleteListener;
25import android.content.res.Resources;
26import android.database.Cursor;
27import android.net.Uri;
28import android.os.Looper;
29import android.support.v4.app.TaskStackBuilder;
30import android.text.SpannableString;
31import android.text.SpannableStringBuilder;
32import android.text.TextUtils;
33import android.text.format.DateUtils;
34import android.text.style.CharacterStyle;
35import android.text.style.TextAppearanceSpan;
36import android.view.View;
37import android.widget.RemoteViews;
38import android.widget.RemoteViewsService;
39
40import com.android.mail.R;
41import com.android.mail.browse.SendersView;
42import com.android.mail.compose.ComposeActivity;
43import com.android.mail.preferences.MailPrefs;
44import com.android.mail.providers.Account;
45import com.android.mail.providers.Conversation;
46import com.android.mail.providers.Folder;
47import com.android.mail.providers.UIProvider;
48import com.android.mail.providers.UIProvider.ConversationListQueryParameters;
49import com.android.mail.providers.UIProvider.FolderType;
50import com.android.mail.utils.AccountUtils;
51import com.android.mail.utils.FolderUri;
52import com.android.mail.utils.DelayedTaskHandler;
53import com.android.mail.utils.LogTag;
54import com.android.mail.utils.LogUtils;
55import com.android.mail.utils.Utils;
56
57import java.util.ArrayList;
58
59public class WidgetService extends RemoteViewsService {
60    /**
61     * Lock to avoid race condition between widgets.
62     */
63    private static final Object sWidgetLock = new Object();
64
65    private static final String LOG_TAG = LogTag.getLogTag();
66
67    @Override
68    public RemoteViewsFactory onGetViewFactory(Intent intent) {
69        return new MailFactory(getApplicationContext(), intent, this);
70    }
71
72    protected void configureValidAccountWidget(Context context, RemoteViews remoteViews,
73            int appWidgetId, Account account, final int folderType, final Uri folderUri,
74            final Uri folderConversationListUri, String folderName) {
75        configureValidAccountWidget(context, remoteViews, appWidgetId, account, folderType,
76                folderUri, folderConversationListUri, folderName, WidgetService.class);
77    }
78
79    /**
80     * Modifies the remoteView for the given account and folder.
81     */
82    public static void configureValidAccountWidget(Context context, RemoteViews remoteViews,
83            int appWidgetId, Account account, final int folderType, final Uri folderUri,
84            final Uri folderConversationListUri, String folderDisplayName, Class<?> widgetService) {
85        remoteViews.setViewVisibility(R.id.widget_folder, View.VISIBLE);
86
87        // If the folder or account name are empty, we don't want to overwrite the valid data that
88        // had been saved previously.  Since the launcher will save the state of the remote views
89        // we should rely on the fact that valid data has been saved.  But we should still log this,
90        // as it shouldn't happen
91        if (TextUtils.isEmpty(folderDisplayName) || TextUtils.isEmpty(account.name)) {
92            LogUtils.e(LOG_TAG, new Error(),
93                    "Empty folder or account name.  account: %s, folder: %s",
94                    account.name, folderDisplayName);
95        }
96        if (!TextUtils.isEmpty(folderDisplayName)) {
97            remoteViews.setTextViewText(R.id.widget_folder, folderDisplayName);
98        }
99        remoteViews.setViewVisibility(R.id.widget_account_noflip, View.VISIBLE);
100
101        if (!TextUtils.isEmpty(account.name)) {
102            remoteViews.setTextViewText(R.id.widget_account_noflip, account.name);
103            remoteViews.setTextViewText(R.id.widget_account, account.name);
104        }
105        remoteViews.setViewVisibility(R.id.widget_account_unread_flipper, View.GONE);
106        remoteViews.setViewVisibility(R.id.widget_compose, View.VISIBLE);
107        remoteViews.setViewVisibility(R.id.conversation_list, View.VISIBLE);
108        remoteViews.setViewVisibility(R.id.empty_conversation_list, View.VISIBLE);
109        remoteViews.setViewVisibility(R.id.widget_folder_not_synced, View.GONE);
110        remoteViews.setViewVisibility(R.id.widget_configuration, View.GONE);
111        remoteViews.setEmptyView(R.id.conversation_list, R.id.empty_conversation_list);
112
113        WidgetService.configureValidWidgetIntents(context, remoteViews, appWidgetId, account,
114                folderType, folderUri, folderConversationListUri, folderDisplayName, widgetService);
115    }
116
117    public static void configureValidWidgetIntents(Context context, RemoteViews remoteViews,
118            int appWidgetId, Account account, final int folderType, final Uri folderUri,
119            final Uri folderConversationListUri, final String folderDisplayName,
120            Class<?> serviceClass) {
121        remoteViews.setViewVisibility(R.id.widget_configuration, View.GONE);
122
123
124        // Launch an intent to avoid ANRs
125        final Intent intent = new Intent(context, serviceClass);
126        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
127        intent.putExtra(Utils.EXTRA_ACCOUNT, account.serialize());
128        intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_TYPE, folderType);
129        intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_URI, folderUri);
130        intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_CONVERSATION_LIST_URI,
131                folderConversationListUri);
132        intent.putExtra(BaseWidgetProvider.EXTRA_FOLDER_DISPLAY_NAME, folderDisplayName);
133        intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
134        remoteViews.setRemoteAdapter(R.id.conversation_list, intent);
135        // Open mail app when click on header
136        final Intent mailIntent = Utils.createViewFolderIntent(context, folderUri, account);
137        PendingIntent clickIntent = PendingIntent.getActivity(context, 0, mailIntent,
138                PendingIntent.FLAG_UPDATE_CURRENT);
139        remoteViews.setOnClickPendingIntent(R.id.widget_header, clickIntent);
140
141        // On click intent for Compose
142        final Intent composeIntent = new Intent();
143        composeIntent.setAction(Intent.ACTION_SEND);
144        composeIntent.putExtra(Utils.EXTRA_ACCOUNT, account.serialize());
145        composeIntent.setData(account.composeIntentUri);
146        composeIntent.putExtra(ComposeActivity.EXTRA_FROM_EMAIL_TASK, true);
147        if (account.composeIntentUri != null) {
148            composeIntent.putExtra(Utils.EXTRA_COMPOSE_URI, account.composeIntentUri);
149        }
150
151        // Build a task stack that forces the conversation list on the stack before the compose
152        // activity.
153        final TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context);
154        clickIntent = taskStackBuilder.addNextIntent(mailIntent)
155                .addNextIntent(composeIntent)
156                .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
157        remoteViews.setOnClickPendingIntent(R.id.widget_compose, clickIntent);
158
159        // On click intent for Conversation
160        final Intent conversationIntent = new Intent();
161        conversationIntent.setAction(Intent.ACTION_VIEW);
162        clickIntent = PendingIntent.getActivity(context, 0, conversationIntent,
163                PendingIntent.FLAG_UPDATE_CURRENT);
164        remoteViews.setPendingIntentTemplate(R.id.conversation_list, clickIntent);
165    }
166
167    /**
168     * Persists the information about the specified widget.
169     */
170    public static void saveWidgetInformation(Context context, int appWidgetId, Account account,
171                final String folderUri) {
172        MailPrefs.get(context).configureWidget(appWidgetId, account, folderUri);
173    }
174
175    /**
176     * Returns true if this widget id has been configured and saved.
177     */
178    public boolean isWidgetConfigured(Context context, int appWidgetId, Account account) {
179        return isAccountValid(context, account) &&
180                MailPrefs.get(context).isWidgetConfigured(appWidgetId);
181    }
182
183    protected boolean isAccountValid(Context context, Account account) {
184        if (account != null) {
185            Account[] accounts = AccountUtils.getSyncingAccounts(context);
186            for (Account existing : accounts) {
187                if (existing != null && account.uri.equals(existing.uri)) {
188                    return true;
189                }
190            }
191        }
192        return false;
193    }
194
195    /**
196     * Remote Views Factory for Mail Widget.
197     */
198    protected static class MailFactory
199            implements RemoteViewsService.RemoteViewsFactory, OnLoadCompleteListener<Cursor> {
200        private static final int MAX_CONVERSATIONS_COUNT = 25;
201        private static final int MAX_SENDERS_LENGTH = 25;
202
203        private static final int FOLDER_LOADER_ID = 0;
204        private static final int CONVERSATION_CURSOR_LOADER_ID = 1;
205        private static final int ACCOUNT_LOADER_ID = 2;
206
207        private final Context mContext;
208        private final int mAppWidgetId;
209        private final Account mAccount;
210        private final int mFolderType;
211        private final Uri mFolderUri;
212        private final Uri mFolderConversationListUri;
213        private final String mFolderDisplayName;
214        private final WidgetConversationListItemViewBuilder mWidgetConversationListItemViewBuilder;
215        private CursorLoader mConversationCursorLoader;
216        private Cursor mConversationCursor;
217        private CursorLoader mFolderLoader;
218        private CursorLoader mAccountLoader;
219        private FolderUpdateHandler mFolderUpdateHandler;
220        private int mFolderCount;
221        private boolean mShouldShowViewMore;
222        private boolean mFolderInformationShown = false;
223        private final WidgetService mService;
224        private String mSendersSplitToken;
225        private String mElidedPaddingToken;
226        private TextAppearanceSpan mUnreadStyle;
227        private TextAppearanceSpan mReadStyle;
228
229        public MailFactory(Context context, Intent intent, WidgetService service) {
230            mContext = context;
231            mAppWidgetId = intent.getIntExtra(
232                    AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
233            mAccount = Account.newinstance(intent.getStringExtra(Utils.EXTRA_ACCOUNT));
234            mFolderType = intent.getIntExtra(WidgetProvider.EXTRA_FOLDER_TYPE, FolderType.DEFAULT);
235            mFolderDisplayName = intent.getStringExtra(WidgetProvider.EXTRA_FOLDER_DISPLAY_NAME);
236
237            final Uri folderUri = intent.getParcelableExtra(WidgetProvider.EXTRA_FOLDER_URI);
238            final Uri folderConversationListUri =
239                    intent.getParcelableExtra(WidgetProvider.EXTRA_FOLDER_CONVERSATION_LIST_URI);
240            if (folderUri != null && folderConversationListUri != null) {
241                mFolderUri = folderUri;
242                mFolderConversationListUri = folderConversationListUri;
243            } else {
244                // This is a old intent created in version UR8 (or earlier).
245                String folderString = intent.getStringExtra(Utils.EXTRA_FOLDER);
246                //noinspection deprecation
247                Folder folder = Folder.fromString(folderString);
248                if (folder != null) {
249                    mFolderUri = folder.folderUri.fullUri;
250                    mFolderConversationListUri = folder.conversationListUri;
251                } else {
252                    mFolderUri = Uri.EMPTY;
253                    mFolderConversationListUri = Uri.EMPTY;
254                    // this will mark the widget as unconfigured
255                    BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolderType,
256                            mFolderUri, mFolderConversationListUri, mFolderDisplayName);
257                }
258            }
259
260            mWidgetConversationListItemViewBuilder = new WidgetConversationListItemViewBuilder(
261                    context);
262            mService = service;
263        }
264
265        @Override
266        public void onCreate() {
267            // Save the map between widgetId and account to preference
268            saveWidgetInformation(mContext, mAppWidgetId, mAccount, mFolderUri.toString());
269
270            // If the account of this widget has been removed, we want to update the widget to
271            // "Tap to configure" mode.
272            if (!mService.isWidgetConfigured(mContext, mAppWidgetId, mAccount)) {
273                BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolderType,
274                        mFolderUri, mFolderConversationListUri, mFolderDisplayName);
275            }
276
277            mFolderInformationShown = false;
278
279            // We want to limit the query result to 25 and don't want these queries to cause network
280            // traffic
281            // We also want this cursor to receive notifications on all changes.  Any change that
282            // the user made locally, the default policy of the UI provider is to not send
283            // notifications for.  But in this case, since the widget is not using the
284            // ConversationCursor instance that the UI is using, the widget would not be updated.
285            final Uri.Builder builder = mFolderConversationListUri.buildUpon();
286            final String maxConversations = Integer.toString(MAX_CONVERSATIONS_COUNT);
287            final Uri widgetConversationQueryUri = builder
288                    .appendQueryParameter(ConversationListQueryParameters.LIMIT, maxConversations)
289                    .appendQueryParameter(ConversationListQueryParameters.USE_NETWORK,
290                            Boolean.FALSE.toString())
291                    .appendQueryParameter(ConversationListQueryParameters.ALL_NOTIFICATIONS,
292                            Boolean.TRUE.toString()).build();
293
294            final Resources res = mContext.getResources();
295            mConversationCursorLoader = new CursorLoader(mContext, widgetConversationQueryUri,
296                    UIProvider.CONVERSATION_PROJECTION, null, null, null);
297            mConversationCursorLoader.registerListener(CONVERSATION_CURSOR_LOADER_ID, this);
298            mConversationCursorLoader.setUpdateThrottle(
299                    res.getInteger(R.integer.widget_refresh_delay_ms));
300            mConversationCursorLoader.startLoading();
301            mSendersSplitToken = res.getString(R.string.senders_split_token);
302            mElidedPaddingToken = res.getString(R.string.elided_padding_token);
303            mFolderLoader = new CursorLoader(mContext, mFolderUri, UIProvider.FOLDERS_PROJECTION,
304                    null, null, null);
305            mFolderLoader.registerListener(FOLDER_LOADER_ID, this);
306            mFolderUpdateHandler = new FolderUpdateHandler(
307                    res.getInteger(R.integer.widget_folder_refresh_delay_ms));
308            mFolderUpdateHandler.scheduleTask();
309
310            mAccountLoader = new CursorLoader(mContext, mAccount.uri,
311                    UIProvider.ACCOUNTS_PROJECTION_NO_CAPABILITIES, null, null, null);
312            mAccountLoader.registerListener(ACCOUNT_LOADER_ID, this);
313            mAccountLoader.startLoading();
314        }
315
316        @Override
317        public void onDestroy() {
318            synchronized (sWidgetLock) {
319                if (mConversationCursorLoader != null) {
320                    mConversationCursorLoader.reset();
321                    mConversationCursorLoader.unregisterListener(this);
322                    mConversationCursorLoader = null;
323                }
324
325                // The Loader should close the cursor, so just unset the reference
326                // to it here.
327                mConversationCursor = null;
328            }
329
330            if (mFolderLoader != null) {
331                mFolderLoader.reset();
332                mFolderLoader.unregisterListener(this);
333                mFolderLoader = null;
334            }
335
336            if (mAccountLoader != null) {
337                mAccountLoader.reset();
338                mAccountLoader.unregisterListener(this);
339                mAccountLoader = null;
340            }
341        }
342
343        @Override
344        public void onDataSetChanged() {
345            // We are not using this as signal to requery the cursor.  The query will be started
346            // in the following ways:
347            // 1) The Service is started and the loader is started in onCreate()
348            //       This will happen when the service is not running, and
349            //       AppWidgetManager#notifyAppWidgetViewDataChanged() is called
350            // 2) The service is running, with a previously created loader.  The loader is watching
351            //    for updates from the existing cursor.  If one is seen, the loader will load a new
352            //    cursor in the background.
353            mFolderUpdateHandler.scheduleTask();
354        }
355
356        /**
357         * Returns the number of items should be shown in the widget list.  This method also updates
358         * the boolean that indicates whether the "show more" item should be shown.
359         * @return the number of items to be displayed in the list.
360         */
361        @Override
362        public int getCount() {
363            synchronized (sWidgetLock) {
364                final int count = getConversationCount();
365                final int cursorCount = mConversationCursor != null ?
366                        mConversationCursor.getCount() : 0;
367                mShouldShowViewMore = count < cursorCount || count < mFolderCount;
368                return count + (mShouldShowViewMore ? 1 : 0);
369            }
370        }
371
372        /**
373         * Returns the number of conversations that should be shown in the widget.  This method
374         * doesn't update the boolean that indicates that the "show more" item should be included
375         * in the list.
376         * @return count
377         */
378        private int getConversationCount() {
379            synchronized (sWidgetLock) {
380                final int cursorCount = mConversationCursor != null ?
381                        mConversationCursor.getCount() : 0;
382                return Math.min(cursorCount, MAX_CONVERSATIONS_COUNT);
383            }
384        }
385
386        /**
387         * @return the {@link RemoteViews} for a specific position in the list.
388         */
389        @Override
390        public RemoteViews getViewAt(int position) {
391            synchronized (sWidgetLock) {
392                // "View more conversations" view.
393                if (mConversationCursor == null || mConversationCursor.isClosed()
394                        || (mShouldShowViewMore && position >= getConversationCount())) {
395                    return getViewMoreConversationsView();
396                }
397
398                if (!mConversationCursor.moveToPosition(position)) {
399                    // If we ever fail to move to a position, return the
400                    // "View More conversations"
401                    // view.
402                    LogUtils.e(LOG_TAG, "Failed to move to position %d in the cursor.", position);
403                    return getViewMoreConversationsView();
404                }
405
406                Conversation conversation = new Conversation(mConversationCursor);
407                // Split the senders and status from the instructions.
408                SpannableStringBuilder senderBuilder = new SpannableStringBuilder();
409
410                if (conversation.conversationInfo != null) {
411                    ArrayList<SpannableString> senders = new ArrayList<SpannableString>();
412                    SendersView.format(mContext, conversation.conversationInfo, "",
413                            MAX_SENDERS_LENGTH, senders, null, null, mAccount.name, true);
414                    senderBuilder = ellipsizeStyledSenders(senders);
415                } else {
416                    senderBuilder.append(conversation.senders);
417                    senderBuilder.setSpan(conversation.read ? getReadStyle() : getUnreadStyle(), 0,
418                            senderBuilder.length(), 0);
419                }
420                // Get styled date.
421                CharSequence date = DateUtils.getRelativeTimeSpanString(mContext,
422                        conversation.dateMs);
423
424                final int ignoreFolderType;
425                if ((mFolderType & FolderType.INBOX) != 0) {
426                    ignoreFolderType = FolderType.INBOX;
427                } else {
428                    ignoreFolderType = -1;
429                }
430
431                // Load up our remote view.
432                RemoteViews remoteViews = mWidgetConversationListItemViewBuilder.getStyledView(date,
433                        conversation, new FolderUri(mFolderUri), ignoreFolderType,
434                        senderBuilder, filterTag(conversation.subject));
435
436                // On click intent.
437                remoteViews.setOnClickFillInIntent(R.id.widget_conversation_list_item,
438                        Utils.createViewConversationIntent(mContext, conversation, mFolderUri,
439                                mAccount));
440
441                return remoteViews;
442            }
443        }
444
445        private CharacterStyle getUnreadStyle() {
446            if (mUnreadStyle == null) {
447                mUnreadStyle = new TextAppearanceSpan(mContext,
448                        R.style.SendersUnreadTextAppearance);
449            }
450            return CharacterStyle.wrap(mUnreadStyle);
451        }
452
453        private CharacterStyle getReadStyle() {
454            if (mReadStyle == null) {
455                mReadStyle = new TextAppearanceSpan(mContext, R.style.SendersReadTextAppearance);
456            }
457            return CharacterStyle.wrap(mReadStyle);
458        }
459
460        private SpannableStringBuilder ellipsizeStyledSenders(
461                ArrayList<SpannableString> styledSenders) {
462            SpannableStringBuilder builder = new SpannableStringBuilder();
463            SpannableString prevSender = null;
464            for (SpannableString sender : styledSenders) {
465                if (sender == null) {
466                    LogUtils.e(LOG_TAG, "null sender while iterating over styledSenders");
467                    continue;
468                }
469                CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
470                if (SendersView.sElidedString.equals(sender.toString())) {
471                    prevSender = sender;
472                    sender = copyStyles(spans, mElidedPaddingToken + sender + mElidedPaddingToken);
473                } else if (builder.length() > 0
474                        && (prevSender == null || !SendersView.sElidedString.equals(prevSender
475                                .toString()))) {
476                    prevSender = sender;
477                    sender = copyStyles(spans, mSendersSplitToken + sender);
478                } else {
479                    prevSender = sender;
480                }
481                builder.append(sender);
482            }
483            return builder;
484        }
485
486        private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
487            SpannableString s = new SpannableString(newText);
488            if (spans != null && spans.length > 0) {
489                s.setSpan(spans[0], 0, s.length(), 0);
490            }
491            return s;
492        }
493
494        /**
495         * @return the "View more conversations" view.
496         */
497        private RemoteViews getViewMoreConversationsView() {
498            RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
499            view.setTextViewText(
500                    R.id.loading_text, mContext.getText(R.string.view_more_conversations));
501            view.setOnClickFillInIntent(R.id.widget_loading,
502                    Utils.createViewFolderIntent(mContext, mFolderUri, mAccount));
503            return view;
504        }
505
506        @Override
507        public RemoteViews getLoadingView() {
508            RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
509            view.setTextViewText(
510                    R.id.loading_text, mContext.getText(R.string.loading_conversation));
511            return view;
512        }
513
514        @Override
515        public int getViewTypeCount() {
516            return 2;
517        }
518
519        @Override
520        public long getItemId(int position) {
521            return position;
522        }
523
524        @Override
525        public boolean hasStableIds() {
526            return false;
527        }
528
529        @Override
530        public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
531            final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
532            final RemoteViews remoteViews =
533                    new RemoteViews(mContext.getPackageName(), R.layout.widget);
534
535            if (!mService.isWidgetConfigured(mContext, mAppWidgetId, mAccount)) {
536                BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolderType,
537                        mFolderUri, mFolderConversationListUri, mFolderDisplayName);
538            }
539
540            if (loader == mFolderLoader) {
541                if (!isDataValid(data)) {
542                    return;
543                }
544
545                final int unreadCount = data.getInt(UIProvider.FOLDER_UNREAD_COUNT_COLUMN);
546                final String folderName = data.getString(UIProvider.FOLDER_NAME_COLUMN);
547                mFolderCount = data.getInt(UIProvider.FOLDER_TOTAL_COUNT_COLUMN);
548
549                if (!mFolderInformationShown && !TextUtils.isEmpty(folderName) &&
550                        !TextUtils.isEmpty(mAccount.name)) {
551                    // We want to do a full update to the widget at least once, as the widget
552                    // manager doesn't cache the state of the remote views when doing a partial
553                    // widget update. This causes the folder name to be shown as blank if the state
554                    // of the widget is restored.
555                    mService.configureValidAccountWidget(mContext, remoteViews, mAppWidgetId,
556                            mAccount, mFolderType, mFolderUri, mFolderConversationListUri,
557                            folderName);
558                    appWidgetManager.updateAppWidget(mAppWidgetId, remoteViews);
559                    mFolderInformationShown = true;
560                }
561
562                // There is no reason to overwrite a valid non-null folder name with an empty string
563                if (!TextUtils.isEmpty(folderName)) {
564                    remoteViews.setViewVisibility(R.id.widget_folder, View.VISIBLE);
565                    remoteViews.setTextViewText(R.id.widget_folder, folderName);
566                } else {
567                    LogUtils.e(LOG_TAG, "Empty folder name");
568                }
569                if (!TextUtils.isEmpty(mAccount.name)) {
570                    remoteViews.setTextViewText(R.id.widget_account_noflip, mAccount.name);
571                    remoteViews.setTextViewText(R.id.widget_account, mAccount.name);
572                }
573
574                final CharSequence unreadCountString = Utils
575                        .getUnreadMessageString(mContext.getApplicationContext(), unreadCount);
576
577                // If there are 0 unread messages, hide the unread count text view.
578                // Otherwise, show the unread count.
579                if (unreadCount == 0) {
580                    remoteViews.setViewVisibility(R.id.widget_account_noflip, View.VISIBLE);
581                    remoteViews.setViewVisibility(R.id.widget_account_unread_flipper, View.GONE);
582                } else {
583                    remoteViews.setViewVisibility(R.id.widget_account_noflip, View.GONE);
584                    remoteViews.setViewVisibility(R.id.widget_account_unread_flipper, View.VISIBLE);
585                    remoteViews.setTextViewText(R.id.widget_unread_count, unreadCountString);
586                }
587
588                appWidgetManager.partiallyUpdateAppWidget(mAppWidgetId, remoteViews);
589            } else if (loader == mConversationCursorLoader) {
590                // We want to cache the new cursor
591                synchronized (sWidgetLock) {
592                    if (!isDataValid(data)) {
593                        mConversationCursor = null;
594                    } else {
595                        mConversationCursor = data;
596                    }
597                }
598
599                appWidgetManager.notifyAppWidgetViewDataChanged(mAppWidgetId,
600                        R.id.conversation_list);
601
602                if (mConversationCursor == null || mConversationCursor.getCount() == 0) {
603                    remoteViews.setTextViewText(R.id.empty_conversation_list,
604                            mContext.getString(R.string.no_conversations));
605                    appWidgetManager.partiallyUpdateAppWidget(mAppWidgetId, remoteViews);
606                }
607            } else if (loader == mAccountLoader) {
608                BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolderType,
609                        mFolderUri, mFolderConversationListUri, mFolderDisplayName);
610            }
611        }
612
613        /**
614         * Returns a boolean indicating whether this cursor has valid data.
615         * Note: This seeks to the first position in the cursor
616         */
617        private static boolean isDataValid(Cursor cursor) {
618            return cursor != null && !cursor.isClosed() && cursor.moveToFirst();
619        }
620
621        /**
622         * If the subject contains the tag of a mailing-list (text surrounded with []), return the
623         * subject with that tag ellipsized, e.g. "[android-gmail-team] Hello" -> "[andr...] Hello"
624         */
625        private static String filterTag(String subject) {
626            String result = subject;
627            if (subject.length() > 0 && subject.charAt(0) == '[') {
628                int end = subject.indexOf(']');
629                if (end > 0) {
630                    String tag = subject.substring(1, end);
631                    result = "[" + Utils.ellipsize(tag, 7) + "]" + subject.substring(end + 1);
632                }
633            }
634
635            return result;
636        }
637
638        /**
639         * A {@link DelayedTaskHandler} to throttle folder update to a reasonable rate.
640         */
641        private class FolderUpdateHandler extends DelayedTaskHandler {
642            public FolderUpdateHandler(int refreshDelay) {
643                super(Looper.myLooper(), refreshDelay);
644            }
645
646            @Override
647            protected void performTask() {
648                // Start the loader. The cached data will be returned if present.
649                if (mFolderLoader != null) {
650                    mFolderLoader.startLoading();
651                }
652            }
653        }
654    }
655}
656