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