UIControllerTwoPane.java revision f5492ea991d3b296b8158f6ea0e85cdbae5941ed
1/* 2 * Copyright (C) 2010 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 com.android.email.Clock; 20import com.android.email.Email; 21import com.android.email.Preferences; 22import com.android.email.R; 23import com.android.email.RefreshManager; 24import com.android.email.activity.setup.AccountSecurity; 25import com.android.email.activity.setup.AccountSettingsXL; 26import com.android.emailcommon.Logging; 27import com.android.emailcommon.provider.EmailContent.Account; 28import com.android.emailcommon.provider.EmailContent.Mailbox; 29import com.android.emailcommon.utility.EmailAsyncTask; 30import com.android.emailcommon.utility.Utility; 31 32import android.app.Activity; 33import android.app.Fragment; 34import android.app.FragmentManager; 35import android.app.FragmentTransaction; 36import android.content.Context; 37import android.os.Bundle; 38import android.util.Log; 39import android.view.Menu; 40import android.view.MenuInflater; 41import android.view.MenuItem; 42 43import java.security.InvalidParameterException; 44import java.util.Set; 45import java.util.Stack; 46 47/** 48 * UI Controller for x-large devices. Supports a multi-pane layout. 49 */ 50class UIControllerTwoPane extends UIControllerBase implements 51 MailboxFinder.Callback, 52 ThreePaneLayout.Callback, 53 MailboxListFragment.Callback, 54 MessageListFragment.Callback, 55 MessageViewFragment.Callback { 56 private static final String BUNDLE_KEY_ACCOUNT_ID = "UIControllerTwoPane.state.account_id"; 57 private static final String BUNDLE_KEY_MAILBOX_ID = "UIControllerTwoPane.state.mailbox_id"; 58 private static final String BUNDLE_KEY_MESSAGE_ID = "UIControllerTwoPane.state.message_id"; 59 private static final String BUNDLE_KEY_MAILBOX_STACK 60 = "UIControllerTwoPane.state.mailbox_stack"; 61 62 /* package */ static final int MAILBOX_REFRESH_MIN_INTERVAL = 30 * 1000; // in milliseconds 63 /* package */ static final int INBOX_AUTO_REFRESH_MIN_INTERVAL = 10 * 1000; // in milliseconds 64 65 /** Current account id */ 66 private long mAccountId = NO_ACCOUNT; 67 68 // TODO Remove this instance variable and replace it with a call to mMessageListFragment to 69 // retrieve it's mailbox ID. There's no reason we should be duplicating data 70 /** 71 * The id of the currently viewed mailbox in the mailbox list fragment. 72 * IMPORTANT: Do not confuse this with the value returned by {@link #getMessageListMailboxId()} 73 * which is the mailbox id associated with the message list fragment. The two may be different. 74 */ 75 private long mMailboxListMailboxId = NO_MAILBOX; 76 77 /** Current message id */ 78 private long mMessageId = NO_MESSAGE; 79 80 private ActionBarController mActionBarController; 81 private final ActionBarControllerCallback mActionBarControllerCallback = 82 new ActionBarControllerCallback(); 83 84 // Other UI elements 85 private ThreePaneLayout mThreePane; 86 87 /** 88 * Fragments that are installed. 89 * 90 * A fragment is installed when: 91 * - it is attached to the activity 92 * - the parent activity is created 93 * - and it is not scheduled to be removed. 94 * 95 * We set callbacks to fragments only when they are installed. 96 */ 97 private MailboxListFragment mMailboxListFragment; 98 private MessageListFragment mMessageListFragment; 99 private MessageViewFragment mMessageViewFragment; 100 101 private MessageCommandButtonView mMessageCommandButtons; 102 103 private MailboxFinder mMailboxFinder; 104 105 private final RefreshListener mRefreshListener = new RefreshListener(); 106 private MessageOrderManager mOrderManager; 107 private final MessageOrderManagerCallback mMessageOrderManagerCallback = 108 new MessageOrderManagerCallback(); 109 /** Mailbox IDs that the user has navigated away from; used to provide "back" functionality */ 110 private final Stack<Long> mMailboxStack = new Stack<Long>(); 111 112 /** 113 * The mailbox name selected on the mailbox list. 114 * Passed via {@link #onCurrentMailboxUpdated}. 115 */ 116 private String mCurrentMailboxName; 117 118 /** 119 * The unread count for the mailbox selected on the mailbox list. 120 * Passed via {@link #onCurrentMailboxUpdated}. 121 * 122 * 0 if the mailbox doesn't have the concept of "unread". e.g. Drafts. 123 */ 124 private int mCurrentMailboxUnreadCount; 125 126 public UIControllerTwoPane(EmailActivity activity) { 127 super(activity); 128 } 129 130 @Override 131 public int getLayoutId() { 132 return R.layout.email_activity_two_pane; 133 } 134 135 // MailboxFinder$Callback 136 @Override 137 public void onAccountNotFound() { 138 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 139 Log.d(Logging.LOG_TAG, this + " onAccountNotFound()"); 140 } 141 // Shouldn't happen 142 } 143 144 @Override 145 public void onAccountSecurityHold(long accountId) { 146 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 147 Log.d(Logging.LOG_TAG, this + " onAccountSecurityHold()"); 148 } 149 mActivity.startActivity(AccountSecurity.actionUpdateSecurityIntent(mActivity, accountId, 150 true)); 151 } 152 153 @Override 154 public void onMailboxFound(long accountId, long mailboxId) { 155 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 156 Log.d(Logging.LOG_TAG, this + " onMailboxFound()"); 157 } 158 updateMessageList(mailboxId, true, true); 159 } 160 161 @Override 162 public void onMailboxNotFound(long accountId) { 163 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 164 Log.d(Logging.LOG_TAG, this + " onMailboxNotFound()"); 165 } 166 // TODO: handle more gracefully. 167 Log.e(Logging.LOG_TAG, "unable to find mailbox for account " + accountId); 168 } 169 170 @Override 171 public void onMailboxNotFound() { 172 // TODO: handle more gracefully. 173 Log.e(Logging.LOG_TAG, "unable to find mailbox"); 174 } 175 176 // ThreePaneLayoutCallback 177 @Override 178 public void onVisiblePanesChanged(int previousVisiblePanes) { 179 refreshActionBar(); 180 181 // If the right pane is gone, remove the message view. 182 final int visiblePanes = mThreePane.getVisiblePanes(); 183 if (((visiblePanes & ThreePaneLayout.PANE_RIGHT) == 0) && 184 ((previousVisiblePanes & ThreePaneLayout.PANE_RIGHT) != 0)) { 185 // Message view just got hidden 186 mMessageId = NO_MESSAGE; 187 if (mMessageListFragment != null) { 188 mMessageListFragment.setSelectedMessage(NO_MESSAGE); 189 } 190 uninstallMessageViewFragment(mActivity.getFragmentManager().beginTransaction()) 191 .commit(); 192 } 193 // Disable CAB when the message list is not visible. 194 if (mMessageListFragment != null) { 195 mMessageListFragment.onHidden((visiblePanes & ThreePaneLayout.PANE_MIDDLE) == 0); 196 } 197 } 198 199 private void refreshActionBar() { 200 if (mActionBarController != null) { 201 mActionBarController.refresh(); 202 } 203 } 204 205 // MailboxListFragment$Callback 206 @Override 207 public void onMailboxSelected(long accountId, long mailboxId, boolean navigate, 208 boolean dragDrop) { 209 if (dragDrop) { 210 // We don't want to change the message list for D&D. 211 212 // STOPSHIP fixit: the new mailbox list created here doesn't know D&D is in progress. 213 214 updateMailboxList(accountId, mailboxId, true, 215 false /* don't clear message list and message view */); 216 } else if (mailboxId == NO_MAILBOX) { 217 // reload the top-level message list. Always implies navigate. 218 openAccount(accountId); 219 } else if (navigate) { 220 if (mMailboxStack.isEmpty() || mailboxId != mMailboxListMailboxId) { 221 // Don't navigate to the same mailbox id twice in a row 222 mMailboxStack.push(mMailboxListMailboxId); 223 openMailbox(accountId, mailboxId); 224 } 225 } else { 226 updateMessageList(mailboxId, true, true); 227 } 228 } 229 230 @Override 231 public void onAccountSelected(long accountId) { 232 // TODO openAccount should do the check eventually, but it's necessary for now. 233 if (accountId != getUIAccountId()) { 234 openAccount(accountId); 235 } 236 } 237 238 @Override 239 public void onCurrentMailboxUpdated(long mailboxId, String mailboxName, int unreadCount) { 240 mCurrentMailboxName = mailboxName; 241 mCurrentMailboxUnreadCount = unreadCount; 242 refreshActionBar(); 243 } 244 245 // MessageListFragment$Callback 246 @Override 247 public void onMessageOpen(long messageId, long messageMailboxId, long listMailboxId, 248 int type) { 249 if (type == MessageListFragment.Callback.TYPE_DRAFT) { 250 MessageCompose.actionEditDraft(mActivity, messageId); 251 } else { 252 updateMessageView(messageId); 253 } 254 } 255 256 @Override 257 public void onEnterSelectionMode(boolean enter) { 258 } 259 260 /** 261 * Apply the auto-advance policy upon initation of a batch command that could potentially 262 * affect the currently selected conversation. 263 */ 264 @Override 265 public void onAdvancingOpAccepted(Set<Long> affectedMessages) { 266 if (!isMessageSelected()) { 267 // Do nothing if message view is not visible. 268 return; 269 } 270 271 int autoAdvanceDir = Preferences.getPreferences(mActivity).getAutoAdvanceDirection(); 272 if ((autoAdvanceDir == Preferences.AUTO_ADVANCE_MESSAGE_LIST) || (mOrderManager == null)) { 273 if (affectedMessages.contains(getMessageId())) { 274 goBackToMailbox(); 275 } 276 return; 277 } 278 279 // Navigate to the first unselected item in the appropriate direction. 280 switch (autoAdvanceDir) { 281 case Preferences.AUTO_ADVANCE_NEWER: 282 while (affectedMessages.contains(mOrderManager.getCurrentMessageId())) { 283 if (!mOrderManager.moveToNewer()) { 284 goBackToMailbox(); 285 return; 286 } 287 } 288 updateMessageView(mOrderManager.getCurrentMessageId()); 289 break; 290 291 case Preferences.AUTO_ADVANCE_OLDER: 292 while (affectedMessages.contains(mOrderManager.getCurrentMessageId())) { 293 if (!mOrderManager.moveToOlder()) { 294 goBackToMailbox(); 295 return; 296 } 297 } 298 updateMessageView(mOrderManager.getCurrentMessageId()); 299 break; 300 } 301 } 302 303 @Override 304 public void onListLoaded() { 305 } 306 307 // MessageViewFragment$Callback 308 @Override 309 public void onMessageViewShown(int mailboxType) { 310 updateMessageOrderManager(); 311 updateNavigationArrows(); 312 } 313 314 @Override 315 public void onMessageViewGone() { 316 stopMessageOrderManager(); 317 } 318 319 @Override 320 public boolean onUrlInMessageClicked(String url) { 321 return ActivityHelper.openUrlInMessage(mActivity, url, getActualAccountId()); 322 } 323 324 @Override 325 public void onMessageSetUnread() { 326 goBackToMailbox(); 327 } 328 329 @Override 330 public void onMessageNotExists() { 331 goBackToMailbox(); 332 } 333 334 @Override 335 public void onLoadMessageStarted() { 336 // TODO Any nice UI for this? 337 } 338 339 @Override 340 public void onLoadMessageFinished() { 341 // TODO Any nice UI for this? 342 } 343 344 @Override 345 public void onLoadMessageError(String errorMessage) { 346 } 347 348 @Override 349 public void onRespondedToInvite(int response) { 350 onCurrentMessageGone(); 351 } 352 353 @Override 354 public void onCalendarLinkClicked(long epochEventStartTime) { 355 ActivityHelper.openCalendar(mActivity, epochEventStartTime); 356 } 357 358 @Override 359 public void onBeforeMessageGone() { 360 onCurrentMessageGone(); 361 } 362 363 @Override 364 public void onForward() { 365 MessageCompose.actionForward(mActivity, getMessageId()); 366 } 367 368 @Override 369 public void onReply() { 370 MessageCompose.actionReply(mActivity, getMessageId(), false); 371 } 372 373 @Override 374 public void onReplyAll() { 375 MessageCompose.actionReply(mActivity, getMessageId(), true); 376 } 377 378 /** 379 * Must be called just after the activity sets up the content view. 380 * 381 * (Due to the complexity regarding class/activity initialization order, we can't do this in 382 * the constructor.) TODO this should no longer be true when we merge activities. 383 */ 384 @Override 385 public void onActivityViewReady() { 386 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 387 Log.d(Logging.LOG_TAG, this + " onActivityViewReady"); 388 } 389 mActionBarController = new ActionBarController(mActivity, mActivity.getLoaderManager(), 390 mActivity.getActionBar(), mActionBarControllerCallback); 391 392 // Set up content 393 mThreePane = (ThreePaneLayout) mActivity.findViewById(R.id.three_pane); 394 mThreePane.setCallback(this); 395 396 mMessageCommandButtons = mThreePane.getMessageCommandButtons(); 397 mMessageCommandButtons.setCallback(new CommandButtonCallback()); 398 } 399 400 /** 401 * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 402 * 403 * @see #getActualAccountId() 404 */ 405 @Override 406 public long getUIAccountId() { 407 return mAccountId; 408 } 409 410 /** 411 * @return the currently selected account ID. If the current view is the combined view, 412 * it'll return {@link #NO_ACCOUNT}. 413 * 414 * @see #getUIAccountId() 415 */ 416 @Override 417 public long getActualAccountId() { 418 return mAccountId == Account.ACCOUNT_ID_COMBINED_VIEW ? NO_ACCOUNT : mAccountId; 419 } 420 421 /** 422 * Returns the id of the mailbox used for the message list fragment. 423 * IMPORTANT: Do not confuse this with {@link #mMailboxListMailboxId} which is the id used 424 * for the mailbox list. The two may be different. 425 */ 426 private long getMessageListMailboxId() { 427 return (mMessageListFragment == null) 428 ? Mailbox.NO_MAILBOX 429 : mMessageListFragment.getMailboxId(); 430 } 431 432 /* 433 * STOPSHIP Remove this -- see the base class method. 434 */ 435 @Override 436 public long getMailboxSettingsMailboxId() { 437 return getMessageListMailboxId(); 438 } 439 440 /* 441 * STOPSHIP Remove this -- see the base class method. 442 */ 443 @Override 444 public long getSearchMailboxId() { 445 return getMessageListMailboxId(); 446 } 447 448 private long getMessageId() { 449 return mMessageId; 450 } 451 452 /** 453 * @return true if an account is selected, or the current view is the combined view. 454 */ 455 public boolean isAccountSelected() { 456 return getUIAccountId() != NO_ACCOUNT; 457 } 458 459 public boolean isMailboxSelected() { 460 return getMessageListMailboxId() != NO_MAILBOX; 461 } 462 463 public boolean isMessageSelected() { 464 return getMessageId() != NO_MESSAGE; 465 } 466 467 /** 468 * @return true if refresh is in progress for the current mailbox. 469 */ 470 public boolean isRefreshInProgress() { 471 long messageListMailboxId = getMessageListMailboxId(); 472 return (messageListMailboxId >= 0) 473 && mRefreshManager.isMessageListRefreshing(messageListMailboxId); 474 } 475 476 /** 477 * @return true if the UI should enable the "refresh" command. 478 */ 479 public boolean isRefreshEnabled() { 480 // - Don't show for combined inboxes, but 481 // - Show even for non-refreshable mailboxes, in which case we refresh the mailbox list 482 return -1 != getActualAccountId(); 483 } 484 485 /** 486 * Called by the host activity at the end of {@link Activity#onCreate}. 487 */ 488 @Override 489 public void onActivityCreated() { 490 super.onActivityCreated(); 491 mRefreshManager.registerListener(mRefreshListener); 492 mActionBarController.onActivityCreated(); 493 } 494 495 /** {@inheritDoc} */ 496 @Override 497 public void onActivityStart() { 498 super.onActivityStart(); 499 if (isMessageSelected()) { 500 updateMessageOrderManager(); 501 } 502 } 503 504 /** {@inheritDoc} */ 505 @Override 506 public void onActivityResume() { 507 super.onActivityResume(); 508 refreshActionBar(); 509 } 510 511 /** {@inheritDoc} */ 512 @Override 513 public void onActivityPause() { 514 super.onActivityPause(); 515 } 516 517 /** {@inheritDoc} */ 518 @Override 519 public void onActivityStop() { 520 stopMessageOrderManager(); 521 super.onActivityStop(); 522 } 523 524 /** {@inheritDoc} */ 525 @Override 526 public void onActivityDestroy() { 527 closeMailboxFinder(); 528 mRefreshManager.unregisterListener(mRefreshListener); 529 super.onActivityDestroy(); 530 } 531 532 /** {@inheritDoc} */ 533 @Override 534 public void onSaveInstanceState(Bundle outState) { 535 super.onSaveInstanceState(outState); 536 outState.putLong(BUNDLE_KEY_ACCOUNT_ID, mAccountId); 537 outState.putLong(BUNDLE_KEY_MAILBOX_ID, mMailboxListMailboxId); 538 outState.putLong(BUNDLE_KEY_MESSAGE_ID, mMessageId); 539 if (!mMailboxStack.isEmpty()) { 540 // Save the mailbox stack 541 long[] mailboxIds = Utility.toPrimitiveLongArray(mMailboxStack); 542 outState.putLongArray(BUNDLE_KEY_MAILBOX_STACK, mailboxIds); 543 } 544 } 545 546 /** {@inheritDoc} */ 547 @Override 548 public void restoreInstanceState(Bundle savedInstanceState) { 549 super.restoreInstanceState(savedInstanceState); 550 mAccountId = savedInstanceState.getLong(BUNDLE_KEY_ACCOUNT_ID, NO_ACCOUNT); 551 mMailboxListMailboxId = savedInstanceState.getLong(BUNDLE_KEY_MAILBOX_ID, NO_MAILBOX); 552 mMessageId = savedInstanceState.getLong(BUNDLE_KEY_MESSAGE_ID, NO_MESSAGE); 553 long[] mailboxIds = savedInstanceState.getLongArray(BUNDLE_KEY_MAILBOX_STACK); 554 if (mailboxIds != null) { 555 // Restore the mailbox stack; ugly hack to get around 'Long' versus 'long' 556 mMailboxStack.clear(); 557 for (long id : mailboxIds) { 558 mMailboxStack.push(id); 559 } 560 } 561 562 // STOPSHIP If MailboxFinder is still running, it needs restarting after loadState(). 563 // This probably means we need to start MailboxFinder if mMailboxId == -1. 564 } 565 566 @Override 567 void installFragment(Fragment fragment) { 568 super.installFragment(fragment); 569 if (fragment instanceof MailboxListFragment) { 570 mMailboxListFragment = (MailboxListFragment) fragment; 571 mMailboxListFragment.setCallback(this); 572 } else if (fragment instanceof MessageListFragment) { 573 mMessageListFragment = (MessageListFragment) fragment; 574 mMessageListFragment.setCallback(this); 575 } else if (fragment instanceof MessageViewFragment) { 576 mMessageViewFragment = (MessageViewFragment) fragment; 577 mMessageViewFragment.setCallback(this); 578 } else { 579 // Ignore -- uninteresting fragments such as dialogs. 580 } 581 } 582 583 private FragmentTransaction uninstallMailboxListFragment(FragmentTransaction ft) { 584 if (mMailboxListFragment != null) { 585 ft.remove(mMailboxListFragment); 586 mMailboxListFragment.setCallback(null); 587 mMailboxListFragment = null; 588 } 589 return ft; 590 } 591 592 private FragmentTransaction uninstallMessageListFragment(FragmentTransaction ft) { 593 if (mMessageListFragment != null) { 594 ft.remove(mMessageListFragment); 595 mMessageListFragment.setCallback(null); 596 mMessageListFragment = null; 597 } 598 return ft; 599 } 600 601 private FragmentTransaction uninstallMessageViewFragment(FragmentTransaction ft) { 602 if (mMessageViewFragment != null) { 603 ft.remove(mMessageViewFragment); 604 mMessageViewFragment.setCallback(null); 605 mMessageViewFragment = null; 606 } 607 return ft; 608 } 609 610 /** 611 * {@inheritDoc} 612 * 613 * On two-pane, it's the account's root mailboxes on the left pane with Inbox on the right pane. 614 */ 615 @Override 616 public void openAccount(long accountId) { 617 mMailboxStack.clear(); 618 open(accountId, NO_MAILBOX, NO_MESSAGE); 619 refreshActionBar(); 620 } 621 622 /** 623 * Opens the given mailbox. on two-pane, this will update both the mailbox list and the 624 * message list. 625 * 626 * NOTE: It's assumed that the mailbox is associated with the specified account. If the 627 * mailbox is not associated with the account, the behaviour is undefined. 628 */ 629 private void openMailbox(long accountId, long mailboxId) { 630 updateMailboxList(accountId, mailboxId, true, true); 631 updateMessageList(mailboxId, true, true); 632 refreshActionBar(); 633 } 634 635 /** 636 * {@inheritDoc} 637 */ 638 @Override 639 public void open(long accountId, long mailboxId, long messageId) { 640 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 641 Log.d(Logging.LOG_TAG, this + " open accountId=" + accountId 642 + " mailboxId=" + mailboxId + " messageId=" + messageId); 643 } 644 if (accountId == NO_ACCOUNT) { 645 throw new IllegalArgumentException(); 646 } else if (mailboxId == NO_MAILBOX) { 647 updateMailboxList(accountId, NO_MAILBOX, true, true); 648 649 // Show the appropriate message list 650 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 651 // When opening the Combined view, the right pane will be "combined inbox". 652 updateMessageList(Mailbox.QUERY_ALL_INBOXES, true, true); 653 } else { 654 // Try to find the inbox for the account 655 closeMailboxFinder(); 656 mMailboxFinder = new MailboxFinder(mActivity, mAccountId, Mailbox.TYPE_INBOX, this); 657 mMailboxFinder.startLookup(); 658 } 659 } else if (messageId == NO_MESSAGE) { 660 // STOPSHIP Use the appropriate parent mailbox ID 661 updateMailboxList(accountId, mailboxId, true, true); 662 updateMessageList(mailboxId, true, true); 663 } else { 664 // STOPSHIP Use the appropriate parent mailbox ID 665 updateMailboxList(accountId, mailboxId, false, true); 666 updateMessageList(mailboxId, false, true); 667 updateMessageView(messageId); 668 } 669 } 670 671 /** 672 * Pre-fragment transaction check. 673 * 674 * @throw IllegalStateException if updateXxx methods can't be called in the current state. 675 */ 676 private void preFragmentTransactionCheck() { 677 if (!isFragmentInstallable()) { 678 // Code assumes mMailboxListFragment/etc are set right within the 679 // commitFragmentTransaction() call (because we use synchronous transaction), 680 // so updateXxx() can't be called if fragments are not installable yet. 681 throw new IllegalStateException(); 682 } 683 } 684 685 /** 686 * Loads the given account and optionally selects the given mailbox and message. If the 687 * specified account is already selected, no actions will be performed unless 688 * <code>forceReload</code> is <code>true</code>. 689 * 690 * @param accountId ID of the account to load. Must never be {@link #NO_ACCOUNT}. 691 * @param parentMailboxId ID of the mailbox to use as the parent mailbox. Pass 692 * {@link #NO_MAILBOX} to show the root mailboxes. 693 * @param changeVisiblePane if true, the message view will be hidden. 694 * @param clearDependentPane if true, the message list and the message view will be cleared 695 */ 696 697 // TODO The name "updateMailboxList" is misleading, as it also updates members such as 698 // mAccountId. We need better structure but let's do that after refactoring 699 // MailboxListFragment.onMailboxSelected, and removed the UI callbacks such as 700 // TargetActivity.onAccountChanged. 701 702 private void updateMailboxList(long accountId, long parentMailboxId, 703 boolean changeVisiblePane, boolean clearDependentPane) { 704 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 705 Log.d(Logging.LOG_TAG, this + " updateMailboxList accountId=" + accountId 706 + " parentMailboxId=" + parentMailboxId); 707 } 708 preFragmentTransactionCheck(); 709 if (accountId == NO_ACCOUNT) { 710 throw new InvalidParameterException(); 711 } 712 713 // TODO Check if the current fragment has been initialized with the same parameters, and 714 // then return. 715 716 mAccountId = accountId; 717 mMailboxListMailboxId = parentMailboxId; 718 719 // Open mailbox list, remove message list / message view 720 final FragmentManager fm = mActivity.getFragmentManager(); 721 final FragmentTransaction ft = fm.beginTransaction(); 722 uninstallMailboxListFragment(ft); 723 if (clearDependentPane) { 724 mMessageId = NO_MESSAGE; 725 uninstallMessageListFragment(ft); 726 uninstallMessageViewFragment(ft); 727 } 728 ft.add(mThreePane.getLeftPaneId(), 729 MailboxListFragment.newInstance(getUIAccountId(), parentMailboxId)); 730 commitFragmentTransaction(ft); 731 732 if (changeVisiblePane) { 733 mThreePane.showLeftPane(); 734 } 735 updateRefreshProgress(); 736 } 737 738 /** 739 * Go back to a mailbox list view. If a message view is currently active, it will 740 * be hidden. 741 */ 742 private void goBackToMailbox() { 743 if (isMessageSelected()) { 744 mThreePane.showLeftPane(); // Show mailbox list 745 } 746 } 747 748 /** 749 * Selects the specified mailbox and optionally loads a message within it. If a message is 750 * not loaded, a list of the messages contained within the mailbox is shown. Otherwise the 751 * given message is shown. If <code>navigateToMailbox<code> is <code>true</code>, the 752 * mailbox is navigated to and any contained mailboxes are shown. 753 * 754 * @param mailboxId ID of the mailbox to load. Must never be <code>0</code> or <code>-1</code>. 755 * @param changeVisiblePane if true, the message view will be hidden. 756 * @param clearDependentPane if true, the message view will be cleared 757 */ 758 private void updateMessageList(long mailboxId, boolean changeVisiblePane, 759 boolean clearDependentPane) { 760 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 761 Log.d(Logging.LOG_TAG, this + " updateMessageList mMailboxId=" + mailboxId); 762 } 763 preFragmentTransactionCheck(); 764 if (mailboxId == 0 || mailboxId == -1) { 765 throw new InvalidParameterException(); 766 } 767 768 // TODO Check if the current fragment has been initialized with the same parameters, and 769 // then return. 770 771 final FragmentManager fm = mActivity.getFragmentManager(); 772 final FragmentTransaction ft = fm.beginTransaction(); 773 uninstallMessageListFragment(ft); 774 if (clearDependentPane) { 775 uninstallMessageViewFragment(ft); 776 mMessageId = NO_MESSAGE; 777 } 778 ft.add(mThreePane.getMiddlePaneId(), MessageListFragment.newInstance(mailboxId)); 779 commitFragmentTransaction(ft); 780 781 if (changeVisiblePane) { 782 mThreePane.showLeftPane(); 783 } 784 785 // TODO We shouldn't select the mailbox when we're updating the message list. These two 786 // functions should be done separately. Find a better location for this call to be done. 787 mMailboxListFragment.setSelectedMailbox(mailboxId); 788 updateRefreshProgress(); 789 } 790 791 /** 792 * Show a message on the message view. 793 * 794 * @param messageId ID of the mailbox to load. Must never be {@link #NO_MESSAGE}. 795 */ 796 private void updateMessageView(long messageId) { 797 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 798 Log.d(Logging.LOG_TAG, this + " updateMessageView messageId=" + messageId); 799 } 800 preFragmentTransactionCheck(); 801 if (messageId == NO_MESSAGE) { 802 throw new InvalidParameterException(); 803 } 804 805 // TODO Check if the current fragment has been initialized with the same parameters, and 806 // then return. 807 808 // Update member 809 mMessageId = messageId; 810 811 // Open message 812 final FragmentManager fm = mActivity.getFragmentManager(); 813 final FragmentTransaction ft = fm.beginTransaction(); 814 uninstallMessageViewFragment(ft); 815 ft.add(mThreePane.getRightPaneId(), MessageViewFragment.newInstance(messageId)); 816 commitFragmentTransaction(ft); 817 818 mThreePane.showRightPane(); // Show message view 819 820 mMessageListFragment.setSelectedMessage(mMessageId); 821 } 822 823 private void closeMailboxFinder() { 824 if (mMailboxFinder != null) { 825 mMailboxFinder.cancel(); 826 mMailboxFinder = null; 827 } 828 } 829 830 private class CommandButtonCallback implements MessageCommandButtonView.Callback { 831 @Override 832 public void onMoveToNewer() { 833 moveToNewer(); 834 } 835 836 @Override 837 public void onMoveToOlder() { 838 moveToOlder(); 839 } 840 } 841 842 private void onCurrentMessageGone() { 843 switch (Preferences.getPreferences(mActivity).getAutoAdvanceDirection()) { 844 case Preferences.AUTO_ADVANCE_NEWER: 845 if (moveToNewer()) return; 846 break; 847 case Preferences.AUTO_ADVANCE_OLDER: 848 if (moveToOlder()) return; 849 break; 850 } 851 // Last message in the box or AUTO_ADVANCE_MESSAGE_LIST. Go back to message list. 852 goBackToMailbox(); 853 } 854 855 /** 856 * Potentially create a new {@link MessageOrderManager}; if it's not already started or if 857 * the account has changed, and sync it to the current message. 858 */ 859 private void updateMessageOrderManager() { 860 if (!isMailboxSelected()) { 861 return; 862 } 863 final long mailboxId = getMessageListMailboxId(); 864 if (mOrderManager == null || mOrderManager.getMailboxId() != mailboxId) { 865 stopMessageOrderManager(); 866 mOrderManager = 867 new MessageOrderManager(mActivity, mailboxId, mMessageOrderManagerCallback); 868 } 869 if (isMessageSelected()) { 870 mOrderManager.moveTo(getMessageId()); 871 } 872 } 873 874 private class MessageOrderManagerCallback implements MessageOrderManager.Callback { 875 @Override 876 public void onMessagesChanged() { 877 updateNavigationArrows(); 878 } 879 880 @Override 881 public void onMessageNotFound() { 882 // Current message gone. 883 goBackToMailbox(); 884 } 885 } 886 887 /** 888 * Stop {@link MessageOrderManager}. 889 */ 890 private void stopMessageOrderManager() { 891 if (mOrderManager != null) { 892 mOrderManager.close(); 893 mOrderManager = null; 894 } 895 } 896 897 /** 898 * Disable/enable the move-to-newer/older buttons. 899 */ 900 private void updateNavigationArrows() { 901 if (mOrderManager == null) { 902 // shouldn't happen, but just in case 903 mMessageCommandButtons.enableNavigationButtons(false, false, 0, 0); 904 } else { 905 mMessageCommandButtons.enableNavigationButtons( 906 mOrderManager.canMoveToNewer(), mOrderManager.canMoveToOlder(), 907 mOrderManager.getCurrentPosition(), mOrderManager.getTotalMessageCount()); 908 } 909 } 910 911 private boolean moveToOlder() { 912 if ((mOrderManager != null) && mOrderManager.moveToOlder()) { 913 updateMessageView(mOrderManager.getCurrentMessageId()); 914 return true; 915 } 916 return false; 917 } 918 919 private boolean moveToNewer() { 920 if ((mOrderManager != null) && mOrderManager.moveToNewer()) { 921 updateMessageView(mOrderManager.getCurrentMessageId()); 922 return true; 923 } 924 return false; 925 } 926 927 /** 928 * Handles {@link android.app.Activity#onCreateOptionsMenu} callback. 929 */ 930 @Override 931 public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) { 932 inflater.inflate(R.menu.email_activity_options, menu); 933 return true; 934 } 935 936 /** {@inheritDoc} */ 937 @Override 938 public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) { 939 ActivityHelper.updateRefreshMenuIcon(menu.findItem(R.id.refresh), 940 isRefreshEnabled(), 941 isRefreshInProgress()); 942 return true; 943 } 944 945 /** {@inheritDoc} */ 946 @Override 947 public boolean onOptionsItemSelected(MenuItem item) { 948 switch (item.getItemId()) { 949 case android.R.id.home: 950 // Comes from the action bar when the app icon on the left is pressed. 951 // It works like a back press, but it won't close the activity. 952 return onBackPressed(false); 953 case R.id.compose: 954 return onCompose(); 955 case R.id.refresh: 956 onRefresh(); 957 return true; 958 case R.id.account_settings: 959 return onAccountSettings(); 960 } 961 return false; 962 } 963 964 /** {@inheritDoc} */ 965 @Override 966 public boolean onBackPressed(boolean isSystemBackKey) { 967 if (mThreePane.onBackPressed(isSystemBackKey)) { 968 return true; 969 } else if (!mMailboxStack.isEmpty()) { 970 long mailboxId = mMailboxStack.pop(); 971 if (mailboxId == NO_MAILBOX) { 972 // No mailbox; reload the top-level message list 973 openAccount(mAccountId); 974 } else { 975 openMailbox(mAccountId, mailboxId); 976 } 977 return true; 978 } 979 return false; 980 } 981 982 /** 983 * Handles the "Compose" option item. Opens the message compose activity. 984 */ 985 private boolean onCompose() { 986 if (!isAccountSelected()) { 987 return false; // this shouldn't really happen 988 } 989 MessageCompose.actionCompose(mActivity, getActualAccountId()); 990 return true; 991 } 992 993 /** 994 * Handles the "Compose" option item. Opens the settings activity. 995 */ 996 private boolean onAccountSettings() { 997 AccountSettingsXL.actionSettings(mActivity, getActualAccountId()); 998 return true; 999 } 1000 1001 /** 1002 * Handles the "refresh" option item. Opens the settings activity. 1003 * TODO used by experimental code in the activity -- otherwise can be private. 1004 */ 1005 @Override 1006 public void onRefresh() { 1007 // Cancel previously running instance if any. 1008 new RefreshTask(mTaskTracker, mActivity, getActualAccountId(), 1009 getMessageListMailboxId()).cancelPreviousAndExecuteParallel(); 1010 } 1011 1012 /** 1013 * Start/stop the "refresh" animation on the action bar according to the current refresh state. 1014 * 1015 * (We start the animation if {@link UIControllerTwoPane#isRefreshInProgress} returns true, 1016 * and stop otherwise.) 1017 */ 1018 private void updateRefreshProgress() { 1019 mActivity.invalidateOptionsMenu(); 1020 } 1021 1022 private class RefreshListener 1023 implements RefreshManager.Listener { 1024 @Override 1025 public void onMessagingError(final long accountId, long mailboxId, final String message) { 1026 updateRefreshProgress(); 1027 } 1028 1029 @Override 1030 public void onRefreshStatusChanged(long accountId, long mailboxId) { 1031 updateRefreshProgress(); 1032 } 1033 } 1034 1035 /** 1036 * Class to handle refresh. 1037 * 1038 * When the user press "refresh", 1039 * <ul> 1040 * <li>Refresh the current mailbox, if it's refreshable. (e.g. don't refresh combined inbox, 1041 * drafts, etc. 1042 * <li>Refresh the mailbox list, if it hasn't been refreshed in the last 1043 * {@link #MAILBOX_REFRESH_MIN_INTERVAL}. 1044 * <li>Refresh inbox, if it's not the current mailbox and it hasn't been refreshed in the last 1045 * {@link #INBOX_AUTO_REFRESH_MIN_INTERVAL}. 1046 * </ul> 1047 */ 1048 /* package */ static class RefreshTask extends EmailAsyncTask<Void, Void, Boolean> { 1049 private final Clock mClock; 1050 private final Context mContext; 1051 private final long mAccountId; 1052 private final long mMailboxId; 1053 private final RefreshManager mRefreshManager; 1054 /* package */ long mInboxId; 1055 1056 public RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId, 1057 long mailboxId) { 1058 this(tracker, context, accountId, mailboxId, Clock.INSTANCE, 1059 RefreshManager.getInstance(context)); 1060 } 1061 1062 /* package */ RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId, 1063 long mailboxId, Clock clock, RefreshManager refreshManager) { 1064 super(tracker); 1065 mClock = clock; 1066 mContext = context; 1067 mRefreshManager = refreshManager; 1068 mAccountId = accountId; 1069 mMailboxId = mailboxId; 1070 } 1071 1072 /** 1073 * Do DB access on a worker thread. 1074 */ 1075 @Override 1076 protected Boolean doInBackground(Void... params) { 1077 mInboxId = Account.getInboxId(mContext, mAccountId); 1078 return Mailbox.isRefreshable(mContext, mMailboxId); 1079 } 1080 1081 /** 1082 * Do the actual refresh. 1083 */ 1084 @Override 1085 protected void onPostExecute(Boolean isCurrentMailboxRefreshable) { 1086 if (isCancelled() || isCurrentMailboxRefreshable == null) { 1087 return; 1088 } 1089 if (isCurrentMailboxRefreshable) { 1090 mRefreshManager.refreshMessageList(mAccountId, mMailboxId, false); 1091 } 1092 // Refresh mailbox list 1093 if (mAccountId != -1) { 1094 if (shouldRefreshMailboxList()) { 1095 mRefreshManager.refreshMailboxList(mAccountId); 1096 } 1097 } 1098 // Refresh inbox 1099 if (shouldAutoRefreshInbox()) { 1100 mRefreshManager.refreshMessageList(mAccountId, mInboxId, false); 1101 } 1102 } 1103 1104 /** 1105 * @return true if the mailbox list of the current account hasn't been refreshed 1106 * in the last {@link #MAILBOX_REFRESH_MIN_INTERVAL}. 1107 */ 1108 /* package */ boolean shouldRefreshMailboxList() { 1109 if (mRefreshManager.isMailboxListRefreshing(mAccountId)) { 1110 return false; 1111 } 1112 final long nextRefreshTime = mRefreshManager.getLastMailboxListRefreshTime(mAccountId) 1113 + MAILBOX_REFRESH_MIN_INTERVAL; 1114 if (nextRefreshTime > mClock.getTime()) { 1115 return false; 1116 } 1117 return true; 1118 } 1119 1120 /** 1121 * @return true if the inbox of the current account hasn't been refreshed 1122 * in the last {@link #INBOX_AUTO_REFRESH_MIN_INTERVAL}. 1123 */ 1124 /* package */ boolean shouldAutoRefreshInbox() { 1125 if (mInboxId == mMailboxId) { 1126 return false; // Current ID == inbox. No need to auto-refresh. 1127 } 1128 if (mRefreshManager.isMessageListRefreshing(mInboxId)) { 1129 return false; 1130 } 1131 final long nextRefreshTime = mRefreshManager.getLastMessageListRefreshTime(mInboxId) 1132 + INBOX_AUTO_REFRESH_MIN_INTERVAL; 1133 if (nextRefreshTime > mClock.getTime()) { 1134 return false; 1135 } 1136 return true; 1137 } 1138 } 1139 1140 private class ActionBarControllerCallback implements ActionBarController.Callback { 1141 @Override 1142 public String getCurrentMailboxName() { 1143 return mCurrentMailboxName; 1144 } 1145 1146 @Override 1147 public int getCurrentMailboxUnreadCount() { 1148 return mCurrentMailboxUnreadCount; 1149 } 1150 1151 @Override 1152 public long getUIAccountId() { 1153 return UIControllerTwoPane.this.getUIAccountId(); 1154 } 1155 1156 @Override 1157 public boolean isAccountSelected() { 1158 return UIControllerTwoPane.this.isAccountSelected(); 1159 } 1160 1161 @Override 1162 public void onAccountSelected(long accountId) { 1163 openAccount(accountId); 1164 } 1165 1166 @Override 1167 public void onNoAccountsFound() { 1168 Welcome.actionStart(mActivity); 1169 mActivity.finish(); 1170 } 1171 1172 @Override 1173 public boolean shouldShowMailboxName() { 1174 // Show when the left pane is hidden. 1175 return (mThreePane.getVisiblePanes() & ThreePaneLayout.PANE_LEFT) == 0; 1176 } 1177 1178 @Override 1179 public boolean shouldShowUp() { 1180 final int visiblePanes = mThreePane.getVisiblePanes(); 1181 final boolean leftPaneHidden = ((visiblePanes & ThreePaneLayout.PANE_LEFT) == 0); 1182 return leftPaneHidden || !mMailboxStack.isEmpty(); 1183 } 1184 } 1185} 1186