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