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