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