1 /* 2 * Copyright (C) 2011 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.Fragment; 21 import android.app.FragmentTransaction; 22 import android.os.Bundle; 23 import android.util.Log; 24 import android.view.Menu; 25 import android.view.MenuInflater; 26 import android.view.MenuItem; 27 28 import com.android.email.Email; 29 import com.android.email.MessageListContext; 30 import com.android.email.R; 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 36 import java.util.Set; 37 38 39 /** 40 * UI Controller for non x-large devices. Supports a single-pane layout. 41 * 42 * One one-pane, only at most one fragment can be installed at a time. 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 * Major TODOs 48 * - TODO Implement callbacks 49 */ 50 class UIControllerOnePane extends UIControllerBase { 51 private static final String BUNDLE_KEY_PREVIOUS_FRAGMENT 52 = "UIControllerOnePane.PREVIOUS_FRAGMENT"; 53 54 // Our custom poor-man's back stack which has only one entry at maximum. 55 private Fragment mPreviousFragment; 56 57 // MailboxListFragment.Callback 58 @Override onAccountSelected(long accountId)59 public void onAccountSelected(long accountId) { 60 // It's from combined view, so "forceShowInbox" doesn't really matter. 61 // (We're always switching accounts.) 62 switchAccount(accountId, true); 63 } 64 65 // MailboxListFragment.Callback 66 @Override onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation)67 public void onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation) { 68 if (nestedNavigation) { 69 return; // Nothing to do on 1-pane. 70 } 71 openMailbox(accountId, mailboxId); 72 } 73 74 // MailboxListFragment.Callback 75 @Override onParentMailboxChanged()76 public void onParentMailboxChanged() { 77 refreshActionBar(); 78 } 79 80 // MessageListFragment.Callback 81 @Override onAdvancingOpAccepted(Set<Long> affectedMessages)82 public void onAdvancingOpAccepted(Set<Long> affectedMessages) { 83 // Nothing to do on 1 pane. 84 } 85 86 // MessageListFragment.Callback 87 @Override onMessageOpen( long messageId, long messageMailboxId, long listMailboxId, int type)88 public void onMessageOpen( 89 long messageId, long messageMailboxId, long listMailboxId, int type) { 90 if (type == MessageListFragment.Callback.TYPE_DRAFT) { 91 MessageCompose.actionEditDraft(mActivity, messageId); 92 } else { 93 open(mListContext, messageId); 94 } 95 } 96 97 // MessageListFragment.Callback 98 @Override onDragStarted()99 public boolean onDragStarted() { 100 // No drag&drop on 1-pane 101 return false; 102 } 103 104 // MessageListFragment.Callback 105 @Override onDragEnded()106 public void onDragEnded() { 107 // No drag&drop on 1-pane 108 } 109 110 // MessageViewFragment.Callback 111 @Override onForward()112 public void onForward() { 113 MessageCompose.actionForward(mActivity, getMessageId()); 114 } 115 116 // MessageViewFragment.Callback 117 @Override onReply()118 public void onReply() { 119 MessageCompose.actionReply(mActivity, getMessageId(), false); 120 } 121 122 // MessageViewFragment.Callback 123 @Override onReplyAll()124 public void onReplyAll() { 125 MessageCompose.actionReply(mActivity, getMessageId(), true); 126 } 127 128 // MessageViewFragment.Callback 129 @Override onCalendarLinkClicked(long epochEventStartTime)130 public void onCalendarLinkClicked(long epochEventStartTime) { 131 ActivityHelper.openCalendar(mActivity, epochEventStartTime); 132 } 133 134 // MessageViewFragment.Callback 135 @Override onUrlInMessageClicked(String url)136 public boolean onUrlInMessageClicked(String url) { 137 return ActivityHelper.openUrlInMessage(mActivity, url, getActualAccountId()); 138 } 139 140 // MessageViewFragment.Callback 141 @Override onLoadMessageError(String errorMessage)142 public void onLoadMessageError(String errorMessage) { 143 // TODO Auto-generated method stub 144 } 145 146 // MessageViewFragment.Callback 147 @Override onLoadMessageFinished()148 public void onLoadMessageFinished() { 149 // TODO Auto-generated method stub 150 } 151 152 // MessageViewFragment.Callback 153 @Override onLoadMessageStarted()154 public void onLoadMessageStarted() { 155 // TODO Auto-generated method stub 156 } 157 isInboxShown()158 private boolean isInboxShown() { 159 if (!isMessageListInstalled()) { 160 return false; 161 } 162 return getMessageListFragment().isInboxList(); 163 } 164 165 // This is all temporary as we'll have a different action bar controller for 1-pane. 166 private class ActionBarControllerCallback implements ActionBarController.Callback { 167 @Override getTitleMode()168 public int getTitleMode() { 169 if (isMailboxListInstalled()) { 170 return TITLE_MODE_ACCOUNT_WITH_ALL_FOLDERS_LABEL; 171 } 172 if (isMessageViewInstalled()) { 173 return TITLE_MODE_MESSAGE_SUBJECT; 174 } 175 return TITLE_MODE_ACCOUNT_WITH_MAILBOX; 176 } 177 getMessageSubject()178 public String getMessageSubject() { 179 if (isMessageViewInstalled() && getMessageViewFragment().isMessageOpen()) { 180 return getMessageViewFragment().getMessage().mSubject; 181 } else { 182 return null; 183 } 184 } 185 186 @Override shouldShowUp()187 public boolean shouldShowUp() { 188 return isMessageViewInstalled() 189 || (isMessageListInstalled() && !isInboxShown()) 190 || isMailboxListInstalled(); 191 } 192 193 @Override getUIAccountId()194 public long getUIAccountId() { 195 return UIControllerOnePane.this.getUIAccountId(); 196 } 197 198 @Override getMailboxId()199 public long getMailboxId() { 200 return UIControllerOnePane.this.getMailboxId(); 201 } 202 203 @Override onMailboxSelected(long accountId, long mailboxId)204 public void onMailboxSelected(long accountId, long mailboxId) { 205 if (mailboxId == Mailbox.NO_MAILBOX) { 206 showAllMailboxes(); 207 } else { 208 openMailbox(accountId, mailboxId); 209 } 210 } 211 212 @Override isAccountSelected()213 public boolean isAccountSelected() { 214 return UIControllerOnePane.this.isAccountSelected(); 215 } 216 217 @Override onAccountSelected(long accountId)218 public void onAccountSelected(long accountId) { 219 switchAccount(accountId, true); // Always go to inbox 220 } 221 222 @Override onNoAccountsFound()223 public void onNoAccountsFound() { 224 Welcome.actionStart(mActivity); 225 mActivity.finish(); 226 } 227 228 @Override getSearchHint()229 public String getSearchHint() { 230 if (!isMessageListInstalled()) { 231 return null; 232 } 233 return UIControllerOnePane.this.getSearchHint(); 234 } 235 236 @Override onSearchStarted()237 public void onSearchStarted() { 238 if (!isMessageListInstalled()) { 239 return; 240 } 241 UIControllerOnePane.this.onSearchStarted(); 242 } 243 244 @Override onSearchSubmit(String queryTerm)245 public void onSearchSubmit(String queryTerm) { 246 if (!isMessageListInstalled()) { 247 return; 248 } 249 UIControllerOnePane.this.onSearchSubmit(queryTerm); 250 } 251 252 @Override onSearchExit()253 public void onSearchExit() { 254 UIControllerOnePane.this.onSearchExit(); 255 } 256 257 @Override onUpPressed()258 public void onUpPressed() { 259 onBackPressed(false); 260 } 261 } 262 UIControllerOnePane(EmailActivity activity)263 public UIControllerOnePane(EmailActivity activity) { 264 super(activity); 265 } 266 267 @Override createActionBarController(Activity activity)268 protected ActionBarController createActionBarController(Activity activity) { 269 270 // For now, we just reuse the same action bar controller used for 2-pane. 271 // We may change it later. 272 273 return new ActionBarController(activity, activity.getLoaderManager(), 274 activity.getActionBar(), new ActionBarControllerCallback()); 275 } 276 277 @Override onSaveInstanceState(Bundle outState)278 public void onSaveInstanceState(Bundle outState) { 279 super.onSaveInstanceState(outState); 280 if (mPreviousFragment != null) { 281 mFragmentManager.putFragment(outState, 282 BUNDLE_KEY_PREVIOUS_FRAGMENT, mPreviousFragment); 283 } 284 } 285 286 @Override onRestoreInstanceState(Bundle savedInstanceState)287 public void onRestoreInstanceState(Bundle savedInstanceState) { 288 super.onRestoreInstanceState(savedInstanceState); 289 mPreviousFragment = mFragmentManager.getFragment(savedInstanceState, 290 BUNDLE_KEY_PREVIOUS_FRAGMENT); 291 } 292 293 @Override getLayoutId()294 public int getLayoutId() { 295 return R.layout.email_activity_one_pane; 296 } 297 298 @Override getUIAccountId()299 public long getUIAccountId() { 300 if (mListContext != null) { 301 return mListContext.mAccountId; 302 } 303 if (isMailboxListInstalled()) { 304 return getMailboxListFragment().getAccountId(); 305 } 306 return Account.NO_ACCOUNT; 307 } 308 getMailboxId()309 private long getMailboxId() { 310 if (mListContext != null) { 311 return mListContext.getMailboxId(); 312 } 313 return Mailbox.NO_MAILBOX; 314 } 315 316 @Override onBackPressed(boolean isSystemBackKey)317 public boolean onBackPressed(boolean isSystemBackKey) { 318 if (Email.DEBUG) { 319 // This is VERY important -- no check for DEBUG_LIFECYCLE 320 Log.d(Logging.LOG_TAG, this + " onBackPressed: " + isSystemBackKey); 321 } 322 // The action bar controller has precedence. Must call it first. 323 if (mActionBarController.onBackPressed(isSystemBackKey)) { 324 return true; 325 } 326 // If the mailbox list is shown and showing a nested mailbox, let it navigate up first. 327 if (isMailboxListInstalled() && getMailboxListFragment().navigateUp()) { 328 if (DEBUG_FRAGMENTS) { 329 Log.d(Logging.LOG_TAG, this + " Back: back handled by mailbox list"); 330 } 331 return true; 332 } 333 334 // Custom back stack 335 if (shouldPopFromBackStack(isSystemBackKey)) { 336 if (DEBUG_FRAGMENTS) { 337 Log.d(Logging.LOG_TAG, this + " Back: Popping from back stack"); 338 } 339 popFromBackStack(); 340 return true; 341 } 342 343 // No entry in the back stack. 344 if (isMessageViewInstalled()) { 345 if (DEBUG_FRAGMENTS) { 346 Log.d(Logging.LOG_TAG, this + " Back: Message view -> Message List"); 347 } 348 // If the message view is shown, show the "parent" message list. 349 // This happens when we get a deep link to a message. (e.g. from a widget) 350 openMailbox(mListContext.mAccountId, mListContext.getMailboxId()); 351 return true; 352 } else if (isMailboxListInstalled()) { 353 // If the mailbox list is shown, always go back to the inbox. 354 switchAccount(getMailboxListFragment().getAccountId(), true /* force show inbox */); 355 return true; 356 } else if (isMessageListInstalled() && !isInboxShown()) { 357 // Non-inbox list. Go to inbox. 358 switchAccount(mListContext.mAccountId, true /* force show inbox */); 359 return true; 360 } 361 return false; 362 } 363 364 @Override openInternal(final MessageListContext listContext, final long messageId)365 public void openInternal(final MessageListContext listContext, final long messageId) { 366 if (Email.DEBUG) { 367 // This is VERY important -- don't check for DEBUG_LIFECYCLE 368 Log.i(Logging.LOG_TAG, this + " open " + listContext + " messageId=" + messageId); 369 } 370 371 if (messageId != Message.NO_MESSAGE) { 372 openMessage(messageId); 373 } else { 374 showFragment(MessageListFragment.newInstance(listContext)); 375 } 376 } 377 378 /** 379 * @return currently installed {@link Fragment} (1-pane has only one at most), or null if none 380 * exists. 381 */ getInstalledFragment()382 private Fragment getInstalledFragment() { 383 if (isMailboxListInstalled()) { 384 return getMailboxListFragment(); 385 } else if (isMessageListInstalled()) { 386 return getMessageListFragment(); 387 } else if (isMessageViewInstalled()) { 388 return getMessageViewFragment(); 389 } 390 return null; 391 } 392 393 /** 394 * Show the mailbox list. 395 * 396 * This is the only way to open the mailbox list on 1-pane. 397 * {@link #open(MessageListContext, long)} will only open either the message list or the 398 * message view. 399 */ openMailboxList(long accountId)400 private void openMailboxList(long accountId) { 401 setListContext(null); 402 showFragment(MailboxListFragment.newInstance(accountId, Mailbox.NO_MAILBOX, false)); 403 } 404 openMessage(long messageId)405 private void openMessage(long messageId) { 406 showFragment(MessageViewFragment.newInstance(messageId)); 407 } 408 409 /** 410 * Push the installed fragment into our custom back stack (or optionally 411 * {@link FragmentTransaction#remove} it) and {@link FragmentTransaction#add} {@code fragment}. 412 * 413 * @param fragment {@link Fragment} to be added. 414 * 415 * TODO Delay-call the whole method and use the synchronous transaction. 416 */ showFragment(Fragment fragment)417 private void showFragment(Fragment fragment) { 418 final FragmentTransaction ft = mFragmentManager.beginTransaction(); 419 final Fragment installed = getInstalledFragment(); 420 if ((installed instanceof MessageViewFragment) 421 && (fragment instanceof MessageViewFragment)) { 422 // Newer/older navigation, auto-advance, etc. 423 // In this case we want to keep the backstack untouched, so that after back navigation 424 // we can restore the message list, including scroll position and batch selection. 425 } else { 426 if (DEBUG_FRAGMENTS) { 427 Log.i(Logging.LOG_TAG, this + " backstack: [push] " + getInstalledFragment() 428 + " -> " + fragment); 429 } 430 if (mPreviousFragment != null) { 431 if (DEBUG_FRAGMENTS) { 432 Log.d(Logging.LOG_TAG, this + " showFragment: destroying previous fragment " 433 + mPreviousFragment); 434 } 435 removeFragment(ft, mPreviousFragment); 436 mPreviousFragment = null; 437 } 438 // Remove the current fragment or push it into the backstack. 439 if (installed != null) { 440 if (installed instanceof MessageViewFragment) { 441 // Message view should never be pushed to the backstack. 442 if (DEBUG_FRAGMENTS) { 443 Log.d(Logging.LOG_TAG, this + " showFragment: removing " + installed); 444 } 445 ft.remove(installed); 446 } else { 447 // Other fragments should be pushed. 448 mPreviousFragment = installed; 449 if (DEBUG_FRAGMENTS) { 450 Log.d(Logging.LOG_TAG, this + " showFragment: detaching " 451 + mPreviousFragment); 452 } 453 ft.detach(mPreviousFragment); 454 } 455 } 456 } 457 // Show the new one 458 if (DEBUG_FRAGMENTS) { 459 Log.d(Logging.LOG_TAG, this + " showFragment: replacing with " + fragment); 460 } 461 ft.replace(R.id.fragment_placeholder, fragment); 462 ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 463 commitFragmentTransaction(ft); 464 } 465 466 /** 467 * @param isSystemBackKey <code>true</code> if the system back key was pressed. 468 * <code>false</code> if it's caused by the "home" icon click on the action bar. 469 * @return true if we should pop from our custom back stack. 470 */ shouldPopFromBackStack(boolean isSystemBackKey)471 private boolean shouldPopFromBackStack(boolean isSystemBackKey) { 472 if (mPreviousFragment == null) { 473 return false; // Nothing in the back stack 474 } 475 if (mPreviousFragment instanceof MessageViewFragment) { 476 throw new IllegalStateException("Message view should never be in backstack"); 477 } 478 final Fragment installed = getInstalledFragment(); 479 if (installed == null) { 480 // If no fragment is installed right now, do nothing. 481 return false; 482 } 483 484 // Okay now we have 2 fragments; the one in the back stack and the one that's currently 485 // installed. 486 if (isInboxShown()) { 487 // Inbox is the top level list - never go back from here. 488 return false; 489 } 490 491 // Disallow the MailboxList--> non-inbox MessageList transition as the Mailbox list 492 // is always considered "higher" than a non-inbox MessageList 493 if ((mPreviousFragment instanceof MessageListFragment) 494 && (!((MessageListFragment) mPreviousFragment).isInboxList()) 495 && (installed instanceof MailboxListFragment)) { 496 return false; 497 } 498 return true; 499 } 500 501 /** 502 * Pop from our custom back stack. 503 * 504 * TODO Delay-call the whole method and use the synchronous transaction. 505 */ popFromBackStack()506 private void popFromBackStack() { 507 if (mPreviousFragment == null) { 508 return; 509 } 510 final FragmentTransaction ft = mFragmentManager.beginTransaction(); 511 final Fragment installed = getInstalledFragment(); 512 if (DEBUG_FRAGMENTS) { 513 Log.i(Logging.LOG_TAG, this + " backstack: [pop] " + installed + " -> " 514 + mPreviousFragment); 515 } 516 removeFragment(ft, installed); 517 518 // Restore listContext. 519 if (mPreviousFragment instanceof MailboxListFragment) { 520 setListContext(null); 521 } else if (mPreviousFragment instanceof MessageListFragment) { 522 setListContext(((MessageListFragment) mPreviousFragment).getListContext()); 523 } else { 524 throw new IllegalStateException("Message view should never be in backstack"); 525 } 526 527 ft.attach(mPreviousFragment); 528 ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE); 529 mPreviousFragment = null; 530 commitFragmentTransaction(ft); 531 return; 532 } 533 showAllMailboxes()534 private void showAllMailboxes() { 535 if (!isAccountSelected()) { 536 return; // Can happen because of asynchronous fragment transactions. 537 } 538 539 openMailboxList(getUIAccountId()); 540 } 541 542 @Override installMailboxListFragment(MailboxListFragment fragment)543 protected void installMailboxListFragment(MailboxListFragment fragment) { 544 stopMessageOrderManager(); 545 super.installMailboxListFragment(fragment); 546 } 547 548 @Override installMessageListFragment(MessageListFragment fragment)549 protected void installMessageListFragment(MessageListFragment fragment) { 550 stopMessageOrderManager(); 551 super.installMessageListFragment(fragment); 552 } 553 554 @Override getMailboxSettingsMailboxId()555 protected long getMailboxSettingsMailboxId() { 556 return isMessageListInstalled() 557 ? getMessageListFragment().getMailboxId() 558 : Mailbox.NO_MAILBOX; 559 } 560 561 /** 562 * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback. 563 */ onCreateOptionsMenu(MenuInflater inflater, Menu menu)564 public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) { 565 if (isMessageListInstalled()) { 566 inflater.inflate(R.menu.message_list_fragment_option, menu); 567 return true; 568 } 569 if (isMessageViewInstalled()) { 570 inflater.inflate(R.menu.message_view_fragment_option, menu); 571 return true; 572 } 573 return false; 574 } 575 576 @Override onPrepareOptionsMenu(MenuInflater inflater, Menu menu)577 public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) { 578 // First, let the base class do what it has to do. 579 super.onPrepareOptionsMenu(inflater, menu); 580 581 final boolean messageViewVisible = isMessageViewInstalled(); 582 if (messageViewVisible) { 583 final MessageOrderManager om = getMessageOrderManager(); 584 menu.findItem(R.id.newer).setVisible(true); 585 menu.findItem(R.id.older).setVisible(true); 586 // orderManager shouldn't be null when the message view is installed, but just in case.. 587 menu.findItem(R.id.newer).setEnabled((om != null) && om.canMoveToNewer()); 588 menu.findItem(R.id.older).setEnabled((om != null) && om.canMoveToOlder()); 589 } 590 return true; 591 } 592 593 @Override onOptionsItemSelected(MenuItem item)594 public boolean onOptionsItemSelected(MenuItem item) { 595 switch (item.getItemId()) { 596 case R.id.newer: 597 moveToNewer(); 598 return true; 599 case R.id.older: 600 moveToOlder(); 601 return true; 602 case R.id.show_all_mailboxes: 603 showAllMailboxes(); 604 return true; 605 } 606 return super.onOptionsItemSelected(item); 607 } 608 609 @Override isRefreshEnabled()610 protected boolean isRefreshEnabled() { 611 // Refreshable only when an actual account is selected, and message view isn't shown. 612 // (i.e. only available on the mailbox list or the message view, but not on the combined 613 // one) 614 if (!isActualAccountSelected() || isMessageViewInstalled()) { 615 return false; 616 } 617 return isMailboxListInstalled() || (mListContext.getMailboxId() > 0); 618 } 619 620 @Override onRefresh()621 protected void onRefresh() { 622 if (!isRefreshEnabled()) { 623 return; 624 } 625 if (isMessageListInstalled()) { 626 mRefreshManager.refreshMessageList(getActualAccountId(), getMailboxId(), true); 627 } else { 628 mRefreshManager.refreshMailboxList(getActualAccountId()); 629 } 630 } 631 632 @Override isRefreshInProgress()633 protected boolean isRefreshInProgress() { 634 if (!isRefreshEnabled()) { 635 return false; 636 } 637 if (isMessageListInstalled()) { 638 return mRefreshManager.isMessageListRefreshing(getMailboxId()); 639 } else { 640 return mRefreshManager.isMailboxListRefreshing(getActualAccountId()); 641 } 642 } 643 navigateToMessage(long messageId)644 @Override protected void navigateToMessage(long messageId) { 645 openMessage(messageId); 646 } 647 updateNavigationArrows()648 @Override protected void updateNavigationArrows() { 649 refreshActionBar(); 650 } 651 } 652