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