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