1 /* 2 * Copyright (C) 2011 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 17 package com.android.email.activity; 18 19 import android.app.Activity; 20 import android.app.Fragment; 21 import android.app.FragmentManager; 22 import android.app.FragmentTransaction; 23 import android.os.Bundle; 24 import android.util.Log; 25 import android.view.Menu; 26 import android.view.MenuInflater; 27 import android.view.MenuItem; 28 29 import com.android.email.Email; 30 import com.android.email.FolderProperties; 31 import com.android.email.MessageListContext; 32 import com.android.email.Preferences; 33 import com.android.email.R; 34 import com.android.email.RefreshManager; 35 import com.android.email.RequireManualSyncDialog; 36 import com.android.email.activity.setup.AccountSettings; 37 import com.android.email.activity.setup.MailboxSettings; 38 import com.android.emailcommon.Logging; 39 import com.android.emailcommon.provider.Account; 40 import com.android.emailcommon.provider.EmailContent.Message; 41 import com.android.emailcommon.provider.HostAuth; 42 import com.android.emailcommon.provider.Mailbox; 43 import com.android.emailcommon.utility.EmailAsyncTask; 44 import com.android.emailcommon.utility.Utility; 45 import com.google.common.base.Objects; 46 import com.google.common.base.Preconditions; 47 48 import java.util.LinkedList; 49 import java.util.List; 50 51 /** 52 * Base class for the UI controller. 53 */ 54 abstract class UIControllerBase implements MailboxListFragment.Callback, 55 MessageListFragment.Callback, MessageViewFragment.Callback { 56 static final boolean DEBUG_FRAGMENTS = false; // DO NOT SUBMIT WITH TRUE 57 58 static final String KEY_LIST_CONTEXT = "UIControllerBase.listContext"; 59 60 /** The owner activity */ 61 final EmailActivity mActivity; 62 final FragmentManager mFragmentManager; 63 64 protected final ActionBarController mActionBarController; 65 66 private MessageOrderManager mOrderManager; 67 private final MessageOrderManagerCallback mMessageOrderManagerCallback = 68 new MessageOrderManagerCallback(); 69 70 final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); 71 72 final RefreshManager mRefreshManager; 73 74 /** 75 * Fragments that are installed. 76 * 77 * A fragment is installed in {@link Fragment#onActivityCreated} and uninstalled in 78 * {@link Fragment#onDestroyView}, using {@link FragmentInstallable} callbacks. 79 * 80 * This means fragments in the back stack are *not* installed. 81 * 82 * We set callbacks to fragments only when they are installed. 83 * 84 * @see FragmentInstallable 85 */ 86 private MailboxListFragment mMailboxListFragment; 87 private MessageListFragment mMessageListFragment; 88 private MessageViewFragment mMessageViewFragment; 89 90 /** 91 * To avoid double-deleting a fragment (which will cause a runtime exception), 92 * we put a fragment in this list when we {@link FragmentTransaction#remove(Fragment)} it, 93 * and remove from the list when we actually uninstall it. 94 */ 95 private final List<Fragment> mRemovedFragments = new LinkedList<Fragment>(); 96 97 /** 98 * The NfcHandler implements Near Field Communication sharing features 99 * whenever the activity is in the foreground. 100 */ 101 private NfcHandler mNfcHandler; 102 103 /** 104 * The active context for the current MessageList. 105 * In some UI layouts such as the one-pane view, the message list may not be visible, but is 106 * on the backstack. This list context will still be accessible in those cases. 107 * 108 * Should be set using {@link #setListContext(MessageListContext)}. 109 */ 110 protected MessageListContext mListContext; 111 112 private class RefreshListener implements RefreshManager.Listener { 113 private MenuItem mRefreshIcon; 114 115 @Override 116 public void onMessagingError(final long accountId, long mailboxId, final String message) { 117 updateRefreshIcon(); 118 } 119 120 @Override 121 public void onRefreshStatusChanged(long accountId, long mailboxId) { 122 updateRefreshIcon(); 123 } 124 125 void setRefreshIcon(MenuItem icon) { 126 mRefreshIcon = icon; 127 updateRefreshIcon(); 128 } 129 130 private void updateRefreshIcon() { 131 if (mRefreshIcon == null) { 132 return; 133 } 134 135 if (isRefreshInProgress()) { 136 mRefreshIcon.setActionView(R.layout.action_bar_indeterminate_progress); 137 } else { 138 mRefreshIcon.setActionView(null); 139 } 140 } 141 }; 142 143 private final RefreshListener mRefreshListener = new RefreshListener(); 144 145 public UIControllerBase(EmailActivity activity) { 146 mActivity = activity; 147 mFragmentManager = activity.getFragmentManager(); 148 mRefreshManager = RefreshManager.getInstance(mActivity); 149 mActionBarController = createActionBarController(activity); 150 if (DEBUG_FRAGMENTS) { 151 FragmentManager.enableDebugLogging(true); 152 } 153 } 154 155 /** 156 * Called by the base class to let a subclass create an {@link ActionBarController}. 157 */ 158 protected abstract ActionBarController createActionBarController(Activity activity); 159 160 /** @return the layout ID for the activity. */ 161 public abstract int getLayoutId(); 162 163 /** 164 * Must be called just after the activity sets up the content view. Used to initialize views. 165 * 166 * (Due to the complexity regarding class/activity initialization order, we can't do this in 167 * the constructor.) 168 */ 169 public void onActivityViewReady() { 170 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 171 Log.d(Logging.LOG_TAG, this + " onActivityViewReady"); 172 } 173 } 174 175 /** 176 * Called at the end of {@link EmailActivity#onCreate}. 177 */ 178 public void onActivityCreated() { 179 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 180 Log.d(Logging.LOG_TAG, this + " onActivityCreated"); 181 } 182 mRefreshManager.registerListener(mRefreshListener); 183 mActionBarController.onActivityCreated(); 184 mNfcHandler = NfcHandler.register(this, mActivity); 185 } 186 187 /** 188 * Handles the {@link android.app.Activity#onStart} callback. 189 */ 190 public void onActivityStart() { 191 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 192 Log.d(Logging.LOG_TAG, this + " onActivityStart"); 193 } 194 if (isMessageViewInstalled()) { 195 updateMessageOrderManager(); 196 } 197 } 198 199 /** 200 * Handles the {@link android.app.Activity#onResume} callback. 201 */ 202 public void onActivityResume() { 203 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 204 Log.d(Logging.LOG_TAG, this + " onActivityResume"); 205 } 206 refreshActionBar(); 207 if (mNfcHandler != null) { 208 mNfcHandler.onAccountChanged(); // workaround for email not set on initial load 209 } 210 long accountId = getUIAccountId(); 211 Preferences.getPreferences(mActivity).setLastUsedAccountId(accountId); 212 showAccountSpecificWarning(accountId); 213 } 214 215 /** 216 * Handles the {@link android.app.Activity#onPause} callback. 217 */ 218 public void onActivityPause() { 219 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 220 Log.d(Logging.LOG_TAG, this + " onActivityPause"); 221 } 222 } 223 224 /** 225 * Handles the {@link android.app.Activity#onStop} callback. 226 */ 227 public void onActivityStop() { 228 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 229 Log.d(Logging.LOG_TAG, this + " onActivityStop"); 230 } 231 stopMessageOrderManager(); 232 } 233 234 /** 235 * Handles the {@link android.app.Activity#onDestroy} callback. 236 */ 237 public void onActivityDestroy() { 238 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 239 Log.d(Logging.LOG_TAG, this + " onActivityDestroy"); 240 } 241 mActionBarController.onActivityDestroy(); 242 mRefreshManager.unregisterListener(mRefreshListener); 243 mTaskTracker.cancellAllInterrupt(); 244 } 245 246 /** 247 * Handles the {@link android.app.Activity#onSaveInstanceState} callback. 248 */ 249 public void onSaveInstanceState(Bundle outState) { 250 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 251 Log.d(Logging.LOG_TAG, this + " onSaveInstanceState"); 252 } 253 mActionBarController.onSaveInstanceState(outState); 254 outState.putParcelable(KEY_LIST_CONTEXT, mListContext); 255 } 256 257 /** 258 * Handles the {@link android.app.Activity#onRestoreInstanceState} callback. 259 */ 260 public void onRestoreInstanceState(Bundle savedInstanceState) { 261 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 262 Log.d(Logging.LOG_TAG, this + " restoreInstanceState"); 263 } 264 mActionBarController.onRestoreInstanceState(savedInstanceState); 265 mListContext = savedInstanceState.getParcelable(KEY_LIST_CONTEXT); 266 } 267 268 // MessageViewFragment$Callback 269 @Override 270 public void onMessageSetUnread() { 271 doAutoAdvance(); 272 } 273 274 // MessageViewFragment$Callback 275 @Override 276 public void onMessageNotExists() { 277 doAutoAdvance(); 278 } 279 280 // MessageViewFragment$Callback 281 @Override 282 public void onRespondedToInvite(int response) { 283 doAutoAdvance(); 284 } 285 286 // MessageViewFragment$Callback 287 @Override 288 public void onBeforeMessageGone() { 289 doAutoAdvance(); 290 } 291 292 /** 293 * Install a fragment. Must be caleld from the host activity's 294 * {@link FragmentInstallable#onInstallFragment}. 295 */ 296 public final void onInstallFragment(Fragment fragment) { 297 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 298 Log.d(Logging.LOG_TAG, this + " onInstallFragment fragment=" + fragment); 299 } 300 if (fragment instanceof MailboxListFragment) { 301 installMailboxListFragment((MailboxListFragment) fragment); 302 } else if (fragment instanceof MessageListFragment) { 303 installMessageListFragment((MessageListFragment) fragment); 304 } else if (fragment instanceof MessageViewFragment) { 305 installMessageViewFragment((MessageViewFragment) fragment); 306 } else { 307 throw new IllegalArgumentException("Tried to install unknown fragment"); 308 } 309 } 310 311 /** Install fragment */ 312 protected void installMailboxListFragment(MailboxListFragment fragment) { 313 mMailboxListFragment = fragment; 314 mMailboxListFragment.setCallback(this); 315 316 // TODO: consolidate this refresh with the one that the Fragment itself does. since 317 // the fragment calls setHasOptionsMenu(true) - it invalidates when it gets attached. 318 // However the timing is slightly different and leads to a delay in update if this isn't 319 // here - investigate why. same for the other installs. 320 refreshActionBar(); 321 } 322 323 /** Install fragment */ 324 protected void installMessageListFragment(MessageListFragment fragment) { 325 mMessageListFragment = fragment; 326 mMessageListFragment.setCallback(this); 327 refreshActionBar(); 328 } 329 330 /** Install fragment */ 331 protected void installMessageViewFragment(MessageViewFragment fragment) { 332 mMessageViewFragment = fragment; 333 mMessageViewFragment.setCallback(this); 334 335 updateMessageOrderManager(); 336 refreshActionBar(); 337 } 338 339 /** 340 * Uninstall a fragment. Must be caleld from the host activity's 341 * {@link FragmentInstallable#onUninstallFragment}. 342 */ 343 public final void onUninstallFragment(Fragment fragment) { 344 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 345 Log.d(Logging.LOG_TAG, this + " onUninstallFragment fragment=" + fragment); 346 } 347 mRemovedFragments.remove(fragment); 348 if (fragment == mMailboxListFragment) { 349 uninstallMailboxListFragment(); 350 } else if (fragment == mMessageListFragment) { 351 uninstallMessageListFragment(); 352 } else if (fragment == mMessageViewFragment) { 353 uninstallMessageViewFragment(); 354 } else { 355 throw new IllegalArgumentException("Tried to uninstall unknown fragment"); 356 } 357 } 358 359 /** Uninstall {@link MailboxListFragment} */ 360 protected void uninstallMailboxListFragment() { 361 mMailboxListFragment.setCallback(null); 362 mMailboxListFragment = null; 363 } 364 365 /** Uninstall {@link MessageListFragment} */ 366 protected void uninstallMessageListFragment() { 367 mMessageListFragment.setCallback(null); 368 mMessageListFragment = null; 369 } 370 371 /** Uninstall {@link MessageViewFragment} */ 372 protected void uninstallMessageViewFragment() { 373 mMessageViewFragment.setCallback(null); 374 mMessageViewFragment = null; 375 } 376 377 /** 378 * If a {@link Fragment} is not already in {@link #mRemovedFragments}, 379 * {@link FragmentTransaction#remove} it and add to the list. 380 * 381 * Do nothing if {@code fragment} is null. 382 */ 383 protected final void removeFragment(FragmentTransaction ft, Fragment fragment) { 384 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 385 Log.d(Logging.LOG_TAG, this + " removeFragment fragment=" + fragment); 386 } 387 if (fragment == null) { 388 return; 389 } 390 if (!mRemovedFragments.contains(fragment)) { 391 // Remove try/catch when b/4981556 is fixed (framework bug) 392 try { 393 ft.remove(fragment); 394 } catch (IllegalStateException ex) { 395 Log.e(Logging.LOG_TAG, "Swalling IllegalStateException due to known bug for " 396 + " fragment: " + fragment, ex); 397 Log.e(Logging.LOG_TAG, Utility.dumpFragment(fragment)); 398 } 399 addFragmentToRemovalList(fragment); 400 } 401 } 402 403 /** 404 * Remove a {@link Fragment} from {@link #mRemovedFragments}. No-op if {@code fragment} is 405 * null. 406 * 407 * {@link #removeMailboxListFragment}, {@link #removeMessageListFragment} and 408 * {@link #removeMessageViewFragment} all call this, so subclasses don't have to do this when 409 * using them. 410 * 411 * However, unfortunately, subclasses have to call this manually when popping from the 412 * back stack to avoid double-delete. 413 */ 414 protected void addFragmentToRemovalList(Fragment fragment) { 415 if (fragment != null) { 416 mRemovedFragments.add(fragment); 417 } 418 } 419 420 /** 421 * Remove the fragment if it's installed. 422 */ 423 protected FragmentTransaction removeMailboxListFragment(FragmentTransaction ft) { 424 removeFragment(ft, mMailboxListFragment); 425 return ft; 426 } 427 428 /** 429 * Remove the fragment if it's installed. 430 */ 431 protected FragmentTransaction removeMessageListFragment(FragmentTransaction ft) { 432 removeFragment(ft, mMessageListFragment); 433 return ft; 434 } 435 436 /** 437 * Remove the fragment if it's installed. 438 */ 439 protected FragmentTransaction removeMessageViewFragment(FragmentTransaction ft) { 440 removeFragment(ft, mMessageViewFragment); 441 return ft; 442 } 443 444 /** @return true if a {@link MailboxListFragment} is installed. */ 445 protected final boolean isMailboxListInstalled() { 446 return mMailboxListFragment != null; 447 } 448 449 /** @return true if a {@link MessageListFragment} is installed. */ 450 protected final boolean isMessageListInstalled() { 451 return mMessageListFragment != null; 452 } 453 454 /** @return true if a {@link MessageViewFragment} is installed. */ 455 protected final boolean isMessageViewInstalled() { 456 return mMessageViewFragment != null; 457 } 458 459 /** @return the installed {@link MailboxListFragment} or null. */ 460 protected final MailboxListFragment getMailboxListFragment() { 461 return mMailboxListFragment; 462 } 463 464 /** @return the installed {@link MessageListFragment} or null. */ 465 protected final MessageListFragment getMessageListFragment() { 466 return mMessageListFragment; 467 } 468 469 /** @return the installed {@link MessageViewFragment} or null. */ 470 protected final MessageViewFragment getMessageViewFragment() { 471 return mMessageViewFragment; 472 } 473 474 /** 475 * Commit a {@link FragmentTransaction}. 476 */ 477 protected void commitFragmentTransaction(FragmentTransaction ft) { 478 if (DEBUG_FRAGMENTS) { 479 Log.d(Logging.LOG_TAG, this + " commitFragmentTransaction: " + ft); 480 } 481 if (!ft.isEmpty()) { 482 // NB: there should be no cases in which a transaction is committed after 483 // onSaveInstanceState. Unfortunately, the "state loss" check also happens when in 484 // LoaderCallbacks.onLoadFinished, and we wish to perform transactions there. The check 485 // by the framework is conservative and prevents cases where there are transactions 486 // affecting Loader lifecycles - but we have no such cases. 487 // TODO: use asynchronous callbacks from loaders to avoid this implicit dependency 488 ft.commitAllowingStateLoss(); 489 mFragmentManager.executePendingTransactions(); 490 } 491 } 492 493 /** 494 * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 495 * 496 * @see #getActualAccountId() 497 */ 498 public abstract long getUIAccountId(); 499 500 /** 501 * @return true if an account is selected, or the current view is the combined view. 502 */ 503 public final boolean isAccountSelected() { 504 return getUIAccountId() != Account.NO_ACCOUNT; 505 } 506 507 /** 508 * @return if an actual account is selected. (i.e. {@link Account#ACCOUNT_ID_COMBINED_VIEW} 509 * is not considered "actual".s) 510 */ 511 public final boolean isActualAccountSelected() { 512 return isAccountSelected() && (getUIAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW); 513 } 514 515 /** 516 * @return the currently selected account ID. If the current view is the combined view, 517 * it'll return {@link Account#NO_ACCOUNT}. 518 * 519 * @see #getUIAccountId() 520 */ 521 public final long getActualAccountId() { 522 return isActualAccountSelected() ? getUIAccountId() : Account.NO_ACCOUNT; 523 } 524 525 /** 526 * Show the default view for the given account. 527 * 528 * @param accountId ID of the account to load. Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 529 * Must never be {@link Account#NO_ACCOUNT}. 530 * @param forceShowInbox If {@code false} and the given account is already selected, do nothing. 531 * If {@code false}, we always change the view even if the account is selected. 532 */ 533 public final void switchAccount(long accountId, boolean forceShowInbox) { 534 535 if (Account.isSecurityHold(mActivity, accountId)) { 536 ActivityHelper.showSecurityHoldDialog(mActivity, accountId); 537 mActivity.finish(); 538 return; 539 } 540 541 if (accountId == getUIAccountId() && !forceShowInbox) { 542 // Do nothing if the account is already selected. Not even going back to the inbox. 543 return; 544 } 545 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 546 openMailbox(accountId, Mailbox.QUERY_ALL_INBOXES); 547 } else { 548 long inboxId = Mailbox.findMailboxOfType(mActivity, accountId, Mailbox.TYPE_INBOX); 549 if (inboxId == Mailbox.NO_MAILBOX) { 550 // The account doesn't have Inbox yet... Redirect to Welcome and let it wait for 551 // the initial sync... 552 Log.w(Logging.LOG_TAG, "Account " + accountId +" doesn't have Inbox. Redirecting" 553 + " to Welcome..."); 554 Welcome.actionOpenAccountInbox(mActivity, accountId); 555 mActivity.finish(); 556 } else { 557 openMailbox(accountId, inboxId); 558 } 559 } 560 if (mNfcHandler != null) { 561 mNfcHandler.onAccountChanged(); 562 } 563 Preferences.getPreferences(mActivity).setLastUsedAccountId(accountId); 564 showAccountSpecificWarning(accountId); 565 } 566 567 /** 568 * Returns the id of the parent mailbox used for the mailbox list fragment. 569 * 570 * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with 571 * {@link #getMessageListMailboxId()} 572 */ 573 protected long getMailboxListMailboxId() { 574 return isMailboxListInstalled() ? getMailboxListFragment().getSelectedMailboxId() 575 : Mailbox.NO_MAILBOX; 576 } 577 578 /** 579 * Returns the id of the mailbox used for the message list fragment. 580 * 581 * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with 582 * {@link #getMessageListMailboxId()} 583 */ 584 protected long getMessageListMailboxId() { 585 return isMessageListInstalled() ? getMessageListFragment().getMailboxId() 586 : Mailbox.NO_MAILBOX; 587 } 588 589 /** 590 * Shortcut for {@link #open} with {@link Message#NO_MESSAGE}. 591 */ 592 protected final void openMailbox(long accountId, long mailboxId) { 593 open(MessageListContext.forMailbox(accountId, mailboxId), Message.NO_MESSAGE); 594 } 595 596 /** 597 * Opens a given list 598 * @param listContext the list context for the message list to open 599 * @param messageId if specified and not {@link Message#NO_MESSAGE}, will open the message 600 * in the message list. 601 */ 602 public final void open(final MessageListContext listContext, final long messageId) { 603 setListContext(listContext); 604 openInternal(listContext, messageId); 605 606 if (listContext.isSearch()) { 607 mActionBarController.enterSearchMode(listContext.getSearchParams().mFilter); 608 } 609 } 610 611 /** 612 * Sets the internal value of the list context for the message list. 613 */ 614 protected void setListContext(MessageListContext listContext) { 615 if (Objects.equal(listContext, mListContext)) { 616 return; 617 } 618 619 if (Email.DEBUG && Logging.DEBUG_LIFECYCLE) { 620 Log.i(Logging.LOG_TAG, this + " setListContext: " + listContext); 621 } 622 mListContext = listContext; 623 } 624 625 protected abstract void openInternal( 626 final MessageListContext listContext, final long messageId); 627 628 /** 629 * Performs the back action. 630 * 631 * @param isSystemBackKey <code>true</code> if the system back key was pressed. 632 * <code>false</code> if it's caused by the "home" icon click on the action bar. 633 */ 634 public abstract boolean onBackPressed(boolean isSystemBackKey); 635 636 public void onSearchStarted() { 637 // Show/hide the original search icon. 638 mActivity.invalidateOptionsMenu(); 639 } 640 641 /** 642 * Must be called from {@link Activity#onSearchRequested()}. 643 * This initiates the search entry mode - see {@link #onSearchSubmit} for when the search 644 * is actually submitted. 645 */ 646 public void onSearchRequested() { 647 long accountId = getActualAccountId(); 648 boolean accountSearchable = false; 649 if (accountId > 0) { 650 Account account = Account.restoreAccountWithId(mActivity, accountId); 651 if (account != null) { 652 String protocol = account.getProtocol(mActivity); 653 accountSearchable = (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0; 654 } 655 } 656 657 if (!accountSearchable) { 658 return; 659 } 660 661 if (isMessageListReady()) { 662 mActionBarController.enterSearchMode(null); 663 } 664 } 665 666 /** 667 * @return Whether or not a message list is ready and has its initial meta data loaded. 668 */ 669 protected boolean isMessageListReady() { 670 return isMessageListInstalled() && getMessageListFragment().hasDataLoaded(); 671 } 672 673 /** 674 * Determines the mailbox to search, if a search was to be initiated now. 675 * This will return {@code null} if the UI is not focused on any particular mailbox to search 676 * on. 677 */ 678 private Mailbox getSearchableMailbox() { 679 if (!isMessageListReady()) { 680 return null; 681 } 682 MessageListFragment messageList = getMessageListFragment(); 683 684 // If already in a search, future searches will search the original mailbox. 685 return mListContext.isSearch() 686 ? messageList.getSearchedMailbox() 687 : messageList.getMailbox(); 688 } 689 690 // TODO: this logic probably needs to be tested in the backends as well, so it may be nice 691 // to consolidate this to a centralized place, so that they don't get out of sync. 692 /** 693 * @return whether or not this account should do a global search instead when a user 694 * initiates a search on the given mailbox. 695 */ 696 private static boolean shouldDoGlobalSearch(Account account, Mailbox mailbox) { 697 return ((account.mFlags & Account.FLAGS_SUPPORTS_GLOBAL_SEARCH) != 0) 698 && (mailbox.mType == Mailbox.TYPE_INBOX); 699 } 700 701 /** 702 * Retrieves the hint text to be shown for when a search entry is being made. 703 */ 704 protected String getSearchHint() { 705 if (!isMessageListReady()) { 706 return ""; 707 } 708 Account account = getMessageListFragment().getAccount(); 709 Mailbox mailbox = getSearchableMailbox(); 710 711 if (mailbox == null) { 712 return ""; 713 } 714 715 if (shouldDoGlobalSearch(account, mailbox)) { 716 return mActivity.getString(R.string.search_hint); 717 } 718 719 // Regular mailbox, or IMAP - search within that mailbox. 720 String mailboxName = FolderProperties.getInstance(mActivity).getDisplayName(mailbox); 721 return String.format( 722 mActivity.getString(R.string.search_mailbox_hint), 723 mailboxName); 724 } 725 726 /** 727 * Kicks off a search query, if the UI is in a state where a search is possible. 728 */ 729 protected void onSearchSubmit(final String queryTerm) { 730 final long accountId = getUIAccountId(); 731 if (!Account.isNormalAccount(accountId)) { 732 return; // Invalid account to search from. 733 } 734 735 Mailbox searchableMailbox = getSearchableMailbox(); 736 if (searchableMailbox == null) { 737 return; 738 } 739 final long mailboxId = searchableMailbox.mId; 740 741 if (Email.DEBUG) { 742 Log.d(Logging.LOG_TAG, 743 "Submitting search: [" + queryTerm + "] in mailboxId=" + mailboxId); 744 } 745 746 mActivity.startActivity(EmailActivity.createSearchIntent( 747 mActivity, accountId, mailboxId, queryTerm)); 748 749 750 // TODO: this causes a slight flicker. 751 // A new instance of the activity will sit on top. When the user exits search and 752 // returns to this activity, the search box should not be open then. 753 mActionBarController.exitSearchMode(); 754 } 755 756 /** 757 * Handles exiting of search entry mode. 758 */ 759 protected void onSearchExit() { 760 if ((mListContext != null) && mListContext.isSearch()) { 761 mActivity.finish(); 762 } else { 763 // Re show the search icon. 764 mActivity.invalidateOptionsMenu(); 765 } 766 } 767 768 /** 769 * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback. 770 */ 771 public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) { 772 inflater.inflate(R.menu.email_activity_options, menu); 773 return true; 774 } 775 776 /** 777 * Handles the {@link android.app.Activity#onPrepareOptionsMenu} callback. 778 */ 779 public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) { 780 // Update the refresh button. 781 MenuItem item = menu.findItem(R.id.refresh); 782 if (isRefreshEnabled()) { 783 item.setVisible(true); 784 mRefreshListener.setRefreshIcon(item); 785 } else { 786 item.setVisible(false); 787 mRefreshListener.setRefreshIcon(null); 788 } 789 790 // Deal with protocol-specific menu options. 791 boolean mailboxHasServerCounterpart = false; 792 boolean accountSearchable = false; 793 boolean isEas = false; 794 795 if (isMessageListReady()) { 796 long accountId = getActualAccountId(); 797 if (accountId > 0) { 798 Account account = Account.restoreAccountWithId(mActivity, accountId); 799 if (account != null) { 800 String protocol = account.getProtocol(mActivity); 801 isEas = HostAuth.SCHEME_EAS.equals(protocol); 802 Mailbox mailbox = getMessageListFragment().getMailbox(); 803 mailboxHasServerCounterpart = (mailbox != null) 804 && mailbox.loadsFromServer(protocol); 805 accountSearchable = (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0; 806 } 807 } 808 } 809 810 boolean showSearchIcon = !mActionBarController.isInSearchMode() 811 && accountSearchable && mailboxHasServerCounterpart; 812 813 menu.findItem(R.id.search).setVisible(showSearchIcon); 814 menu.findItem(R.id.mailbox_settings).setVisible(isEas && mailboxHasServerCounterpart); 815 return true; 816 } 817 818 /** 819 * Handles the {@link android.app.Activity#onOptionsItemSelected} callback. 820 * 821 * @return true if the option item is handled. 822 */ 823 public boolean onOptionsItemSelected(MenuItem item) { 824 switch (item.getItemId()) { 825 case android.R.id.home: 826 // Comes from the action bar when the app icon on the left is pressed. 827 // It works like a back press, but it won't close the activity. 828 return onBackPressed(false); 829 case R.id.compose: 830 return onCompose(); 831 case R.id.refresh: 832 onRefresh(); 833 return true; 834 case R.id.account_settings: 835 return onAccountSettings(); 836 case R.id.search: 837 onSearchRequested(); 838 return true; 839 case R.id.mailbox_settings: 840 final long mailboxId = getMailboxSettingsMailboxId(); 841 if (mailboxId != Mailbox.NO_MAILBOX) { 842 MailboxSettings.start(mActivity, mailboxId); 843 } 844 return true; 845 } 846 return false; 847 } 848 849 /** 850 * Opens the message compose activity. 851 */ 852 private boolean onCompose() { 853 if (!isAccountSelected()) { 854 return false; // this shouldn't really happen 855 } 856 MessageCompose.actionCompose(mActivity, getActualAccountId()); 857 return true; 858 } 859 860 /** 861 * Handles the "Settings" option item. Opens the settings activity. 862 */ 863 private boolean onAccountSettings() { 864 AccountSettings.actionSettings(mActivity, getActualAccountId()); 865 return true; 866 } 867 868 /** 869 * @return the ID of the message in focus and visible, if any. Returns 870 * {@link Message#NO_MESSAGE} if no message is opened. 871 */ 872 protected long getMessageId() { 873 return isMessageViewInstalled() 874 ? getMessageViewFragment().getMessageId() 875 : Message.NO_MESSAGE; 876 } 877 878 879 /** 880 * @return mailbox ID for "mailbox settings" option. 881 */ 882 protected abstract long getMailboxSettingsMailboxId(); 883 884 /** 885 * Performs "refesh". 886 */ 887 protected abstract void onRefresh(); 888 889 /** 890 * @return true if refresh is in progress for the current mailbox. 891 */ 892 protected abstract boolean isRefreshInProgress(); 893 894 /** 895 * @return true if the UI should enable the "refresh" command. 896 */ 897 protected abstract boolean isRefreshEnabled(); 898 899 /** 900 * Refresh the action bar and menu items, including the "refreshing" icon. 901 */ 902 protected void refreshActionBar() { 903 if (mActionBarController != null) { 904 mActionBarController.refresh(); 905 } 906 mActivity.invalidateOptionsMenu(); 907 } 908 909 // MessageListFragment.Callback 910 @Override 911 public void onMailboxNotFound(boolean isFirstLoad) { 912 // Something bad happened - the account or mailbox we were looking for was deleted. 913 // Just restart and let the entry flow find a good default view. 914 if (isFirstLoad) { 915 // Only show this if it's the first load (e.g. a shortcut) rather an a return to 916 // a mailbox (which might be in a just-deleted account) 917 Utility.showToast(mActivity, R.string.toast_mailbox_not_found); 918 } 919 long accountId = getUIAccountId(); 920 if (accountId != Account.NO_ACCOUNT) { 921 mActivity.startActivity(Welcome.createOpenAccountInboxIntent(mActivity, accountId)); 922 } else { 923 Welcome.actionStart(mActivity); 924 925 } 926 mActivity.finish(); 927 } 928 929 protected final MessageOrderManager getMessageOrderManager() { 930 return mOrderManager; 931 } 932 933 /** Perform "auto-advance. */ 934 protected final void doAutoAdvance() { 935 switch (Preferences.getPreferences(mActivity).getAutoAdvanceDirection()) { 936 case Preferences.AUTO_ADVANCE_NEWER: 937 if (moveToNewer()) return; 938 break; 939 case Preferences.AUTO_ADVANCE_OLDER: 940 if (moveToOlder()) return; 941 break; 942 } 943 if (isMessageViewInstalled()) { // We really should have the message view but just in case 944 // Go back to mailbox list. 945 // Use onBackPressed(), so we'll restore the message view state, such as scroll 946 // position. 947 // Also make sure to pass false to isSystemBackKey, so on two-pane we don't go back 948 // to the collapsed mode. 949 onBackPressed(true); 950 } 951 } 952 953 /** 954 * Subclass must implement it to enable/disable the newer/older buttons. 955 */ 956 protected abstract void updateNavigationArrows(); 957 958 protected final boolean moveToOlder() { 959 if ((mOrderManager != null) && mOrderManager.moveToOlder()) { 960 navigateToMessage(mOrderManager.getCurrentMessageId()); 961 return true; 962 } 963 return false; 964 } 965 966 protected final boolean moveToNewer() { 967 if ((mOrderManager != null) && mOrderManager.moveToNewer()) { 968 navigateToMessage(mOrderManager.getCurrentMessageId()); 969 return true; 970 } 971 return false; 972 } 973 974 /** 975 * Called when the user taps newer/older. Subclass must implement it to open the specified 976 * message. 977 * 978 * It's a bit different from just showing the message view fragment; on one-pane we show the 979 * message view fragment but don't want to change back state. 980 */ 981 protected abstract void navigateToMessage(long messageId); 982 983 /** 984 * Potentially create a new {@link MessageOrderManager}; if it's not already started or if 985 * the account has changed, and sync it to the current message. 986 */ 987 private void updateMessageOrderManager() { 988 if (!isMessageViewInstalled()) { 989 return; 990 } 991 Preconditions.checkNotNull(mListContext); 992 993 if (mOrderManager == null || !mOrderManager.getListContext().equals(mListContext)) { 994 stopMessageOrderManager(); 995 mOrderManager = new MessageOrderManager( 996 mActivity, mListContext, mMessageOrderManagerCallback); 997 } 998 mOrderManager.moveTo(getMessageId()); 999 updateNavigationArrows(); 1000 } 1001 1002 /** 1003 * Stop {@link MessageOrderManager}. 1004 */ 1005 protected final void stopMessageOrderManager() { 1006 if (mOrderManager != null) { 1007 mOrderManager.close(); 1008 mOrderManager = null; 1009 } 1010 } 1011 1012 private class MessageOrderManagerCallback implements MessageOrderManager.Callback { 1013 @Override 1014 public void onMessagesChanged() { 1015 updateNavigationArrows(); 1016 } 1017 1018 @Override 1019 public void onMessageNotFound() { 1020 doAutoAdvance(); 1021 } 1022 } 1023 1024 1025 private void showAccountSpecificWarning(long accountId) { 1026 if (accountId != Account.NO_ACCOUNT && accountId != Account.NO_ACCOUNT) { 1027 Account account = Account.restoreAccountWithId(mActivity, accountId); 1028 if (account != null && 1029 Preferences.getPreferences(mActivity) 1030 .shouldShowRequireManualSync(mActivity, account)) { 1031 new RequireManualSyncDialog(mActivity, account).show(); 1032 } 1033 } 1034 } 1035 1036 @Override 1037 public String toString() { 1038 return getClass().getSimpleName(); // Shown on logcat 1039 } 1040 } 1041