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