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