UIControllerBase.java revision 396a425d799334c0242ba2d29e4b5715e0360f7e
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 * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 463 * 464 * @see #getActualAccountId() 465 */ 466 public abstract long getUIAccountId(); 467 468 /** 469 * @return true if an account is selected, or the current view is the combined view. 470 */ 471 public final boolean isAccountSelected() { 472 return getUIAccountId() != Account.NO_ACCOUNT; 473 } 474 475 /** 476 * @return if an actual account is selected. (i.e. {@link Account#ACCOUNT_ID_COMBINED_VIEW} 477 * is not considered "actual".s) 478 */ 479 public final boolean isActualAccountSelected() { 480 return isAccountSelected() && (getUIAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW); 481 } 482 483 /** 484 * @return the currently selected account ID. If the current view is the combined view, 485 * it'll return {@link Account#NO_ACCOUNT}. 486 * 487 * @see #getUIAccountId() 488 */ 489 public final long getActualAccountId() { 490 return isActualAccountSelected() ? getUIAccountId() : Account.NO_ACCOUNT; 491 } 492 493 /** 494 * Show the default view for the given account. 495 * 496 * @param accountId ID of the account to load. Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 497 * Must never be {@link Account#NO_ACCOUNT}. 498 * @param forceShowInbox If {@code false} and the given account is already selected, do nothing. 499 * If {@code false}, we always change the view even if the account is selected. 500 */ 501 public final void switchAccount(long accountId, boolean forceShowInbox) { 502 503 if (Account.isSecurityHold(mActivity, accountId)) { 504 ActivityHelper.showSecurityHoldDialog(mActivity, accountId); 505 mActivity.finish(); 506 return; 507 } 508 509 if (accountId == getUIAccountId() && !forceShowInbox) { 510 // Do nothing if the account is already selected. Not even going back to the inbox. 511 return; 512 } 513 514 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 515 openMailbox(accountId, Mailbox.QUERY_ALL_INBOXES); 516 } else { 517 long inboxId = Mailbox.findMailboxOfType(mActivity, accountId, Mailbox.TYPE_INBOX); 518 if (inboxId == Mailbox.NO_MAILBOX) { 519 // The account doesn't have Inbox yet... Redirect to Welcome and let it wait for 520 // the initial sync... 521 Log.w(Logging.LOG_TAG, "Account " + accountId +" doesn't have Inbox. Redirecting" 522 + " to Welcome..."); 523 Welcome.actionOpenAccountInbox(mActivity, accountId); 524 mActivity.finish(); 525 return; 526 } else { 527 openMailbox(accountId, inboxId); 528 } 529 } 530 } 531 532 /** 533 * Returns the id of the parent mailbox used for the mailbox list fragment. 534 * 535 * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with 536 * {@link #getMessageListMailboxId()} 537 */ 538 protected long getMailboxListMailboxId() { 539 return isMailboxListInstalled() ? getMailboxListFragment().getSelectedMailboxId() 540 : Mailbox.NO_MAILBOX; 541 } 542 543 /** 544 * Returns the id of the mailbox used for the message list fragment. 545 * 546 * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with 547 * {@link #getMessageListMailboxId()} 548 */ 549 protected long getMessageListMailboxId() { 550 return isMessageListInstalled() ? getMessageListFragment().getMailboxId() 551 : Mailbox.NO_MAILBOX; 552 } 553 554 /** 555 * Shortcut for {@link #open} with {@link Message#NO_MESSAGE}. 556 */ 557 protected final void openMailbox(long accountId, long mailboxId) { 558 open(MessageListContext.forMailbox(accountId, mailboxId), Message.NO_MESSAGE); 559 } 560 561 /** 562 * Opens a given list 563 * @param listContext the list context for the message list to open 564 * @param messageId if specified and not {@link Message#NO_MESSAGE}, will open the message 565 * in the message list. 566 */ 567 public final void open(final MessageListContext listContext, final long messageId) { 568 setListContext(listContext); 569 openInternal(listContext, messageId); 570 571 if (listContext.isSearch()) { 572 mActionBarController.enterSearchMode(listContext.getSearchParams().mFilter); 573 } 574 } 575 576 /** 577 * Sets the internal value of the list context for the message list. 578 */ 579 protected void setListContext(MessageListContext listContext) { 580 if (Objects.equal(listContext, mListContext)) { 581 return; 582 } 583 584 // TODO: remove this when the search mailbox no longer shows up on the list 585 // Special case search. Since the search mailbox shows up in the mailbox list, the mailbox 586 // list can give us a callback to open that mailbox, and it will look like a normal 587 // mailbox open instead of a search, blowing away a perfectly good list context. 588 if (mListContext != null 589 && mListContext.isSearch() 590 && mListContext.getMailboxId() == listContext.getMailboxId()) { 591 return; 592 } 593 594 if (Email.DEBUG && Logging.DEBUG_LIFECYCLE) { 595 Log.i(Logging.LOG_TAG, this + " setListContext: " + listContext); 596 } 597 mListContext = listContext; 598 } 599 600 protected abstract void openInternal( 601 final MessageListContext listContext, final long messageId); 602 603 /** 604 * Performs the back action. 605 * 606 * @param isSystemBackKey <code>true</code> if the system back key was pressed. 607 * <code>false</code> if it's caused by the "home" icon click on the action bar. 608 */ 609 public abstract boolean onBackPressed(boolean isSystemBackKey); 610 611 /** 612 * Must be called from {@link Activity#onSearchRequested()}. 613 * This initiates the search entry mode - see {@link #onSearchSubmit} for when the search 614 * is actually submitted. 615 */ 616 public void onSearchRequested() { 617 if (isMessageListReady()) { 618 mActionBarController.enterSearchMode(null); 619 } 620 } 621 622 /** 623 * @return Whether or not a message list is ready and has its initial meta data loaded. 624 */ 625 protected boolean isMessageListReady() { 626 return isMessageListInstalled() && getMessageListFragment().hasDataLoaded(); 627 } 628 629 /** 630 * Determines the mailbox to search, if a search was to be initiated now. 631 * This will return {@code null} if the UI is not focused on any particular mailbox to search 632 * on. 633 */ 634 private Mailbox getSearchableMailbox() { 635 if (!isMessageListReady()) { 636 return null; 637 } 638 MessageListFragment messageList = getMessageListFragment(); 639 640 // If already in a search, future searches will search the original mailbox. 641 return mListContext.isSearch() 642 ? messageList.getSearchedMailbox() 643 : messageList.getMailbox(); 644 } 645 646 // TODO: this logic probably needs to be tested in the backends as well, so it may be nice 647 // to consolidate this to a centralized place, so that they don't get out of sync. 648 /** 649 * @return whether or not this account should do a global search instead when a user 650 * initiates a search on the given mailbox. 651 */ 652 private static boolean shouldDoGlobalSearch(Account account, Mailbox mailbox) { 653 return ((account.mFlags & Account.FLAGS_SUPPORTS_GLOBAL_SEARCH) != 0) 654 && (mailbox.mType == Mailbox.TYPE_INBOX); 655 } 656 657 /** 658 * Retrieves the hint text to be shown for when a search entry is being made. 659 */ 660 protected String getSearchHint() { 661 if (!isMessageListReady()) { 662 return ""; 663 } 664 Account account = getMessageListFragment().getAccount(); 665 Mailbox mailbox = getSearchableMailbox(); 666 667 if (mailbox == null) { 668 return ""; 669 } 670 671 if (shouldDoGlobalSearch(account, mailbox)) { 672 return mActivity.getString(R.string.search_hint); 673 } 674 675 // Regular mailbox, or IMAP - search within that mailbox. 676 String mailboxName = FolderProperties.getInstance(mActivity).getDisplayName(mailbox); 677 return String.format( 678 mActivity.getString(R.string.search_mailbox_hint), 679 mailboxName); 680 } 681 682 /** 683 * Kicks off a search query, if the UI is in a state where a search is possible. 684 */ 685 protected void onSearchSubmit(final String queryTerm) { 686 final long accountId = getUIAccountId(); 687 if (!Account.isNormalAccount(accountId)) { 688 return; // Invalid account to search from. 689 } 690 691 Mailbox searchableMailbox = getSearchableMailbox(); 692 if (searchableMailbox == null) { 693 return; 694 } 695 final long mailboxId = searchableMailbox.mId; 696 697 if (Email.DEBUG) { 698 Log.d(Logging.LOG_TAG, 699 "Submitting search: [" + queryTerm + "] in mailboxId=" + mailboxId); 700 } 701 702 mActivity.startActivity(EmailActivity.createSearchIntent( 703 mActivity, accountId, mailboxId, queryTerm)); 704 705 706 // TODO: this causes a slight flicker. 707 // A new instance of the activity will sit on top. When the user exits search and 708 // returns to this activity, the search box should not be open then. 709 mActionBarController.exitSearchMode(); 710 } 711 712 /** 713 * Handles exiting of search entry mode. 714 */ 715 protected void onSearchExit() { 716 if ((mListContext != null) && mListContext.isSearch()) { 717 mActivity.finish(); 718 } 719 } 720 721 /** 722 * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback. 723 */ 724 public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) { 725 inflater.inflate(R.menu.email_activity_options, menu); 726 return true; 727 } 728 729 /** 730 * Handles the {@link android.app.Activity#onPrepareOptionsMenu} callback. 731 */ 732 public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) { 733 // Update the refresh button. 734 MenuItem item = menu.findItem(R.id.refresh); 735 if (isRefreshEnabled()) { 736 item.setVisible(true); 737 mRefreshListener.setRefreshIcon(item); 738 } else { 739 item.setVisible(false); 740 mRefreshListener.setRefreshIcon(null); 741 } 742 743 // Deal with protocol-specific menu options. 744 boolean isEas = false; 745 boolean accountSearchable = false; 746 long accountId = getActualAccountId(); 747 if (accountId > 0) { 748 Account account = Account.restoreAccountWithId(mActivity, accountId); 749 if (account != null) { 750 String protocol = account.getProtocol(mActivity); 751 if (HostAuth.SCHEME_EAS.equals(protocol)) { 752 isEas = true; 753 } 754 accountSearchable = (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0; 755 } 756 } 757 758 // TODO: Should use an isSyncable call to prevent drafts/outbox from allowing this 759 menu.findItem(R.id.search).setVisible(accountSearchable); 760 // TODO Show only for syncable mailbox as well. 761 menu.findItem(R.id.mailbox_settings).setVisible(isEas 762 && (getMailboxSettingsMailboxId() != Mailbox.NO_MAILBOX)); 763 764 return true; 765 } 766 767 /** 768 * Handles the {@link android.app.Activity#onOptionsItemSelected} callback. 769 * 770 * @return true if the option item is handled. 771 */ 772 public boolean onOptionsItemSelected(MenuItem item) { 773 switch (item.getItemId()) { 774 case android.R.id.home: 775 // Comes from the action bar when the app icon on the left is pressed. 776 // It works like a back press, but it won't close the activity. 777 return onBackPressed(false); 778 case R.id.compose: 779 return onCompose(); 780 case R.id.refresh: 781 onRefresh(); 782 return true; 783 case R.id.account_settings: 784 return onAccountSettings(); 785 case R.id.search: 786 onSearchRequested(); 787 return true; 788 case R.id.mailbox_settings: 789 final long mailboxId = getMailboxSettingsMailboxId(); 790 if (mailboxId != Mailbox.NO_MAILBOX) { 791 MailboxSettings.start(mActivity, mailboxId); 792 } 793 return true; 794 } 795 return false; 796 } 797 798 /** 799 * Opens the message compose activity. 800 */ 801 private boolean onCompose() { 802 if (!isAccountSelected()) { 803 return false; // this shouldn't really happen 804 } 805 MessageCompose.actionCompose(mActivity, getActualAccountId()); 806 return true; 807 } 808 809 /** 810 * Handles the "Settings" option item. Opens the settings activity. 811 */ 812 private boolean onAccountSettings() { 813 AccountSettings.actionSettings(mActivity, getActualAccountId()); 814 return true; 815 } 816 817 /** 818 * @return the ID of the message in focus and visible, if any. Returns 819 * {@link Message#NO_MESSAGE} if no message is opened. 820 */ 821 protected long getMessageId() { 822 return isMessageViewInstalled() 823 ? getMessageViewFragment().getMessageId() 824 : Message.NO_MESSAGE; 825 } 826 827 828 /** 829 * @return mailbox ID for "mailbox settings" option. 830 */ 831 protected abstract long getMailboxSettingsMailboxId(); 832 833 /** 834 * Performs "refesh". 835 */ 836 protected abstract void onRefresh(); 837 838 /** 839 * @return true if refresh is in progress for the current mailbox. 840 */ 841 protected abstract boolean isRefreshInProgress(); 842 843 /** 844 * @return true if the UI should enable the "refresh" command. 845 */ 846 protected abstract boolean isRefreshEnabled(); 847 848 /** 849 * Refresh the action bar and menu items, including the "refreshing" icon. 850 */ 851 protected void refreshActionBar() { 852 if (mActionBarController != null) { 853 mActionBarController.refresh(); 854 } 855 mActivity.invalidateOptionsMenu(); 856 } 857 858 protected final MessageOrderManager getMessageOrderManager() { 859 return mOrderManager; 860 } 861 862 /** Perform "auto-advance. */ 863 protected final void doAutoAdvance() { 864 switch (Preferences.getPreferences(mActivity).getAutoAdvanceDirection()) { 865 case Preferences.AUTO_ADVANCE_NEWER: 866 if (moveToNewer()) return; 867 break; 868 case Preferences.AUTO_ADVANCE_OLDER: 869 if (moveToOlder()) return; 870 break; 871 } 872 if (isMessageViewInstalled()) { // We really should have the message view but just in case 873 // Go back to mailbox list. 874 // Use onBackPressed(), so we'll restore the message view state, such as scroll 875 // position. 876 // Also make sure to pass false to isSystemBackKey, so on two-pane we don't go back 877 // to the collapsed mode. 878 onBackPressed(true); 879 } 880 } 881 882 /** 883 * Subclass must implement it to enable/disable the newer/older buttons. 884 */ 885 protected abstract void updateNavigationArrows(); 886 887 protected final boolean moveToOlder() { 888 if ((mOrderManager != null) && mOrderManager.moveToOlder()) { 889 navigateToMessage(mOrderManager.getCurrentMessageId()); 890 return true; 891 } 892 return false; 893 } 894 895 protected final boolean moveToNewer() { 896 if ((mOrderManager != null) && mOrderManager.moveToNewer()) { 897 navigateToMessage(mOrderManager.getCurrentMessageId()); 898 return true; 899 } 900 return false; 901 } 902 903 /** 904 * Called when the user taps newer/older. Subclass must implement it to open the specified 905 * message. 906 * 907 * It's a bit different from just showing the message view fragment; on one-pane we show the 908 * message view fragment but don't want to change back state. 909 */ 910 protected abstract void navigateToMessage(long messageId); 911 912 /** 913 * Potentially create a new {@link MessageOrderManager}; if it's not already started or if 914 * the account has changed, and sync it to the current message. 915 */ 916 private void updateMessageOrderManager() { 917 if (!isMessageViewInstalled()) { 918 return; 919 } 920 Preconditions.checkNotNull(mListContext); 921 922 final long mailboxId = mListContext.getMailboxId(); 923 if (mOrderManager == null || mOrderManager.getMailboxId() != mailboxId) { 924 stopMessageOrderManager(); 925 mOrderManager = 926 new MessageOrderManager(mActivity, mailboxId, mMessageOrderManagerCallback); 927 } 928 mOrderManager.moveTo(getMessageId()); 929 updateNavigationArrows(); 930 } 931 932 /** 933 * Stop {@link MessageOrderManager}. 934 */ 935 protected final void stopMessageOrderManager() { 936 if (mOrderManager != null) { 937 mOrderManager.close(); 938 mOrderManager = null; 939 } 940 } 941 942 private class MessageOrderManagerCallback implements MessageOrderManager.Callback { 943 @Override 944 public void onMessagesChanged() { 945 updateNavigationArrows(); 946 } 947 948 @Override 949 public void onMessageNotFound() { 950 doAutoAdvance(); 951 } 952 } 953 954 @Override 955 public String toString() { 956 return getClass().getSimpleName(); // Shown on logcat 957 } 958} 959