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