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