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