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