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