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