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