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