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