UIControllerBase.java revision 1ef8ec61c9e4d717c6d4dff6136f85f84c387856
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 if (isMessageListReady()) { 627 mActionBarController.enterSearchMode(null); 628 } 629 } 630 631 /** 632 * @return Whether or not a message list is ready and has its initial meta data loaded. 633 */ 634 protected boolean isMessageListReady() { 635 return isMessageListInstalled() && getMessageListFragment().hasDataLoaded(); 636 } 637 638 /** 639 * Determines the mailbox to search, if a search was to be initiated now. 640 * This will return {@code null} if the UI is not focused on any particular mailbox to search 641 * on. 642 */ 643 private Mailbox getSearchableMailbox() { 644 if (!isMessageListReady()) { 645 return null; 646 } 647 MessageListFragment messageList = getMessageListFragment(); 648 649 // If already in a search, future searches will search the original mailbox. 650 return mListContext.isSearch() 651 ? messageList.getSearchedMailbox() 652 : messageList.getMailbox(); 653 } 654 655 // TODO: this logic probably needs to be tested in the backends as well, so it may be nice 656 // to consolidate this to a centralized place, so that they don't get out of sync. 657 /** 658 * @return whether or not this account should do a global search instead when a user 659 * initiates a search on the given mailbox. 660 */ 661 private static boolean shouldDoGlobalSearch(Account account, Mailbox mailbox) { 662 return ((account.mFlags & Account.FLAGS_SUPPORTS_GLOBAL_SEARCH) != 0) 663 && (mailbox.mType == Mailbox.TYPE_INBOX); 664 } 665 666 /** 667 * Retrieves the hint text to be shown for when a search entry is being made. 668 */ 669 protected String getSearchHint() { 670 if (!isMessageListReady()) { 671 return ""; 672 } 673 Account account = getMessageListFragment().getAccount(); 674 Mailbox mailbox = getSearchableMailbox(); 675 676 if (mailbox == null) { 677 return ""; 678 } 679 680 if (shouldDoGlobalSearch(account, mailbox)) { 681 return mActivity.getString(R.string.search_hint); 682 } 683 684 // Regular mailbox, or IMAP - search within that mailbox. 685 String mailboxName = FolderProperties.getInstance(mActivity).getDisplayName(mailbox); 686 return String.format( 687 mActivity.getString(R.string.search_mailbox_hint), 688 mailboxName); 689 } 690 691 /** 692 * Kicks off a search query, if the UI is in a state where a search is possible. 693 */ 694 protected void onSearchSubmit(final String queryTerm) { 695 final long accountId = getUIAccountId(); 696 if (!Account.isNormalAccount(accountId)) { 697 return; // Invalid account to search from. 698 } 699 700 Mailbox searchableMailbox = getSearchableMailbox(); 701 if (searchableMailbox == null) { 702 return; 703 } 704 final long mailboxId = searchableMailbox.mId; 705 706 if (Email.DEBUG) { 707 Log.d(Logging.LOG_TAG, 708 "Submitting search: [" + queryTerm + "] in mailboxId=" + mailboxId); 709 } 710 711 mActivity.startActivity(EmailActivity.createSearchIntent( 712 mActivity, accountId, mailboxId, queryTerm)); 713 714 715 // TODO: this causes a slight flicker. 716 // A new instance of the activity will sit on top. When the user exits search and 717 // returns to this activity, the search box should not be open then. 718 mActionBarController.exitSearchMode(); 719 } 720 721 /** 722 * Handles exiting of search entry mode. 723 */ 724 protected void onSearchExit() { 725 if ((mListContext != null) && mListContext.isSearch()) { 726 mActivity.finish(); 727 } 728 } 729 730 /** 731 * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback. 732 */ 733 public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) { 734 inflater.inflate(R.menu.email_activity_options, menu); 735 return true; 736 } 737 738 /** 739 * Handles the {@link android.app.Activity#onPrepareOptionsMenu} callback. 740 */ 741 public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) { 742 // Update the refresh button. 743 MenuItem item = menu.findItem(R.id.refresh); 744 if (isRefreshEnabled()) { 745 item.setVisible(true); 746 mRefreshListener.setRefreshIcon(item); 747 } else { 748 item.setVisible(false); 749 mRefreshListener.setRefreshIcon(null); 750 } 751 752 // Deal with protocol-specific menu options. 753 boolean isEas = false; 754 boolean accountSearchable = false; 755 long accountId = getActualAccountId(); 756 if (accountId > 0) { 757 Account account = Account.restoreAccountWithId(mActivity, accountId); 758 if (account != null) { 759 String protocol = account.getProtocol(mActivity); 760 if (HostAuth.SCHEME_EAS.equals(protocol)) { 761 isEas = true; 762 } 763 accountSearchable = (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0; 764 } 765 } 766 767 // TODO: Should use an isSyncable call to prevent drafts/outbox from allowing this 768 menu.findItem(R.id.search).setVisible(accountSearchable); 769 // TODO Show only for syncable mailbox as well. 770 menu.findItem(R.id.mailbox_settings).setVisible(isEas 771 && (getMailboxSettingsMailboxId() != Mailbox.NO_MAILBOX)); 772 773 return true; 774 } 775 776 /** 777 * Handles the {@link android.app.Activity#onOptionsItemSelected} callback. 778 * 779 * @return true if the option item is handled. 780 */ 781 public boolean onOptionsItemSelected(MenuItem item) { 782 switch (item.getItemId()) { 783 case android.R.id.home: 784 // Comes from the action bar when the app icon on the left is pressed. 785 // It works like a back press, but it won't close the activity. 786 return onBackPressed(false); 787 case R.id.compose: 788 return onCompose(); 789 case R.id.refresh: 790 onRefresh(); 791 return true; 792 case R.id.account_settings: 793 return onAccountSettings(); 794 case R.id.search: 795 onSearchRequested(); 796 return true; 797 case R.id.mailbox_settings: 798 final long mailboxId = getMailboxSettingsMailboxId(); 799 if (mailboxId != Mailbox.NO_MAILBOX) { 800 MailboxSettings.start(mActivity, mailboxId); 801 } 802 return true; 803 } 804 return false; 805 } 806 807 /** 808 * Opens the message compose activity. 809 */ 810 private boolean onCompose() { 811 if (!isAccountSelected()) { 812 return false; // this shouldn't really happen 813 } 814 MessageCompose.actionCompose(mActivity, getActualAccountId()); 815 return true; 816 } 817 818 /** 819 * Handles the "Settings" option item. Opens the settings activity. 820 */ 821 private boolean onAccountSettings() { 822 AccountSettings.actionSettings(mActivity, getActualAccountId()); 823 return true; 824 } 825 826 /** 827 * @return the ID of the message in focus and visible, if any. Returns 828 * {@link Message#NO_MESSAGE} if no message is opened. 829 */ 830 protected long getMessageId() { 831 return isMessageViewInstalled() 832 ? getMessageViewFragment().getMessageId() 833 : Message.NO_MESSAGE; 834 } 835 836 837 /** 838 * @return mailbox ID for "mailbox settings" option. 839 */ 840 protected abstract long getMailboxSettingsMailboxId(); 841 842 /** 843 * Performs "refesh". 844 */ 845 protected abstract void onRefresh(); 846 847 /** 848 * @return true if refresh is in progress for the current mailbox. 849 */ 850 protected abstract boolean isRefreshInProgress(); 851 852 /** 853 * @return true if the UI should enable the "refresh" command. 854 */ 855 protected abstract boolean isRefreshEnabled(); 856 857 /** 858 * Refresh the action bar and menu items, including the "refreshing" icon. 859 */ 860 protected void refreshActionBar() { 861 if (mActionBarController != null) { 862 mActionBarController.refresh(); 863 } 864 mActivity.invalidateOptionsMenu(); 865 } 866 867 // MessageListFragment.Callback 868 @Override 869 public void onMailboxNotFound() { 870 // Something bad happened - the account or mailbox we were looking for was deleted. 871 // Just restart and let the entry flow find a good default view. 872 Utility.showToast(mActivity, R.string.toast_mailbox_not_found); 873 Welcome.actionStart(mActivity); 874 mActivity.finish(); 875 } 876 877 protected final MessageOrderManager getMessageOrderManager() { 878 return mOrderManager; 879 } 880 881 /** Perform "auto-advance. */ 882 protected final void doAutoAdvance() { 883 switch (Preferences.getPreferences(mActivity).getAutoAdvanceDirection()) { 884 case Preferences.AUTO_ADVANCE_NEWER: 885 if (moveToNewer()) return; 886 break; 887 case Preferences.AUTO_ADVANCE_OLDER: 888 if (moveToOlder()) return; 889 break; 890 } 891 if (isMessageViewInstalled()) { // We really should have the message view but just in case 892 // Go back to mailbox list. 893 // Use onBackPressed(), so we'll restore the message view state, such as scroll 894 // position. 895 // Also make sure to pass false to isSystemBackKey, so on two-pane we don't go back 896 // to the collapsed mode. 897 onBackPressed(true); 898 } 899 } 900 901 /** 902 * Subclass must implement it to enable/disable the newer/older buttons. 903 */ 904 protected abstract void updateNavigationArrows(); 905 906 protected final boolean moveToOlder() { 907 if ((mOrderManager != null) && mOrderManager.moveToOlder()) { 908 navigateToMessage(mOrderManager.getCurrentMessageId()); 909 return true; 910 } 911 return false; 912 } 913 914 protected final boolean moveToNewer() { 915 if ((mOrderManager != null) && mOrderManager.moveToNewer()) { 916 navigateToMessage(mOrderManager.getCurrentMessageId()); 917 return true; 918 } 919 return false; 920 } 921 922 /** 923 * Called when the user taps newer/older. Subclass must implement it to open the specified 924 * message. 925 * 926 * It's a bit different from just showing the message view fragment; on one-pane we show the 927 * message view fragment but don't want to change back state. 928 */ 929 protected abstract void navigateToMessage(long messageId); 930 931 /** 932 * Potentially create a new {@link MessageOrderManager}; if it's not already started or if 933 * the account has changed, and sync it to the current message. 934 */ 935 private void updateMessageOrderManager() { 936 if (!isMessageViewInstalled()) { 937 return; 938 } 939 Preconditions.checkNotNull(mListContext); 940 941 final long mailboxId = mListContext.getMailboxId(); 942 if (mOrderManager == null || mOrderManager.getMailboxId() != mailboxId) { 943 stopMessageOrderManager(); 944 mOrderManager = 945 new MessageOrderManager(mActivity, mailboxId, mMessageOrderManagerCallback); 946 } 947 mOrderManager.moveTo(getMessageId()); 948 updateNavigationArrows(); 949 } 950 951 /** 952 * Stop {@link MessageOrderManager}. 953 */ 954 protected final void stopMessageOrderManager() { 955 if (mOrderManager != null) { 956 mOrderManager.close(); 957 mOrderManager = null; 958 } 959 } 960 961 private class MessageOrderManagerCallback implements MessageOrderManager.Callback { 962 @Override 963 public void onMessagesChanged() { 964 updateNavigationArrows(); 965 } 966 967 @Override 968 public void onMessageNotFound() { 969 doAutoAdvance(); 970 } 971 } 972 973 @Override 974 public String toString() { 975 return getClass().getSimpleName(); // Shown on logcat 976 } 977} 978