UIControllerOnePane.java revision e06e1224414c181e729f7952d80bb70d59fedc20
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 com.android.email.Email; 20import com.android.email.R; 21import com.android.email.activity.MailboxFinder.Callback; 22import com.android.email.activity.setup.AccountSecurity; 23import com.android.emailcommon.Logging; 24import com.android.emailcommon.provider.EmailContent.Account; 25import com.android.emailcommon.provider.EmailContent.Message; 26import com.android.emailcommon.provider.Mailbox; 27import com.android.emailcommon.utility.Utility; 28 29import android.app.Activity; 30import android.app.Fragment; 31import android.app.FragmentTransaction; 32import android.os.Bundle; 33import android.util.Log; 34 35import java.util.Set; 36 37 38/** 39 * UI Controller for non x-large devices. Supports a single-pane layout. 40 * 41 * One one-pane, only at most one fragment can be installed at a time. 42 * 43 * Note due to the asynchronous nature of the fragment transaction, there is a window when 44 * there is no installed or visible fragments. 45 * 46 * Major TODOs 47 * - TODO Newer/Older for message view with swipe! 48 * - TODO Implement callbacks 49 */ 50class 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 switchAccount(accountId); 61 } 62 63 // MailboxListFragment.Callback 64 @Override 65 public void onCurrentMailboxUpdated(long mailboxId, String mailboxName, int unreadCount) { 66 } 67 68 // MailboxListFragment.Callback 69 @Override 70 public void onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation) { 71 if (nestedNavigation) { 72 return; // Nothing to do on 1-pane. 73 } 74 openMailbox(accountId, mailboxId); 75 } 76 77 // MailboxListFragment.Callback 78 @Override 79 public void onParentMailboxChanged() { 80 refreshActionBar(); 81 } 82 83 // MessageListFragment.Callback 84 @Override 85 public void onAdvancingOpAccepted(Set<Long> affectedMessages) { 86 // Nothing to do on 1 pane. 87 } 88 89 // MessageListFragment.Callback 90 @Override 91 public void onEnterSelectionMode(boolean enter) { 92 // TODO Auto-generated method stub 93 } 94 95 // MessageListFragment.Callback 96 @Override 97 public void onListLoaded() { 98 // TODO Auto-generated method stub 99 } 100 101 // MessageListFragment.Callback 102 @Override 103 public void onMailboxNotFound() { 104 open(getUIAccountId(), Mailbox.NO_MAILBOX, Message.NO_MESSAGE); 105 } 106 107 // MessageListFragment.Callback 108 @Override 109 public void onMessageOpen( 110 long messageId, long messageMailboxId, long listMailboxId, int type) { 111 if (type == MessageListFragment.Callback.TYPE_DRAFT) { 112 MessageCompose.actionEditDraft(mActivity, messageId); 113 } else { 114 open(getUIAccountId(), getMailboxId(), messageId); 115 } 116 } 117 118 // MessageListFragment.Callback 119 @Override 120 public boolean onDragStarted() { 121 // No drag&drop on 1-pane 122 return false; 123 } 124 125 // MessageListFragment.Callback 126 @Override 127 public void onDragEnded() { 128 // No drag&drop on 1-pane 129 } 130 131 // MessageViewFragment.Callback 132 @Override 133 public void onForward() { 134 MessageCompose.actionForward(mActivity, getMessageId()); 135 } 136 137 // MessageViewFragment.Callback 138 @Override 139 public void onReply() { 140 MessageCompose.actionReply(mActivity, getMessageId(), false); 141 } 142 143 // MessageViewFragment.Callback 144 @Override 145 public void onReplyAll() { 146 MessageCompose.actionReply(mActivity, getMessageId(), true); 147 } 148 149 // MessageViewFragment.Callback 150 @Override 151 public void onCalendarLinkClicked(long epochEventStartTime) { 152 ActivityHelper.openCalendar(mActivity, epochEventStartTime); 153 } 154 155 // MessageViewFragment.Callback 156 @Override 157 public boolean onUrlInMessageClicked(String url) { 158 return ActivityHelper.openUrlInMessage(mActivity, url, getActualAccountId()); 159 } 160 161 // MessageViewFragment.Callback 162 @Override 163 public void onBeforeMessageGone() { 164 // TODO Auto-generated method stub 165 } 166 167 // MessageViewFragment.Callback 168 @Override 169 public void onMessageSetUnread() { 170 // TODO Auto-generated method stub 171 } 172 173 // MessageViewFragment.Callback 174 @Override 175 public void onRespondedToInvite(int response) { 176 // TODO Auto-generated method stub 177 } 178 179 // MessageViewFragment.Callback 180 @Override 181 public void onLoadMessageError(String errorMessage) { 182 // TODO Auto-generated method stub 183 } 184 185 // MessageViewFragment.Callback 186 @Override 187 public void onLoadMessageFinished() { 188 // TODO Auto-generated method stub 189 } 190 191 // MessageViewFragment.Callback 192 @Override 193 public void onLoadMessageStarted() { 194 // TODO Auto-generated method stub 195 } 196 197 // MessageViewFragment.Callback 198 @Override 199 public void onMessageNotExists() { 200 // TODO Auto-generated method stub 201 } 202 203 // MessageViewFragment.Callback 204 @Override 205 public void onMessageShown() { 206 // TODO Auto-generated method stub 207 } 208 209 // This is all temporary as we'll have a different action bar controller for 1-pane. 210 private class ActionBarControllerCallback implements ActionBarController.Callback { 211 @Override 212 public boolean shouldShowMailboxName() { 213 return false; // no mailbox name/unread count. 214 } 215 216 @Override 217 public String getCurrentMailboxName() { 218 return null; // no mailbox name/unread count. 219 } 220 221 @Override 222 public int getCurrentMailboxUnreadCount() { 223 return 0; // no mailbox name/unread count. 224 } 225 226 @Override 227 public boolean shouldShowUp() { 228 return isMessageViewVisible() 229 || (isMailboxListVisible() && !getMailboxListFragment().isRoot()); 230 } 231 232 @Override 233 public long getUIAccountId() { 234 return UIControllerOnePane.this.getUIAccountId(); 235 } 236 237 @Override 238 public void onMailboxSelected(long mailboxId) { 239 if (mailboxId == Mailbox.NO_MAILBOX) { 240 showAllMailboxes(); 241 } else { 242 UIControllerOnePane.this.openMailbox(getUIAccountId(), mailboxId); 243 } 244 } 245 246 @Override 247 public boolean isAccountSelected() { 248 return UIControllerOnePane.this.isAccountSelected(); 249 } 250 251 @Override 252 public void onAccountSelected(long accountId) { 253 switchAccount(accountId); 254 } 255 256 @Override 257 public void onNoAccountsFound() { 258 Welcome.actionStart(mActivity); 259 mActivity.finish(); 260 } 261 } 262 263 public UIControllerOnePane(EmailActivity activity) { 264 super(activity); 265 } 266 267 @Override 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 278 public void onSaveInstanceState(Bundle outState) { 279 super.onSaveInstanceState(outState); 280 if (mPreviousFragment != null) { 281 mActivity.getFragmentManager().putFragment(outState, 282 BUNDLE_KEY_PREVIOUS_FRAGMENT, mPreviousFragment); 283 } 284 } 285 286 @Override 287 public void restoreInstanceState(Bundle savedInstanceState) { 288 super.restoreInstanceState(savedInstanceState); 289 mPreviousFragment = mActivity.getFragmentManager().getFragment(savedInstanceState, 290 BUNDLE_KEY_PREVIOUS_FRAGMENT); 291 } 292 293 @Override 294 public int getLayoutId() { 295 return R.layout.email_activity_one_pane; 296 } 297 298 @Override 299 public void onActivityViewReady() { 300 super.onActivityViewReady(); 301 } 302 303 @Override 304 public void onActivityCreated() { 305 super.onActivityCreated(); 306 } 307 308 @Override 309 public void onActivityResume() { 310 super.onActivityResume(); 311 refreshActionBar(); 312 } 313 314 /** @return true if a {@link MailboxListFragment} is installed and visible. */ 315 private final boolean isMailboxListVisible() { 316 return isMailboxListInstalled(); 317 } 318 319 /** @return true if a {@link MessageListFragment} is installed and visible. */ 320 private final boolean isMessageListVisible() { 321 return isMessageListInstalled(); 322 } 323 324 /** @return true if a {@link MessageViewFragment} is installed and visible. */ 325 private final boolean isMessageViewVisible() { 326 return isMessageViewInstalled(); 327 } 328 329 @Override 330 public long getUIAccountId() { 331 // Get it from the visible fragment. 332 if (isMailboxListVisible()) { 333 return getMailboxListFragment().getAccountId(); 334 } 335 if (isMessageListVisible()) { 336 return getMessageListFragment().getAccountId(); 337 } 338 if (isMessageViewVisible()) { 339 return getMessageViewFragment().getOpenerAccountId(); 340 } 341 return Account.NO_ACCOUNT; 342 } 343 344 private long getMailboxId() { 345 // Get it from the visible fragment. 346 if (isMessageListVisible()) { 347 return getMessageListFragment().getMailboxId(); 348 } 349 if (isMessageViewVisible()) { 350 return getMessageViewFragment().getOpenerMailboxId(); 351 } 352 return Mailbox.NO_MAILBOX; 353 } 354 355 private long getMessageId() { 356 // Get it from the visible fragment. 357 if (isMessageViewVisible()) { 358 return getMessageViewFragment().getMessageId(); 359 } 360 return Message.NO_MESSAGE; 361 } 362 363 private final MailboxFinder.Callback mInboxLookupCallback = new MailboxFinder.Callback() { 364 @Override 365 public void onMailboxFound(long accountId, long mailboxId) { 366 // Inbox found. 367 openMailbox(accountId, mailboxId); 368 } 369 370 @Override 371 public void onAccountNotFound() { 372 // Account removed? 373 Welcome.actionStart(mActivity); 374 } 375 376 @Override 377 public void onMailboxNotFound(long accountId) { 378 // Inbox not found?? 379 Welcome.actionStart(mActivity); 380 } 381 382 @Override 383 public void onAccountSecurityHold(long accountId) { 384 mActivity.startActivity(AccountSecurity.actionUpdateSecurityIntent(mActivity, accountId, 385 true)); 386 } 387 }; 388 389 @Override 390 protected Callback getInboxLookupCallback() { 391 return mInboxLookupCallback; 392 } 393 394 @Override 395 public boolean onBackPressed(boolean isSystemBackKey) { 396 if (Email.DEBUG) { 397 // This is VERY important -- no check for DEBUG_LIFECYCLE 398 Log.d(Logging.LOG_TAG, this + " onBackPressed: " + isSystemBackKey); 399 } 400 // If the mailbox list is shown and showing a nested mailbox, let it navigate up first. 401 if (isMailboxListInstalled() && getMailboxListFragment().navigateUp()) { 402 if (DEBUG_FRAGMENTS) { 403 Log.d(Logging.LOG_TAG, this + " Back: back handled by mailbox list"); 404 } 405 return true; 406 } 407 408 // Custom back stack 409 if (shouldPopFromBackStack(isSystemBackKey)) { 410 if (DEBUG_FRAGMENTS) { 411 Log.d(Logging.LOG_TAG, this + " Back: Popping from back stack"); 412 } 413 popFromBackStack(); 414 return true; 415 } 416 417 // No entry in the back stack. 418 // If the message view is shown, show the "parent" message list. 419 // This happens when we get a deep link to a message. (e.g. from a widget) 420 if (isMessageViewInstalled()) { 421 if (DEBUG_FRAGMENTS) { 422 Log.d(Logging.LOG_TAG, this + " Back: Message view -> Message List"); 423 } 424 openMailbox(getMessageViewFragment().getOpenerAccountId(), 425 getMessageViewFragment().getOpenerMailboxId()); 426 return true; 427 } 428 return false; 429 } 430 431 @Override 432 public void open(final long accountId, final long mailboxId, final long messageId) { 433 if (Email.DEBUG) { 434 // This is VERY important -- no check for DEBUG_LIFECYCLE 435 Log.i(Logging.LOG_TAG, this + " open accountId=" + accountId 436 + " mailboxId=" + mailboxId + " messageId=" + messageId); 437 } 438 if (accountId == Account.NO_ACCOUNT) { 439 throw new IllegalArgumentException(); 440 } 441 442 if ((getUIAccountId() == accountId) && (getMailboxId() == mailboxId) 443 && (getMessageId() == messageId)) { 444 return; 445 } 446 447 final boolean accountChanging = (getUIAccountId() != accountId); 448 if (messageId != Message.NO_MESSAGE) { 449 showMessageView(accountId, mailboxId, messageId, accountChanging); 450 } else if (mailboxId != Mailbox.NO_MAILBOX) { 451 showMessageList(accountId, mailboxId, accountChanging); 452 } else { 453 // Mailbox not specified. Open Inbox or Combined Inbox. 454 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 455 showMessageList(accountId, Mailbox.QUERY_ALL_INBOXES, accountChanging); 456 } else { 457 startInboxLookup(accountId); 458 } 459 } 460 } 461 462 /** 463 * @return currently installed {@link Fragment} (1-pane has only one at most), or null if none 464 * exists. 465 */ 466 private Fragment getInstalledFragment() { 467 if (isMailboxListInstalled()) { 468 return getMailboxListFragment(); 469 } else if (isMessageListInstalled()) { 470 return getMessageListFragment(); 471 } else if (isMessageViewInstalled()) { 472 return getMessageViewFragment(); 473 } 474 return null; 475 } 476 477 /** 478 * Remove currently installed {@link Fragment} (1-pane has only one at most), or no-op if none 479 * exists. 480 */ 481 private void removeInstalledFragment(FragmentTransaction ft) { 482 removeFragment(ft, getInstalledFragment()); 483 } 484 485 private void showMailboxList(long accountId, long mailboxId, boolean clearBackStack) { 486 showFragment(MailboxListFragment.newInstance(accountId, mailboxId, false), clearBackStack); 487 } 488 489 private void showMessageList(long accountId, long mailboxId, boolean clearBackStack) { 490 showFragment(MessageListFragment.newInstance(accountId, mailboxId), clearBackStack); 491 } 492 493 private void showMessageView(long accountId, long mailboxId, long messageId, 494 boolean clearBackStack) { 495 showFragment(MessageViewFragment.newInstance(accountId, mailboxId, messageId), 496 clearBackStack); 497 } 498 499 /** 500 * Use this instead of {@link FragmentTransaction#commit}. We may switch to the synchronous 501 * transaction some day. 502 */ 503 private void commitFragmentTransaction(FragmentTransaction ft) { 504 ft.commit(); 505 } 506 507 /** 508 * Push the installed fragment into our custom back stack (or optionally 509 * {@link FragmentTransaction#remove} it) and {@link FragmentTransaction#add} {@code fragment}. 510 * 511 * @param fragment {@link Fragment} to be added. 512 * @param clearBackStack set {@code true} to remove the currently installed fragment. 513 * {@code false} to push it into the backstack. 514 * 515 * TODO Delay-call the whole method and use the synchronous transaction. 516 */ 517 private void showFragment(Fragment fragment, boolean clearBackStack) { 518 if (DEBUG_FRAGMENTS) { 519 if (clearBackStack) { 520 Log.i(Logging.LOG_TAG, this + " backstack: [clear] showing " + fragment); 521 } else { 522 Log.i(Logging.LOG_TAG, this + " backstack: [push] " + getInstalledFragment() 523 + " -> " + fragment); 524 } 525 } 526 final FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction(); 527 if (mPreviousFragment != null) { 528 if (DEBUG_FRAGMENTS) { 529 Log.d(Logging.LOG_TAG, this + " showFragment: destroying previous fragment " 530 + mPreviousFragment); 531 } 532 removeFragment(ft, mPreviousFragment); 533 mPreviousFragment = null; 534 } 535 // Remove or push the current one 536 if (clearBackStack) { 537 // Really remove the currently installed one. 538 removeInstalledFragment(ft); 539 } else { 540 // Instead of removing, detach the current one and push into our back stack. 541 mPreviousFragment = getInstalledFragment(); 542 if (mPreviousFragment != null) { 543 if (DEBUG_FRAGMENTS) { 544 Log.d(Logging.LOG_TAG, this + " showFragment: detaching " + mPreviousFragment); 545 } 546 ft.detach(mPreviousFragment); 547 } 548 } 549 // Add the new one 550 if (DEBUG_FRAGMENTS) { 551 Log.d(Logging.LOG_TAG, this + " showFragment: adding " + fragment); 552 } 553 ft.add(R.id.fragment_placeholder, fragment); 554 commitFragmentTransaction(ft); 555 } 556 557 /** 558 * @param isSystemBackKey <code>true</code> if the system back key was pressed. 559 * <code>false</code> if it's caused by the "home" icon click on the action bar. 560 * @return true if we should pop from our custom back stack. 561 */ 562 private boolean shouldPopFromBackStack(boolean isSystemBackKey) { 563 if (mPreviousFragment == null) { 564 return false; // Nothing in the back stack 565 } 566 // Never go back to Message View 567 if (mPreviousFragment instanceof MessageViewFragment) { 568 return false; 569 } 570 final Fragment installed = getInstalledFragment(); 571 if (installed == null) { 572 // If no fragment is installed right now, do nothing. 573 return false; 574 } 575 576 // Okay now we have 2 fragments; the one in the back stack and the one that's currently 577 // installed. 578 if (mPreviousFragment.getClass() == installed.getClass()) { 579 // We never want to go back to the same kind of fragment, which happens when the user 580 // is on the message list, and selects another mailbox on the action bar. 581 return false; 582 } 583 584 if (isSystemBackKey) { 585 // In other cases, the system back key should always work. 586 return true; 587 } else { 588 // Home icon press -- there are cases where we don't want it to work. 589 590 // Disallow the Message list <-> mailbox list transition 591 if ((mPreviousFragment instanceof MailboxListFragment) 592 && (installed instanceof MessageListFragment)) { 593 return false; 594 } 595 if ((mPreviousFragment instanceof MessageListFragment) 596 && (installed instanceof MailboxListFragment)) { 597 return false; 598 } 599 return true; 600 } 601 } 602 603 /** 604 * Pop from our custom back stack. 605 * 606 * TODO Delay-call the whole method and use the synchronous transaction. 607 */ 608 private void popFromBackStack() { 609 if (mPreviousFragment == null) { 610 return; 611 } 612 final FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction(); 613 final Fragment installed = getInstalledFragment(); 614 if (DEBUG_FRAGMENTS) { 615 Log.i(Logging.LOG_TAG, this + " backstack: [pop] " + installed + " -> " 616 + mPreviousFragment); 617 } 618 removeFragment(ft, installed); 619 ft.attach(mPreviousFragment); 620 commitFragmentTransaction(ft); 621 mPreviousFragment = null; 622 return; 623 } 624 625 private void showAllMailboxes() { 626 if (!isAccountSelected()) { 627 return; // Can happen because of asynchronous fragment transactions. 628 } 629 // Don't use open(account, NO_MAILBOX, NO_MESSAGE). This is used to open the default 630 // view, which is Inbox on the message list. (There's actually no way to open the mainbox 631 // list with open(long,long,long)) 632 showMailboxList(getUIAccountId(), Mailbox.NO_MAILBOX, false); 633 } 634 635 /* 636 * STOPSHIP Remove this -- see the base class method. 637 */ 638 @Override 639 public long getMailboxSettingsMailboxId() { 640 // Mailbox settings is still experimental, and doesn't have to work on the phone. 641 Utility.showToast(mActivity, "STOPSHIP: Mailbox settings not supported on 1 pane"); 642 return Mailbox.NO_MAILBOX; 643 } 644 645 /* 646 * STOPSHIP Remove this -- see the base class method. 647 */ 648 @Override 649 public long getSearchMailboxId() { 650 // Search is still experimental, and doesn't have to work on the phone. 651 Utility.showToast(mActivity, "STOPSHIP: Search not supported on 1 pane"); 652 return Mailbox.NO_MAILBOX; 653 } 654 655 @Override 656 protected boolean isRefreshEnabled() { 657 // Refreshable only when an actual account is selected, and message view isn't shown. 658 // (i.e. only available on the mailbox list or the message view, but not on the combined 659 // one) 660 return isActualAccountSelected() && !isMessageViewVisible(); 661 } 662 663 @Override 664 public void onRefresh() { 665 if (!isRefreshEnabled()) { 666 return; 667 } 668 if (isMessageListVisible()) { 669 mRefreshManager.refreshMessageList(getActualAccountId(), getMailboxId(), true); 670 } else { 671 mRefreshManager.refreshMailboxList(getActualAccountId()); 672 } 673 } 674 675 @Override 676 protected boolean isRefreshInProgress() { 677 if (!isRefreshEnabled()) { 678 return false; 679 } 680 if (isMessageListVisible()) { 681 return mRefreshManager.isMessageListRefreshing(getMailboxId()); 682 } else { 683 return mRefreshManager.isMailboxListRefreshing(getActualAccountId()); 684 } 685 } 686} 687