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