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