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