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