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