UIControllerTwoPane.java revision 1ddc2ca4a7e59189099d273bd1d364838dd7ce35
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 android.app.Activity; 20import android.app.FragmentTransaction; 21import android.content.Context; 22import android.os.Bundle; 23import android.util.Log; 24 25import com.android.email.Clock; 26import com.android.email.Email; 27import com.android.email.MessageListContext; 28import com.android.email.Preferences; 29import com.android.email.R; 30import com.android.email.RefreshManager; 31import com.android.emailcommon.Logging; 32import com.android.emailcommon.provider.Account; 33import com.android.emailcommon.provider.EmailContent.Message; 34import com.android.emailcommon.provider.Mailbox; 35import com.android.emailcommon.utility.EmailAsyncTask; 36import com.google.common.annotations.VisibleForTesting; 37 38import java.util.Set; 39 40/** 41 * UI Controller for x-large devices. Supports a multi-pane layout. 42 * 43 * Note: Always use {@link #commitFragmentTransaction} to operate fragment transactions, 44 * so that we can easily switch between synchronous and asynchronous transactions. 45 */ 46class UIControllerTwoPane extends UIControllerBase implements ThreePaneLayout.Callback { 47 @VisibleForTesting 48 static final int MAILBOX_REFRESH_MIN_INTERVAL = 30 * 1000; // in milliseconds 49 50 @VisibleForTesting 51 static final int INBOX_AUTO_REFRESH_MIN_INTERVAL = 10 * 1000; // in milliseconds 52 53 // Other UI elements 54 private ThreePaneLayout mThreePane; 55 56 private MessageCommandButtonView mMessageCommandButtons; 57 58 public UIControllerTwoPane(EmailActivity activity) { 59 super(activity); 60 } 61 62 @Override 63 public int getLayoutId() { 64 return R.layout.email_activity_two_pane; 65 } 66 67 // ThreePaneLayoutCallback 68 @Override 69 public void onVisiblePanesChanged(int previousVisiblePanes) { 70 // If the right pane is gone, remove the message view. 71 final int visiblePanes = mThreePane.getVisiblePanes(); 72 73 if (((visiblePanes & ThreePaneLayout.PANE_RIGHT) == 0) && 74 ((previousVisiblePanes & ThreePaneLayout.PANE_RIGHT) != 0)) { 75 // Message view just got hidden 76 unselectMessage(); 77 } 78 // Disable CAB when the message list is not visible. 79 if (isMessageListInstalled()) { 80 getMessageListFragment().onHidden((visiblePanes & ThreePaneLayout.PANE_MIDDLE) == 0); 81 } 82 refreshActionBar(); 83 } 84 85 // MailboxListFragment$Callback 86 @Override 87 public void onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation) { 88 setListContext(MessageListContext.forMailbox(accountId, mailboxId)); 89 if (getMessageListMailboxId() != mListContext.getMailboxId()) { 90 updateMessageList(true); 91 } 92 } 93 94 // MailboxListFragment$Callback 95 @Override 96 public void onAccountSelected(long accountId) { 97 // It's from combined view, so "forceShowInbox" doesn't really matter. 98 // (We're always switching accounts.) 99 switchAccount(accountId, true); 100 } 101 102 // MailboxListFragment$Callback 103 @Override 104 public void onParentMailboxChanged() { 105 refreshActionBar(); 106 } 107 108 // MessageListFragment$Callback 109 @Override 110 public void onMessageOpen(long messageId, long messageMailboxId, long listMailboxId, 111 int type) { 112 if (type == MessageListFragment.Callback.TYPE_DRAFT) { 113 MessageCompose.actionEditDraft(mActivity, messageId); 114 } else { 115 if (getMessageId() != messageId) { 116 navigateToMessage(messageId); 117 mThreePane.showRightPane(); 118 } 119 } 120 } 121 122 // MessageListFragment$Callback 123 @Override 124 public void onMailboxNotFound() { 125 Log.e(Logging.LOG_TAG, "unable to find mailbox"); 126 } 127 128 // MessageListFragment$Callback 129 @Override 130 public void onEnterSelectionMode(boolean enter) { 131 } 132 133 // MessageListFragment$Callback 134 /** 135 * Apply the auto-advance policy upon initation of a batch command that could potentially 136 * affect the currently selected conversation. 137 */ 138 @Override 139 public void onAdvancingOpAccepted(Set<Long> affectedMessages) { 140 if (!isMessageViewInstalled()) { 141 // Do nothing if message view is not visible. 142 return; 143 } 144 145 final MessageOrderManager orderManager = getMessageOrderManager(); 146 int autoAdvanceDir = Preferences.getPreferences(mActivity).getAutoAdvanceDirection(); 147 if ((autoAdvanceDir == Preferences.AUTO_ADVANCE_MESSAGE_LIST) || (orderManager == null)) { 148 if (affectedMessages.contains(getMessageId())) { 149 goBackToMailbox(); 150 } 151 return; 152 } 153 154 // Navigate to the first unselected item in the appropriate direction. 155 switch (autoAdvanceDir) { 156 case Preferences.AUTO_ADVANCE_NEWER: 157 while (affectedMessages.contains(orderManager.getCurrentMessageId())) { 158 if (!orderManager.moveToNewer()) { 159 goBackToMailbox(); 160 return; 161 } 162 } 163 navigateToMessage(orderManager.getCurrentMessageId()); 164 break; 165 166 case Preferences.AUTO_ADVANCE_OLDER: 167 while (affectedMessages.contains(orderManager.getCurrentMessageId())) { 168 if (!orderManager.moveToOlder()) { 169 goBackToMailbox(); 170 return; 171 } 172 } 173 navigateToMessage(orderManager.getCurrentMessageId()); 174 break; 175 } 176 } 177 178 // MessageListFragment$Callback 179 @Override 180 public void onListLoaded() { 181 } 182 183 // MessageListFragment$Callback 184 @Override 185 public boolean onDragStarted() { 186 Log.w(Logging.LOG_TAG, "Drag started"); 187 188 if (((mListContext != null) && mListContext.isSearch()) 189 || !mThreePane.isLeftPaneVisible()) { 190 // D&D not allowed. 191 return false; 192 } 193 194 // STOPSHIP Save the current mailbox list 195 196 return true; 197 } 198 199 // MessageListFragment$Callback 200 @Override 201 public void onDragEnded() { 202 Log.w(Logging.LOG_TAG, "Drag ended"); 203 204 // STOPSHIP Restore the saved mailbox list 205 } 206 207 208 // MessageViewFragment$Callback 209 @Override 210 public boolean onUrlInMessageClicked(String url) { 211 return ActivityHelper.openUrlInMessage(mActivity, url, getActualAccountId()); 212 } 213 214 // MessageViewFragment$Callback 215 @Override 216 public void onLoadMessageStarted() { 217 } 218 219 // MessageViewFragment$Callback 220 @Override 221 public void onLoadMessageFinished() { 222 } 223 224 // MessageViewFragment$Callback 225 @Override 226 public void onLoadMessageError(String errorMessage) { 227 } 228 229 // MessageViewFragment$Callback 230 @Override 231 public void onCalendarLinkClicked(long epochEventStartTime) { 232 ActivityHelper.openCalendar(mActivity, epochEventStartTime); 233 } 234 235 // MessageViewFragment$Callback 236 @Override 237 public void onForward() { 238 MessageCompose.actionForward(mActivity, getMessageId()); 239 } 240 241 // MessageViewFragment$Callback 242 @Override 243 public void onReply() { 244 MessageCompose.actionReply(mActivity, getMessageId(), false); 245 } 246 247 // MessageViewFragment$Callback 248 @Override 249 public void onReplyAll() { 250 MessageCompose.actionReply(mActivity, getMessageId(), true); 251 } 252 253 /** 254 * Must be called just after the activity sets up the content view. 255 */ 256 @Override 257 public void onActivityViewReady() { 258 super.onActivityViewReady(); 259 260 // Set up content 261 mThreePane = (ThreePaneLayout) mActivity.findViewById(R.id.three_pane); 262 mThreePane.setCallback(this); 263 264 mMessageCommandButtons = mThreePane.getMessageCommandButtons(); 265 mMessageCommandButtons.setCallback(new CommandButtonCallback()); 266 } 267 268 @Override 269 protected ActionBarController createActionBarController(Activity activity) { 270 return new ActionBarController(activity, activity.getLoaderManager(), 271 activity.getActionBar(), new ActionBarControllerCallback()); 272 } 273 274 /** 275 * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 276 * 277 * @see #getActualAccountId() 278 */ 279 @Override 280 public long getUIAccountId() { 281 return isMailboxListInstalled() ? getMailboxListFragment().getAccountId() 282 :Account.NO_ACCOUNT; 283 } 284 285 @Override 286 public long getMailboxSettingsMailboxId() { 287 return getMessageListMailboxId(); 288 } 289 290 /** 291 * @return true if refresh is in progress for the current mailbox. 292 */ 293 @Override 294 protected boolean isRefreshInProgress() { 295 long messageListMailboxId = getMessageListMailboxId(); 296 return (messageListMailboxId >= 0) 297 && mRefreshManager.isMessageListRefreshing(messageListMailboxId); 298 } 299 300 /** 301 * @return true if the UI should enable the "refresh" command. 302 */ 303 @Override 304 protected boolean isRefreshEnabled() { 305 // - Don't show for combined inboxes, but 306 // - Show even for non-refreshable mailboxes, in which case we refresh the mailbox list 307 return getActualAccountId() != Account.NO_ACCOUNT; 308 } 309 310 311 /** {@inheritDoc} */ 312 @Override 313 public void onSaveInstanceState(Bundle outState) { 314 super.onSaveInstanceState(outState); 315 } 316 317 /** {@inheritDoc} */ 318 @Override 319 public void onRestoreInstanceState(Bundle savedInstanceState) { 320 super.onRestoreInstanceState(savedInstanceState); 321 } 322 323 @Override 324 protected void installMessageListFragment(MessageListFragment fragment) { 325 super.installMessageListFragment(fragment); 326 327 if (isMailboxListInstalled()) { 328 getMailboxListFragment().setHighlightedMailbox(fragment.getMailboxId()); 329 } 330 } 331 332 @Override 333 protected void installMessageViewFragment(MessageViewFragment fragment) { 334 super.installMessageViewFragment(fragment); 335 336 if (isMessageListInstalled()) { 337 getMessageListFragment().setSelectedMessage(fragment.getMessageId()); 338 } 339 } 340 341 342 /** 343 * Commit a {@link FragmentTransaction}. 344 */ 345 private void commitFragmentTransaction(FragmentTransaction ft) { 346 if (DEBUG_FRAGMENTS) { 347 Log.d(Logging.LOG_TAG, this + " commitFragmentTransaction: " + ft); 348 } 349 if (!ft.isEmpty()) { 350 // STOPSHIP Don't use AllowingStateLoss. See b/4519430 351 ft.commitAllowingStateLoss(); 352 mFragmentManager.executePendingTransactions(); 353 } 354 } 355 356 @Override 357 public void openInternal(final MessageListContext listContext, final long messageId) { 358 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 359 Log.d(Logging.LOG_TAG, this + " open " + listContext); 360 } 361 362 final FragmentTransaction ft = mFragmentManager.beginTransaction(); 363 updateMailboxList(ft, true); 364 updateMessageList(ft, true); 365 366 if (messageId != Message.NO_MESSAGE) { 367 updateMessageView(ft, messageId); 368 mThreePane.showRightPane(); 369 } else if (mListContext.isSearch()) { 370 mThreePane.showRightPane(); 371 } else { 372 mThreePane.showLeftPane(); 373 } 374 commitFragmentTransaction(ft); 375 } 376 377 /** 378 * Loads the given account and optionally selects the given mailbox and message. If the 379 * specified account is already selected, no actions will be performed unless 380 * <code>forceReload</code> is <code>true</code>. 381 * 382 * @param ft {@link FragmentTransaction} to use. 383 * @param clearDependentPane if true, the message list and the message view will be cleared 384 */ 385 private void updateMailboxList(FragmentTransaction ft, boolean clearDependentPane) { 386 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 387 Log.d(Logging.LOG_TAG, this + " updateMailboxList " + mListContext); 388 } 389 390 long accountId = mListContext.mAccountId; 391 long mailboxId = mListContext.getMailboxId(); 392 if ((getUIAccountId() != accountId) || (getMailboxListMailboxId() != mailboxId)) { 393 removeMailboxListFragment(ft); 394 ft.add(mThreePane.getLeftPaneId(), 395 MailboxListFragment.newInstance(accountId, mailboxId, true)); 396 } 397 if (clearDependentPane) { 398 removeMessageListFragment(ft); 399 removeMessageViewFragment(ft); 400 } 401 } 402 403 /** 404 * Go back to a mailbox list view. If a message view is currently active, it will 405 * be hidden. 406 */ 407 private void goBackToMailbox() { 408 if (isMessageViewInstalled()) { 409 mThreePane.showLeftPane(); // Show mailbox list 410 } 411 } 412 413 /** 414 * Show the message list fragment for the given mailbox. 415 * 416 * @param ft {@link FragmentTransaction} to use. 417 */ 418 private void updateMessageList(FragmentTransaction ft, boolean clearDependentPane) { 419 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 420 Log.d(Logging.LOG_TAG, this + " updateMessageList " + mListContext); 421 } 422 423 if (mListContext.getMailboxId() != getMessageListMailboxId()) { 424 removeMessageListFragment(ft); 425 ft.add(mThreePane.getMiddlePaneId(), MessageListFragment.newInstance(mListContext)); 426 } 427 if (clearDependentPane) { 428 removeMessageViewFragment(ft); 429 } 430 } 431 432 /** 433 * Shortcut to call {@link #updateMessageList(FragmentTransaction, boolean)} and 434 * commit. 435 */ 436 private void updateMessageList(boolean clearDependentPane) { 437 FragmentTransaction ft = mFragmentManager.beginTransaction(); 438 updateMessageList(ft, clearDependentPane); 439 commitFragmentTransaction(ft); 440 } 441 442 /** 443 * Show a message on the message view. 444 * 445 * @param ft {@link FragmentTransaction} to use. 446 * @param messageId ID of the mailbox to load. Must never be {@link Message#NO_MESSAGE}. 447 */ 448 private void updateMessageView(FragmentTransaction ft, long messageId) { 449 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 450 Log.d(Logging.LOG_TAG, this + " updateMessageView messageId=" + messageId); 451 } 452 if (messageId == Message.NO_MESSAGE) { 453 throw new IllegalArgumentException(); 454 } 455 456 if (messageId == getMessageId()) { 457 return; // nothing to do. 458 } 459 460 removeMessageViewFragment(ft); 461 462 ft.add(mThreePane.getRightPaneId(), MessageViewFragment.newInstance(messageId)); 463 } 464 465 /** 466 * Shortcut to call {@link #updateMessageView(FragmentTransaction, long)} and commit. 467 */ 468 @Override protected void navigateToMessage(long messageId) { 469 FragmentTransaction ft = mFragmentManager.beginTransaction(); 470 updateMessageView(ft, messageId); 471 commitFragmentTransaction(ft); 472 } 473 474 /** 475 * Remove the message view if shown. 476 */ 477 private void unselectMessage() { 478 commitFragmentTransaction(removeMessageViewFragment(mFragmentManager.beginTransaction())); 479 if (isMessageListInstalled()) { 480 getMessageListFragment().setSelectedMessage(Message.NO_MESSAGE); 481 } 482 stopMessageOrderManager(); 483 } 484 485 private class CommandButtonCallback implements MessageCommandButtonView.Callback { 486 @Override 487 public void onMoveToNewer() { 488 moveToNewer(); 489 } 490 491 @Override 492 public void onMoveToOlder() { 493 moveToOlder(); 494 } 495 } 496 497 /** 498 * Disable/enable the move-to-newer/older buttons. 499 */ 500 @Override protected void updateNavigationArrows() { 501 final MessageOrderManager orderManager = getMessageOrderManager(); 502 if (orderManager == null) { 503 // shouldn't happen, but just in case 504 mMessageCommandButtons.enableNavigationButtons(false, false, 0, 0); 505 } else { 506 mMessageCommandButtons.enableNavigationButtons( 507 orderManager.canMoveToNewer(), orderManager.canMoveToOlder(), 508 orderManager.getCurrentPosition(), orderManager.getTotalMessageCount()); 509 } 510 } 511 512 /** {@inheritDoc} */ 513 @Override 514 public boolean onBackPressed(boolean isSystemBackKey) { 515 if (!mThreePane.isPaneCollapsible()) { 516 if (mActionBarController.onBackPressed(isSystemBackKey)) { 517 return true; 518 } 519 520 if (mThreePane.showLeftPane()) { 521 return true; 522 } 523 } else { 524 // If it's not the system back key, always attempt to uncollapse the left pane first. 525 if (!isSystemBackKey && mThreePane.uncollapsePane()) { 526 return true; 527 } 528 529 if (mActionBarController.onBackPressed(isSystemBackKey)) { 530 return true; 531 } 532 533 if (mThreePane.showLeftPane()) { 534 return true; 535 } 536 } 537 538 if (isMailboxListInstalled() && getMailboxListFragment().navigateUp()) { 539 return true; 540 } 541 return false; 542 } 543 544 @Override 545 protected void onRefresh() { 546 // Cancel previously running instance if any. 547 new RefreshTask(mTaskTracker, mActivity, getActualAccountId(), 548 getMessageListMailboxId()).cancelPreviousAndExecuteParallel(); 549 } 550 551 /** 552 * Class to handle refresh. 553 * 554 * When the user press "refresh", 555 * <ul> 556 * <li>Refresh the current mailbox, if it's refreshable. (e.g. don't refresh combined inbox, 557 * drafts, etc. 558 * <li>Refresh the mailbox list, if it hasn't been refreshed in the last 559 * {@link #MAILBOX_REFRESH_MIN_INTERVAL}. 560 * <li>Refresh inbox, if it's not the current mailbox and it hasn't been refreshed in the last 561 * {@link #INBOX_AUTO_REFRESH_MIN_INTERVAL}. 562 * </ul> 563 */ 564 @VisibleForTesting 565 static class RefreshTask extends EmailAsyncTask<Void, Void, Boolean> { 566 private final Clock mClock; 567 private final Context mContext; 568 private final long mAccountId; 569 private final long mMailboxId; 570 private final RefreshManager mRefreshManager; 571 @VisibleForTesting 572 long mInboxId; 573 574 public RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId, 575 long mailboxId) { 576 this(tracker, context, accountId, mailboxId, Clock.INSTANCE, 577 RefreshManager.getInstance(context)); 578 } 579 580 @VisibleForTesting 581 RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId, 582 long mailboxId, Clock clock, RefreshManager refreshManager) { 583 super(tracker); 584 mClock = clock; 585 mContext = context; 586 mRefreshManager = refreshManager; 587 mAccountId = accountId; 588 mMailboxId = mailboxId; 589 } 590 591 /** 592 * Do DB access on a worker thread. 593 */ 594 @Override 595 protected Boolean doInBackground(Void... params) { 596 mInboxId = Account.getInboxId(mContext, mAccountId); 597 return Mailbox.isRefreshable(mContext, mMailboxId); 598 } 599 600 /** 601 * Do the actual refresh. 602 */ 603 @Override 604 protected void onSuccess(Boolean isCurrentMailboxRefreshable) { 605 if (isCurrentMailboxRefreshable == null) { 606 return; 607 } 608 if (isCurrentMailboxRefreshable) { 609 mRefreshManager.refreshMessageList(mAccountId, mMailboxId, false); 610 } 611 // Refresh mailbox list 612 if (mAccountId != Account.NO_ACCOUNT) { 613 if (shouldRefreshMailboxList()) { 614 mRefreshManager.refreshMailboxList(mAccountId); 615 } 616 } 617 // Refresh inbox 618 if (shouldAutoRefreshInbox()) { 619 mRefreshManager.refreshMessageList(mAccountId, mInboxId, false); 620 } 621 } 622 623 /** 624 * @return true if the mailbox list of the current account hasn't been refreshed 625 * in the last {@link #MAILBOX_REFRESH_MIN_INTERVAL}. 626 */ 627 @VisibleForTesting 628 boolean shouldRefreshMailboxList() { 629 if (mRefreshManager.isMailboxListRefreshing(mAccountId)) { 630 return false; 631 } 632 final long nextRefreshTime = mRefreshManager.getLastMailboxListRefreshTime(mAccountId) 633 + MAILBOX_REFRESH_MIN_INTERVAL; 634 if (nextRefreshTime > mClock.getTime()) { 635 return false; 636 } 637 return true; 638 } 639 640 /** 641 * @return true if the inbox of the current account hasn't been refreshed 642 * in the last {@link #INBOX_AUTO_REFRESH_MIN_INTERVAL}. 643 */ 644 @VisibleForTesting 645 boolean shouldAutoRefreshInbox() { 646 if (mInboxId == mMailboxId) { 647 return false; // Current ID == inbox. No need to auto-refresh. 648 } 649 if (mRefreshManager.isMessageListRefreshing(mInboxId)) { 650 return false; 651 } 652 final long nextRefreshTime = mRefreshManager.getLastMessageListRefreshTime(mInboxId) 653 + INBOX_AUTO_REFRESH_MIN_INTERVAL; 654 if (nextRefreshTime > mClock.getTime()) { 655 return false; 656 } 657 return true; 658 } 659 } 660 661 private class ActionBarControllerCallback implements ActionBarController.Callback { 662 663 @Override 664 public long getUIAccountId() { 665 return UIControllerTwoPane.this.getUIAccountId(); 666 } 667 668 @Override 669 public long getMailboxId() { 670 return getMessageListMailboxId(); 671 } 672 673 @Override 674 public boolean isAccountSelected() { 675 return UIControllerTwoPane.this.isAccountSelected(); 676 } 677 678 @Override 679 public void onAccountSelected(long accountId) { 680 switchAccount(accountId, false); 681 } 682 683 @Override 684 public void onMailboxSelected(long accountId, long mailboxId) { 685 openMailbox(accountId, mailboxId); 686 } 687 688 @Override 689 public void onNoAccountsFound() { 690 Welcome.actionStart(mActivity); 691 mActivity.finish(); 692 } 693 694 @Override 695 public int getTitleMode() { 696 if (mThreePane.isLeftPaneVisible()) { 697 // Mailbox list visible 698 return TITLE_MODE_ACCOUNT_NAME_ONLY; 699 } else { 700 // Mailbox list hidden 701 return TITLE_MODE_ACCOUNT_WITH_MAILBOX; 702 } 703 } 704 705 public String getMessageSubject() { 706 if (isMessageViewInstalled() && getMessageViewFragment().isMessageOpen()) { 707 return getMessageViewFragment().getMessage().mSubject; 708 } else { 709 return null; 710 } 711 } 712 713 @Override 714 public boolean shouldShowUp() { 715 final int visiblePanes = mThreePane.getVisiblePanes(); 716 final boolean leftPaneHidden = ((visiblePanes & ThreePaneLayout.PANE_LEFT) == 0); 717 return leftPaneHidden 718 || (isMailboxListInstalled() && getMailboxListFragment().canNavigateUp()); 719 } 720 721 @Override 722 public String getSearchHint() { 723 return UIControllerTwoPane.this.getSearchHint(); 724 } 725 726 @Override 727 public void onSearchSubmit(final String queryTerm) { 728 UIControllerTwoPane.this.onSearchSubmit(queryTerm); 729 } 730 731 @Override 732 public void onSearchExit() { 733 UIControllerTwoPane.this.onSearchExit(); 734 } 735 } 736} 737