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