AbstractActivityController.java revision 7ebdfd0f7b082c1e9aad07b2820352fb58beaa3b
1/******************************************************************************* 2 * Copyright (C) 2012 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 *******************************************************************************/ 17 18package com.android.mail.ui; 19 20import android.app.ActionBar; 21import android.app.ActionBar.LayoutParams; 22import android.app.Activity; 23import android.app.AlertDialog; 24import android.app.Dialog; 25import android.app.LoaderManager; 26import android.content.ContentResolver; 27import android.content.Context; 28import android.content.CursorLoader; 29import android.content.DialogInterface; 30import android.content.Intent; 31import android.content.Loader; 32import android.database.Cursor; 33import android.net.Uri; 34import android.os.AsyncTask; 35import android.os.Bundle; 36import android.os.Handler; 37import android.text.TextUtils; 38import android.view.KeyEvent; 39import android.view.LayoutInflater; 40import android.view.Menu; 41import android.view.MenuInflater; 42import android.view.MenuItem; 43import android.view.MotionEvent; 44 45import com.android.mail.ConversationListContext; 46import com.android.mail.R; 47import com.android.mail.compose.ComposeActivity; 48import com.android.mail.providers.Account; 49import com.android.mail.providers.AccountCacheProvider; 50import com.android.mail.providers.Conversation; 51import com.android.mail.providers.Folder; 52import com.android.mail.providers.Settings; 53import com.android.mail.providers.UIProvider; 54import com.android.mail.providers.UIProvider.AutoAdvance; 55import com.android.mail.providers.UIProvider.ConversationColumns; 56import com.android.mail.providers.UIProvider.FolderCapabilities; 57import com.android.mail.utils.LogUtils; 58import com.android.mail.utils.Utils; 59import com.google.common.collect.ImmutableList; 60import com.google.common.collect.Sets; 61 62import java.util.ArrayList; 63import java.util.Arrays; 64import java.util.Collection; 65import java.util.Collections; 66import java.util.HashSet; 67import java.util.Set; 68 69 70/** 71 * This is an abstract implementation of the Activity Controller. This class 72 * knows how to respond to menu items, state changes, layout changes, etc. It 73 * weaves together the views and listeners, dispatching actions to the 74 * respective underlying classes. 75 * <p> 76 * Even though this class is abstract, it should provide default implementations 77 * for most, if not all the methods in the ActivityController interface. This 78 * makes the task of the subclasses easier: OnePaneActivityController and 79 * TwoPaneActivityController can be concise when the common functionality is in 80 * AbstractActivityController. 81 * </p> 82 * <p> 83 * In the Gmail codebase, this was called BaseActivityController 84 * </p> 85 */ 86public abstract class AbstractActivityController implements ActivityController { 87 // Keys for serialization of various information in Bundles. 88 private static final String SAVED_LIST_CONTEXT = "saved-list-context"; 89 private static final String SAVED_ACCOUNT = "saved-account"; 90 91 /** Are we on a tablet device or not. */ 92 public final boolean IS_TABLET_DEVICE; 93 94 protected Account mAccount; 95 protected Folder mFolder; 96 protected ActionBarView mActionBarView; 97 protected final RestrictedActivity mActivity; 98 protected final Context mContext; 99 protected final RecentFolderList mRecentFolderList; 100 protected ConversationListContext mConvListContext; 101 private FetchAccountFolderTask mFetchAccountFolderTask; 102 protected Conversation mCurrentConversation; 103 104 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */ 105 private SuppressNotificationReceiver mNewEmailReceiver = null; 106 107 protected Handler mHandler = new Handler(); 108 protected ConversationListFragment mConversationListFragment; 109 /** 110 * The current mode of the application. All changes in mode are initiated by 111 * the activity controller. View mode changes are propagated to classes that 112 * attach themselves as listeners of view mode changes. 113 */ 114 protected final ViewMode mViewMode; 115 protected ContentResolver mResolver; 116 protected FolderListFragment mFolderListFragment; 117 protected ConversationViewFragment mConversationViewFragment; 118 protected boolean isLoaderInitialized = false; 119 private AsyncRefreshTask mAsyncRefreshTask; 120 121 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet(); 122 protected Settings mCachedSettings; 123 private FetchSearchFolderTask mFetchSearchFolderTask; 124 private FetchInboxTask mFetchInboxTask; 125 126 protected static final String LOG_TAG = new LogUtils().getLogTag(); 127 /** Constants used to differentiate between the types of loaders. */ 128 private static final int LOADER_ACCOUNT_CURSOR = 0; 129 private static final int LOADER_ACCOUNT_SETTINGS = 1; 130 private static final int LOADER_FOLDER_CURSOR = 2; 131 private static final int LOADER_RECENT_FOLDERS = 3; 132 133 public AbstractActivityController(MailActivity activity, ViewMode viewMode) { 134 mActivity = activity; 135 mViewMode = viewMode; 136 mContext = activity.getApplicationContext(); 137 IS_TABLET_DEVICE = Utils.useTabletUI(mContext); 138 mRecentFolderList = new RecentFolderList(mContext, this); 139 } 140 141 @Override 142 public synchronized void attachConversationList(ConversationListFragment fragment) { 143 // If there is an existing fragment, unregister it 144 if (mConversationListFragment != null) { 145 mViewMode.removeListener(mConversationListFragment); 146 } 147 mConversationListFragment = fragment; 148 // If the current fragment is non-null, add it as a listener. 149 if (fragment != null) { 150 mViewMode.addListener(mConversationListFragment); 151 } 152 } 153 154 @Override 155 public synchronized void attachFolderList(FolderListFragment fragment) { 156 // If there is an existing fragment, unregister it 157 if (mFolderListFragment != null) { 158 mViewMode.removeListener(mFolderListFragment); 159 } 160 mFolderListFragment = fragment; 161 if (fragment != null) { 162 mViewMode.addListener(mFolderListFragment); 163 } 164 } 165 166 @Override 167 public void attachConversationView(ConversationViewFragment conversationViewFragment) { 168 mConversationViewFragment = conversationViewFragment; 169 } 170 171 @Override 172 public void clearSubject() { 173 // TODO(viki): Auto-generated method stub 174 } 175 176 @Override 177 public Account getCurrentAccount() { 178 return mAccount; 179 } 180 181 @Override 182 public ConversationListContext getCurrentListContext() { 183 return mConvListContext; 184 } 185 186 @Override 187 public String getHelpContext() { 188 return "Mail"; 189 } 190 191 @Override 192 public int getMode() { 193 return mViewMode.getMode(); 194 } 195 196 @Override 197 public String getUnshownSubject(String subject) { 198 // Calculate how much of the subject is shown, and return the remaining. 199 return null; 200 } 201 202 @Override 203 public void handleConversationLoadError() { 204 // TODO(viki): Auto-generated method stub 205 } 206 207 /** 208 * Initialize the action bar. This is not visible to OnePaneController and 209 * TwoPaneController so they cannot override this behavior. 210 */ 211 private void initCustomActionBarView() { 212 ActionBar actionBar = mActivity.getActionBar(); 213 mActionBarView = (ActionBarView) LayoutInflater.from(mContext).inflate( 214 R.layout.actionbar_view, null); 215 if (actionBar != null && mActionBarView != null) { 216 // Why have a different variable for the same thing? We should apply 217 // the same actions 218 // on mActionBarView instead. 219 mActionBarView.initialize(mActivity, this, mViewMode, actionBar, mRecentFolderList); 220 actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams( 221 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 222 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, 223 ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE); 224 } 225 } 226 227 /** 228 * Returns whether the conversation list fragment is visible or not. 229 * Different layouts will have their own notion on the visibility of 230 * fragments, so this method needs to be overriden. 231 * 232 * @return 233 */ 234 protected abstract boolean isConversationListVisible(); 235 236 @Override 237 public void onAccountChanged(Account account) { 238 if (!account.equals(mAccount)) { 239 mAccount = account; 240 mRecentFolderList.setCurrentAccount(account); 241 restartOptionalLoader(LOADER_RECENT_FOLDERS, null /* args */); 242 restartOptionalLoader(LOADER_ACCOUNT_SETTINGS, null /* args */); 243 mActionBarView.setAccount(mAccount); 244 mActivity.invalidateOptionsMenu(); 245 246 disableNotificationsOnAccountChange(mAccount); 247 248 // Account changed; existing folder is invalid. 249 mFolder = null; 250 fetchAccountFolderInfo(); 251 } 252 } 253 254 public void onSettingsChanged(Settings settings) { 255 mCachedSettings = settings; 256 resetActionBarIcon(); 257 } 258 259 @Override 260 public Settings getSettings() { 261 return mCachedSettings; 262 } 263 264 private void fetchAccountFolderInfo() { 265 if (mFetchAccountFolderTask != null) { 266 mFetchAccountFolderTask.cancel(true); 267 } 268 mFetchAccountFolderTask = new FetchAccountFolderTask(); 269 mFetchAccountFolderTask.execute(); 270 } 271 272 private void fetchSearchFolder(Intent intent) { 273 if (mFetchSearchFolderTask != null) { 274 mFetchSearchFolderTask.cancel(true); 275 } 276 mFetchSearchFolderTask = new FetchSearchFolderTask(intent 277 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY)); 278 mFetchSearchFolderTask.execute(); 279 } 280 281 @Override 282 public void onFolderChanged(Folder folder) { 283 if (folder != null && !folder.equals(mFolder)) { 284 setFolder(folder); 285 mConvListContext = ConversationListContext.forFolder(mContext, mAccount, mFolder); 286 showConversationList(mConvListContext); 287 288 // Add the folder that we were viewing to the recent folders list. 289 // TODO: this may need to be fine tuned. If this is the signal that is indicating that 290 // the list is shown to the user, this could fire in one pane if the user goes directly 291 // to a conversation 292 updateRecentFolderList(); 293 } 294 } 295 296 private void updateRecentFolderList() { 297 mRecentFolderList.setCurrentAccount(mAccount); 298 mRecentFolderList.touchFolder(mFolder); 299 } 300 301 // TODO(mindyp): set this up to store a copy of the folder locally 302 // as soon as we realize we haven't gotten the inbox folder yet. 303 public void loadInbox() { 304 if (mFetchInboxTask != null) { 305 mFetchInboxTask.cancel(true); 306 } 307 mFetchInboxTask = new FetchInboxTask(); 308 mFetchInboxTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 309 } 310 311 /** Set the current folder */ 312 private void setFolder(Folder folder) { 313 // Start watching folder for sync status. 314 if (folder != null && !folder.equals(mFolder)) { 315 mActionBarView.setRefreshInProgress(false); 316 mFolder = folder; 317 mActionBarView.setFolder(mFolder); 318 mActivity.getLoaderManager().restartLoader(LOADER_FOLDER_CURSOR, null, this); 319 } else if (folder == null) { 320 LogUtils.wtf(LOG_TAG, "Folder in setFolder is null"); 321 } 322 } 323 324 @Override 325 public void onActivityResult(int requestCode, int resultCode, Intent data) { 326 // TODO(viki): Auto-generated method stub 327 } 328 329 @Override 330 public void onConversationListVisibilityChanged(boolean visible) { 331 // TODO(viki): Auto-generated method stub 332 } 333 334 /** 335 * By default, doing nothing is right. A two-pane controller will need to 336 * override this. 337 */ 338 @Override 339 public void onConversationVisibilityChanged(boolean visible) { 340 // Do nothing. 341 return; 342 } 343 344 @Override 345 public boolean onCreate(Bundle savedState) { 346 // Initialize the action bar view. 347 initCustomActionBarView(); 348 // Allow shortcut keys to function for the ActionBar and menus. 349 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT); 350 mResolver = mActivity.getContentResolver(); 351 352 mNewEmailReceiver = new SuppressNotificationReceiver(); 353 354 // All the individual UI components listen for ViewMode changes. This 355 // simplifies the amount of logic in the AbstractActivityController, but increases the 356 // possibility of timing-related bugs. 357 mViewMode.addListener(this); 358 assert (mActionBarView != null); 359 mViewMode.addListener(mActionBarView); 360 361 restoreState(savedState); 362 return true; 363 } 364 365 @Override 366 public Dialog onCreateDialog(int id, Bundle bundle) { 367 // TODO(viki): Auto-generated method stub 368 return null; 369 } 370 371 @Override 372 public boolean onCreateOptionsMenu(Menu menu) { 373 MenuInflater inflater = mActivity.getMenuInflater(); 374 inflater.inflate(mActionBarView.getOptionsMenuId(), menu); 375 mActionBarView.onCreateOptionsMenu(menu); 376 return true; 377 } 378 379 @Override 380 public boolean onKeyDown(int keyCode, KeyEvent event) { 381 // TODO(viki): Auto-generated method stub 382 return false; 383 } 384 385 @Override 386 public boolean onOptionsItemSelected(MenuItem item) { 387 final int id = item.getItemId(); 388 boolean handled = true; 389 switch (id) { 390 case android.R.id.home: 391 onUpPressed(); 392 break; 393 case R.id.compose: 394 ComposeActivity.compose(mActivity.getActivityContext(), mAccount); 395 break; 396 case R.id.show_all_folders: 397 showFolderList(); 398 break; 399 case R.id.refresh: 400 requestFolderRefresh(); 401 break; 402 case R.id.settings: 403 Utils.showSettings(mActivity.getActivityContext(), mAccount); 404 break; 405 case R.id.help_info_menu_item: 406 // TODO: enable context sensitive help 407 Utils.showHelp(mActivity.getActivityContext(), mAccount.helpIntentUri, null); 408 break; 409 case R.id.feedback_menu_item: 410 Utils.sendFeedback(mActivity.getActivityContext(), mAccount); 411 break; 412 default: 413 handled = false; 414 break; 415 } 416 return handled; 417 } 418 419 /** 420 * Return the auto advance setting for the current account. 421 * @param activity 422 * @return the autoadvance setting, a constant from {@link AutoAdvance} 423 */ 424 static int getAutoAdvanceSetting(RestrictedActivity activity) { 425 final Settings settings = activity.getSettings(); 426 // TODO(mindyp): if this isn't set, then show the dialog telling the user to set it. 427 // Remove defaulting to AutoAdvance.LIST. 428 final int autoAdvance = (settings != null) ? 429 (settings.autoAdvance == AutoAdvance.UNSET ? 430 AutoAdvance.LIST : settings.autoAdvance) 431 : AutoAdvance.LIST; 432 return autoAdvance; 433 } 434 435 /** 436 * Implements folder changes. This class is a listener because folder changes need to be 437 * performed <b>after</b> the ConversationListFragment has finished animating away the 438 * removal of the conversation. 439 * 440 */ 441 private class FolderChangeListener implements ActionCompleteListener { 442 private final String mFolderChangeList; 443 private final boolean mDestructiveChange; 444 445 public FolderChangeListener(String changeList, boolean destructive) { 446 mFolderChangeList = changeList; 447 mDestructiveChange = destructive; 448 } 449 450 @Override 451 public void onActionComplete() { 452 // Only show undo if this was a destructive folder change. 453 if (mDestructiveChange) { 454 mConversationListFragment.onUndoAvailable(new UndoOperation(1, R.id.change_folder)); 455 } 456 // Update the folders for this conversation 457 Conversation.updateString(mContext, Collections.singletonList(mCurrentConversation), 458 ConversationColumns.FOLDER_LIST, mFolderChangeList); 459 mConversationListFragment.requestListRefresh(); 460 } 461 } 462 463 /** 464 * Update the specified column name in conversation for a boolean value. 465 * @param columnName 466 * @param value 467 */ 468 protected void updateCurrentConversation(String columnName, boolean value) { 469 Conversation.updateBoolean(mContext, ImmutableList.of(mCurrentConversation), columnName, 470 value); 471 mConversationListFragment.requestListRefresh(); 472 } 473 474 /** 475 * Update the specified column name in conversation for an integer value. 476 * @param columnName 477 * @param value 478 */ 479 protected void updateCurrentConversation(String columnName, int value) { 480 Conversation.updateInt(mContext, ImmutableList.of(mCurrentConversation), columnName, value); 481 mConversationListFragment.requestListRefresh(); 482 } 483 484 private void requestFolderRefresh() { 485 if (mFolder != null) { 486 if (mAsyncRefreshTask != null) { 487 mAsyncRefreshTask.cancel(true); 488 } 489 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder); 490 mAsyncRefreshTask.execute(); 491 } 492 } 493 494 /** 495 * Confirm (based on user's settings) and delete a conversation from the conversation list and 496 * from the database. 497 * @param showDialog 498 * @param confirmResource 499 * @param listener 500 */ 501 protected void confirmAndDelete(boolean showDialog, int confirmResource, 502 final ActionCompleteListener listener) { 503 final ArrayList<Conversation> single = new ArrayList<Conversation>(); 504 single.add(mCurrentConversation); 505 if (showDialog) { 506 final AlertDialog.OnClickListener onClick = new AlertDialog.OnClickListener() { 507 @Override 508 public void onClick(DialogInterface dialog, int which) { 509 requestDelete(listener); 510 } 511 }; 512 final CharSequence message = Utils.formatPlural(mContext, confirmResource, 1); 513 new AlertDialog.Builder(mActivity.getActivityContext()).setMessage(message) 514 .setPositiveButton(R.string.ok, onClick) 515 .setNegativeButton(R.string.cancel, null) 516 .create().show(); 517 } else { 518 requestDelete(listener); 519 } 520 } 521 522 523 protected abstract void requestDelete(ActionCompleteListener listener); 524 525 @Override 526 public void onCommit(String uris) { 527 // Get currently active folder info and compare it to the list 528 // these conversations have been given; if they no longer contain 529 // the selected folder, delete them from the list. 530 HashSet<String> folderUris = new HashSet<String>(); 531 if (!TextUtils.isEmpty(uris)) { 532 folderUris.addAll(Arrays.asList(uris.split(","))); 533 } 534 final boolean destructiveChange = !folderUris.contains(mFolder.uri); 535 FolderChangeListener listener = new FolderChangeListener(uris, destructiveChange); 536 if (destructiveChange) { 537 mCurrentConversation.localDeleteOnUpdate = true; 538 mConversationListFragment.requestDelete(listener); 539 } else { 540 listener.onActionComplete(); 541 } 542 } 543 544 @Override 545 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) { 546 // TODO(viki): Auto-generated method stub 547 548 } 549 550 @Override 551 public boolean onPrepareOptionsMenu(Menu menu) { 552 mActionBarView.onPrepareOptionsMenu(menu); 553 return true; 554 } 555 556 @Override 557 public void onPause() { 558 isLoaderInitialized = false; 559 560 enableNotifications(); 561 } 562 563 @Override 564 public void onResume() { 565 // Register the receiver that will prevent the status receiver from 566 // displaying its notification icon as long as we're running. 567 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder 568 // that the notification was received for. 569 disableNotifications(); 570 571 if (mActionBarView != null) { 572 mActionBarView.onResume(); 573 } 574 575 } 576 577 @Override 578 public void onSaveInstanceState(Bundle outState) { 579 if (mAccount != null) { 580 LogUtils.d(LOG_TAG, "Saving the account now"); 581 outState.putParcelable(SAVED_ACCOUNT, mAccount); 582 } 583 if (mConvListContext != null) { 584 outState.putBundle(SAVED_LIST_CONTEXT, mConvListContext.toBundle()); 585 } 586 } 587 588 @Override 589 public void onSearchRequested(String query) { 590 Intent intent = new Intent(); 591 intent.setAction(Intent.ACTION_SEARCH); 592 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query); 593 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount); 594 intent.setComponent(mActivity.getComponentName()); 595 mActivity.startActivity(intent); 596 } 597 598 @Override 599 public void onStartDragMode() { 600 // TODO(viki): Auto-generated method stub 601 } 602 603 @Override 604 public void onStop() { 605 // TODO(viki): Auto-generated method stub 606 } 607 608 @Override 609 public void onStopDragMode() { 610 // TODO(viki): Auto-generated method stub 611 } 612 613 /** 614 * {@inheritDoc} Subclasses must override this to listen to mode changes 615 * from the ViewMode. Subclasses <b>must</b> call the parent's 616 * onViewModeChanged since the parent will handle common state changes. 617 */ 618 @Override 619 public void onViewModeChanged(int newMode) { 620 // Perform any mode specific work here. 621 // reset the action bar icon based on the mode. Why don't the individual 622 // controllers do 623 // this themselves? 624 625 // In conversation list mode, clean up the conversation. 626 if (newMode == ViewMode.CONVERSATION_LIST) { 627 // Clean up the conversation here. 628 } 629 630 // We don't want to invalidate the options menu when switching to 631 // conversation 632 // mode, as it will happen when the conversation finishes loading. 633 if (newMode != ViewMode.CONVERSATION) { 634 mActivity.invalidateOptionsMenu(); 635 } 636 } 637 638 @Override 639 public void onWindowFocusChanged(boolean hasFocus) { 640 // TODO(viki): Auto-generated method stub 641 } 642 643 /** 644 * @param savedState 645 */ 646 protected void restoreListContext(Bundle savedState) { 647 Bundle listContextBundle = savedState.getBundle(SAVED_LIST_CONTEXT); 648 if (listContextBundle != null) { 649 mConvListContext = ConversationListContext.forBundle(listContextBundle); 650 mFolder = mConvListContext.folder; 651 } 652 } 653 654 /** 655 * Restore the state from the previous bundle. Subclasses should call this 656 * method from the parent class, since it performs important UI 657 * initialization. 658 * 659 * @param savedState 660 */ 661 protected void restoreState(Bundle savedState) { 662 final Intent intent = mActivity.getIntent(); 663 if (savedState != null) { 664 restoreListContext(savedState); 665 mAccount = savedState.getParcelable(SAVED_ACCOUNT); 666 mActionBarView.setAccount(mAccount); 667 restartOptionalLoader(LOADER_ACCOUNT_SETTINGS, null /* args */); 668 } else if (intent != null) { 669 if (Intent.ACTION_VIEW.equals(intent.getAction())) { 670 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) { 671 mAccount = ((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT)); 672 mActionBarView.setAccount(mAccount); 673 restartOptionalLoader(LOADER_ACCOUNT_SETTINGS, null /* args */); 674 mActivity.invalidateOptionsMenu(); 675 } 676 if (intent.hasExtra(Utils.EXTRA_FOLDER)) { 677 // Open the folder. 678 LogUtils.d(LOG_TAG, "SHOW THE FOLDER at %s", 679 intent.getParcelableExtra(Utils.EXTRA_FOLDER)); 680 onFolderChanged((Folder) intent.getParcelableExtra(Utils.EXTRA_FOLDER)); 681 } 682 if (intent.hasExtra(Utils.EXTRA_CONVERSATION)) { 683 // Open the conversation. 684 LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s", 685 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION)); 686 setCurrentConversation((Conversation) intent 687 .getParcelableExtra(Utils.EXTRA_CONVERSATION)); 688 showConversation(this.mCurrentConversation); 689 } 690 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) { 691 mViewMode.enterSearchResultsListMode(); 692 mAccount = ((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT)); 693 mActionBarView.setAccount(mAccount); 694 fetchSearchFolder(intent); 695 } 696 } 697 // Create the accounts loader; this loads the account switch spinner. 698 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this); 699 } 700 701 @Override 702 public void setSubject(String subject) { 703 // Do something useful with the subject. This requires changing the 704 // conversation view's subject text. 705 } 706 707 /** 708 * Children can override this method, but they must call super.showConversation(). 709 * {@inheritDoc} 710 */ 711 @Override 712 public void showConversation(Conversation conversation) { 713 } 714 715 @Override 716 public void onConversationSelected(Conversation conversation) { 717 setCurrentConversation(conversation); 718 showConversation(mCurrentConversation); 719 if (mConvListContext != null && mConvListContext.isSearchResult()) { 720 mViewMode.enterSearchResultsConversationMode(); 721 } else { 722 mViewMode.enterConversationMode(); 723 } 724 } 725 726 public void setCurrentConversation(Conversation conversation) { 727 mCurrentConversation = conversation; 728 } 729 730 /** 731 * {@inheritDoc} 732 */ 733 @Override 734 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 735 // Create a loader to listen in on account changes. 736 switch (id) { 737 case LOADER_ACCOUNT_CURSOR: 738 return new CursorLoader(mContext, AccountCacheProvider.getAccountsUri(), 739 UIProvider.ACCOUNTS_PROJECTION, null, null, null); 740 case LOADER_FOLDER_CURSOR: 741 return new CursorLoader(mContext, mFolder.uri, 742 UIProvider.FOLDERS_PROJECTION, null, null, null); 743 case LOADER_ACCOUNT_SETTINGS: 744 if (mAccount.settingsQueryUri != null) { 745 return new CursorLoader(mContext, mAccount.settingsQueryUri, 746 UIProvider.SETTINGS_PROJECTION, null, null, null); 747 } 748 break; 749 case LOADER_RECENT_FOLDERS: 750 if (mAccount.recentFolderListUri != null) { 751 return new CursorLoader(mContext, mAccount.recentFolderListUri, 752 UIProvider.FOLDERS_PROJECTION, null, null, null); 753 } 754 break; 755 default: 756 LogUtils.wtf(LOG_TAG, "Loader returned unexpected id: " + id); 757 } 758 return null; 759 } 760 761 /** 762 * {@link LoaderManager} currently has a bug in 763 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)} 764 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around 765 * this bug by destroying any loaders that may have been created as null (essentially because 766 * they are optional loads, and may not apply to a particular account). 767 * <p> 768 * A simple null check before restarting a loader will not work, because that would not 769 * give the controller a chance to invalidate UI corresponding the prior loader result. 770 * 771 * @param id loader ID to safely restart 772 * @param args arguments to pass to the restarted loader 773 */ 774 private void restartOptionalLoader(int id, Bundle args) { 775 final LoaderManager lm = mActivity.getLoaderManager(); 776 lm.destroyLoader(id); 777 lm.restartLoader(id, args, this); 778 } 779 780 private boolean accountsUpdated(Cursor accountCursor) { 781 // Check to see if the current account hasn't been set, or the account cursor is empty 782 if (mAccount == null || !accountCursor.moveToFirst()) { 783 return true; 784 } 785 786 // Check to see if the number of accounts are different, from the number we saw on the last 787 // updated 788 if (mCurrentAccountUris.size() != accountCursor.getCount()) { 789 return true; 790 } 791 792 // Check to see if the account list is different or if the current account is not found in 793 // the cursor. 794 boolean foundCurrentAccount = false; 795 do { 796 final Uri accountUri = 797 Uri.parse(accountCursor.getString(UIProvider.ACCOUNT_URI_COLUMN)); 798 if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) { 799 foundCurrentAccount = true; 800 } 801 802 if (!mCurrentAccountUris.contains(accountUri)) { 803 return true; 804 } 805 } while (accountCursor.moveToNext()); 806 807 // As long as we found the current account, the list hasn't been updated 808 return !foundCurrentAccount; 809 } 810 811 /** 812 * Update the accounts on the device. This currently loads the first account 813 * in the list. 814 * 815 * @param loader 816 * @param accounts cursor into the AccountCache 817 * @return true if the update was successful, false otherwise 818 */ 819 private boolean updateAccounts(Loader<Cursor> loader, Cursor accounts) { 820 if (accounts == null || !accounts.moveToFirst()) { 821 return false; 822 } 823 824 final Account[] allAccounts = Account.getAllAccounts(accounts); 825 826 // Save the uris for the accounts 827 mCurrentAccountUris.clear(); 828 for (Account account : allAccounts) { 829 mCurrentAccountUris.add(account.uri); 830 } 831 832 final Account newAccount; 833 if (mAccount == null || !mCurrentAccountUris.contains(mAccount.uri)) { 834 accounts.moveToFirst(); 835 newAccount = new Account(accounts); 836 } else { 837 newAccount = mAccount; 838 } 839 // Only bother updating the account/folder if the new account is different than the 840 // existing one 841 final boolean refetchFolderInfo = !newAccount.equals(mAccount); 842 onAccountChanged(newAccount); 843 844 if(refetchFolderInfo) { 845 fetchAccountFolderInfo(); 846 } 847 848 mActionBarView.setAccounts(allAccounts); 849 return (allAccounts.length > 0); 850 } 851 852 private void disableNotifications() { 853 mNewEmailReceiver.activate(mContext, this); 854 } 855 856 private void enableNotifications() { 857 mNewEmailReceiver.deactivate(); 858 } 859 860 private void disableNotificationsOnAccountChange(Account account) { 861 // If the new mail suppression receiver is activated for a different account, we want to 862 // activate it for the new account. 863 if (mNewEmailReceiver.activated() && 864 !mNewEmailReceiver.notificationsDisabledForAccount(account)) { 865 // Deactivate the current receiver, otherwise multiple receivers may be registered. 866 mNewEmailReceiver.deactivate(); 867 mNewEmailReceiver.activate(mContext, this); 868 } 869 } 870 871 /** 872 * {@inheritDoc} 873 */ 874 @Override 875 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 876 // We want to reinitialize only if we haven't ever been initialized, or 877 // if the current account has vanished. 878 if (data == null) { 879 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId()); 880 } 881 switch (loader.getId()) { 882 case LOADER_ACCOUNT_CURSOR: 883 final boolean accountListUpdated = accountsUpdated(data); 884 if (!isLoaderInitialized || accountListUpdated) { 885 isLoaderInitialized = updateAccounts(loader, data); 886 } 887 break; 888 case LOADER_FOLDER_CURSOR: 889 // Check status of the cursor. 890 data.moveToFirst(); 891 Folder folder = new Folder(data); 892 if (folder.isSyncInProgress()) { 893 mActionBarView.onRefreshStarted(); 894 } else { 895 // Stop the spinner here. 896 mActionBarView.onRefreshStopped(folder.lastSyncResult); 897 } 898 if (mConversationListFragment != null) { 899 mConversationListFragment.onFolderUpdated(folder); 900 } 901 LogUtils.v(LOG_TAG, "FOLDER STATUS = " + folder.syncStatus); 902 break; 903 case LOADER_ACCOUNT_SETTINGS: 904 data.moveToFirst(); 905 onSettingsChanged(new Settings(data)); 906 break; 907 case LOADER_RECENT_FOLDERS: 908 mRecentFolderList.loadFromUiProvider(data); 909 break; 910 } 911 } 912 913 /** 914 * {@inheritDoc} 915 */ 916 @Override 917 public void onLoaderReset(Loader<Cursor> loader) { 918 switch (loader.getId()) { 919 case LOADER_ACCOUNT_SETTINGS: 920 onSettingsChanged(null); 921 break; 922 } 923 } 924 925 @Override 926 public void onTouchEvent(MotionEvent event) { 927 if (event.getAction() == MotionEvent.ACTION_DOWN) { 928 int mode = mViewMode.getMode(); 929 if (mode == ViewMode.CONVERSATION_LIST) { 930 mConversationListFragment.onTouchEvent(event); 931 } else if (mode == ViewMode.CONVERSATION) { 932 mConversationViewFragment.onTouchEvent(event); 933 } 934 } 935 } 936 937 private class FetchInboxTask extends AsyncTask<Void, Void, ConversationListContext> { 938 @Override 939 public ConversationListContext doInBackground(Void... params) { 940 // Gets the default inbox since there is no context. 941 return ConversationListContext.forFolder(mActivity.getActivityContext(), mAccount, 942 (Folder) null); 943 } 944 945 @Override 946 public void onPostExecute(ConversationListContext result) { 947 mConvListContext = result; 948 setFolder(mConvListContext.folder); 949 if (mFolderListFragment != null) { 950 mFolderListFragment.selectFolder(mConvListContext.folder); 951 } 952 showConversationList(mConvListContext); 953 954 // Add the folder that we were viewing to the recent folders list. 955 updateRecentFolderList(); 956 } 957 } 958 959 private class FetchAccountFolderTask extends AsyncTask<Void, Void, ConversationListContext> { 960 @Override 961 public ConversationListContext doInBackground(Void... params) { 962 return ConversationListContext.forFolder(mContext, mAccount, mFolder); 963 } 964 965 @Override 966 public void onPostExecute(ConversationListContext result) { 967 mConvListContext = result; 968 setFolder(mConvListContext.folder); 969 if (mFolderListFragment != null) { 970 mFolderListFragment.selectFolder(mConvListContext.folder); 971 } 972 showConversationList(mConvListContext); 973 mFetchAccountFolderTask = null; 974 975 // Add the folder that we were viewing to the recent folders list. 976 updateRecentFolderList(); 977 } 978 } 979 980 private class FetchSearchFolderTask extends AsyncTask<Void, Void, Folder> { 981 String mQuery; 982 public FetchSearchFolderTask(String query) { 983 mQuery = query; 984 } 985 986 @Override 987 public Folder doInBackground(Void... params) { 988 Folder searchFolder = Folder.forSearchResults(mAccount, mQuery, 989 mActivity.getActivityContext()); 990 return searchFolder; 991 } 992 993 @Override 994 public void onPostExecute(Folder folder) { 995 setFolder(folder); 996 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, mQuery); 997 showConversationList(mConvListContext); 998 mActivity.invalidateOptionsMenu(); 999 } 1000 } 1001 1002 protected abstract class DestructiveActionListener implements ActionCompleteListener { 1003 protected final int mAction; 1004 1005 /** 1006 * Create a listener object. action is one of four constants: R.id.y_button (archive), 1007 * R.id.delete , R.id.mute, and R.id.report_spam. 1008 * @param action 1009 */ 1010 public DestructiveActionListener(int action) { 1011 mAction = action; 1012 } 1013 1014 public void performConversationAction(Collection<Conversation> single) { 1015 switch (mAction) { 1016 case R.id.y_button: 1017 LogUtils.d(LOG_TAG, "Archiving conversation " + mCurrentConversation); 1018 Conversation.archive(mContext, single); 1019 break; 1020 case R.id.delete: 1021 LogUtils.d(LOG_TAG, "Deleting conversation " + mCurrentConversation); 1022 Conversation.delete(mContext, single); 1023 break; 1024 case R.id.mute: 1025 LogUtils.d(LOG_TAG, "Muting conversation " + mCurrentConversation); 1026 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) 1027 mCurrentConversation.localDeleteOnUpdate = true; 1028 Conversation.mute(mContext, single); 1029 break; 1030 case R.id.report_spam: 1031 LogUtils.d(LOG_TAG, "reporting spam conversation " + mCurrentConversation); 1032 Conversation.reportSpam(mContext, single); 1033 break; 1034 } 1035 } 1036 1037 public Conversation getNextConversation() { 1038 Conversation next = null; 1039 int pref = getAutoAdvanceSetting(mActivity); 1040 Cursor c = mConversationListFragment.getConversationListCursor(); 1041 if (c != null) { 1042 c.moveToPosition(mCurrentConversation.position); 1043 } 1044 switch (pref) { 1045 case AutoAdvance.NEWER: 1046 if (c.moveToPrevious()) { 1047 next = new Conversation(c); 1048 } 1049 break; 1050 case AutoAdvance.OLDER: 1051 if (c.moveToNext()) { 1052 next = new Conversation(c); 1053 } 1054 break; 1055 } 1056 return next; 1057 } 1058 1059 @Override 1060 public abstract void onActionComplete(); 1061 } 1062} 1063