UIControllerOnePane.java revision 1340b2f82b42e9d6afc7ff7ed4468aefcc8d5bc9
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; 35 36import 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 */ 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 // 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 // This is all temporary as we'll have a different action bar controller for 1-pane. 159 private class ActionBarControllerCallback implements ActionBarController.Callback { 160 @Override 161 public int getTitleMode() { 162 if (isMailboxListInstalled()) { 163 return TITLE_MODE_ACCOUNT_WITH_ALL_FOLDERS_LABEL; 164 } 165 if (isMessageViewInstalled()) { 166 return TITLE_MODE_MESSAGE_SUBJECT; 167 } 168 return TITLE_MODE_ACCOUNT_WITH_MAILBOX; 169 } 170 171 public String getMessageSubject() { 172 if (isMessageViewInstalled() && getMessageViewFragment().isMessageOpen()) { 173 return getMessageViewFragment().getMessage().mSubject; 174 } else { 175 return null; 176 } 177 } 178 179 @Override 180 public boolean shouldShowUp() { 181 return isMessageViewInstalled() 182 || (isMailboxListInstalled() && getMailboxListFragment().canNavigateUp()); 183 } 184 185 @Override 186 public long getUIAccountId() { 187 return UIControllerOnePane.this.getUIAccountId(); 188 } 189 190 @Override 191 public long getMailboxId() { 192 return UIControllerOnePane.this.getMailboxId(); 193 } 194 195 @Override 196 public void onMailboxSelected(long accountId, long mailboxId) { 197 if (mailboxId == Mailbox.NO_MAILBOX) { 198 showAllMailboxes(); 199 } else { 200 openMailbox(accountId, mailboxId); 201 } 202 } 203 204 @Override 205 public boolean isAccountSelected() { 206 return UIControllerOnePane.this.isAccountSelected(); 207 } 208 209 @Override 210 public void onAccountSelected(long accountId) { 211 switchAccount(accountId, true); // Always go to inbox 212 } 213 214 @Override 215 public void onNoAccountsFound() { 216 Welcome.actionStart(mActivity); 217 mActivity.finish(); 218 } 219 220 @Override 221 public String getSearchHint() { 222 if (!isMessageListInstalled()) { 223 return null; 224 } 225 return UIControllerOnePane.this.getSearchHint(); 226 } 227 228 @Override 229 public void onSearchSubmit(String queryTerm) { 230 if (!isMessageListInstalled()) { 231 return; 232 } 233 UIControllerOnePane.this.onSearchSubmit(queryTerm); 234 } 235 236 @Override 237 public void onSearchExit() { 238 UIControllerOnePane.this.onSearchExit(); 239 } 240 } 241 242 public UIControllerOnePane(EmailActivity activity) { 243 super(activity); 244 } 245 246 @Override 247 protected ActionBarController createActionBarController(Activity activity) { 248 249 // For now, we just reuse the same action bar controller used for 2-pane. 250 // We may change it later. 251 252 return new ActionBarController(activity, activity.getLoaderManager(), 253 activity.getActionBar(), new ActionBarControllerCallback()); 254 } 255 256 @Override 257 public void onSaveInstanceState(Bundle outState) { 258 super.onSaveInstanceState(outState); 259 if (mPreviousFragment != null) { 260 mFragmentManager.putFragment(outState, 261 BUNDLE_KEY_PREVIOUS_FRAGMENT, mPreviousFragment); 262 } 263 } 264 265 @Override 266 public void onRestoreInstanceState(Bundle savedInstanceState) { 267 super.onRestoreInstanceState(savedInstanceState); 268 mPreviousFragment = mFragmentManager.getFragment(savedInstanceState, 269 BUNDLE_KEY_PREVIOUS_FRAGMENT); 270 } 271 272 @Override 273 public int getLayoutId() { 274 return R.layout.email_activity_one_pane; 275 } 276 277 @Override 278 public long getUIAccountId() { 279 if (mListContext != null) { 280 return mListContext.mAccountId; 281 } 282 if (isMailboxListInstalled()) { 283 return getMailboxListFragment().getAccountId(); 284 } 285 return Account.NO_ACCOUNT; 286 } 287 288 private long getMailboxId() { 289 if (mListContext != null) { 290 return mListContext.getMailboxId(); 291 } 292 return Mailbox.NO_MAILBOX; 293 } 294 295 @Override 296 public boolean onBackPressed(boolean isSystemBackKey) { 297 if (Email.DEBUG) { 298 // This is VERY important -- no check for DEBUG_LIFECYCLE 299 Log.d(Logging.LOG_TAG, this + " onBackPressed: " + isSystemBackKey); 300 } 301 // The action bar controller has precedence. Must call it first. 302 if (mActionBarController.onBackPressed(isSystemBackKey)) { 303 return true; 304 } 305 // If the mailbox list is shown and showing a nested mailbox, let it navigate up first. 306 if (isMailboxListInstalled() && getMailboxListFragment().navigateUp()) { 307 if (DEBUG_FRAGMENTS) { 308 Log.d(Logging.LOG_TAG, this + " Back: back handled by mailbox list"); 309 } 310 return true; 311 } 312 313 // Custom back stack 314 if (shouldPopFromBackStack(isSystemBackKey)) { 315 if (DEBUG_FRAGMENTS) { 316 Log.d(Logging.LOG_TAG, this + " Back: Popping from back stack"); 317 } 318 popFromBackStack(); 319 return true; 320 } 321 322 // No entry in the back stack. 323 // If the message view is shown, show the "parent" message list. 324 // This happens when we get a deep link to a message. (e.g. from a widget) 325 if (isMessageViewInstalled()) { 326 if (DEBUG_FRAGMENTS) { 327 Log.d(Logging.LOG_TAG, this + " Back: Message view -> Message List"); 328 } 329 openMailbox(mListContext.mAccountId, mListContext.getMailboxId()); 330 return true; 331 } 332 return false; 333 } 334 335 @Override 336 public void openInternal(final MessageListContext listContext, final long messageId) { 337 if (Email.DEBUG) { 338 // This is VERY important -- don't check for DEBUG_LIFECYCLE 339 Log.i(Logging.LOG_TAG, this + " open " + listContext + " messageId=" + messageId); 340 } 341 342 if (messageId != Message.NO_MESSAGE) { 343 openMessage(messageId); 344 } else { 345 showFragment(MessageListFragment.newInstance(listContext)); 346 } 347 } 348 349 /** 350 * @return currently installed {@link Fragment} (1-pane has only one at most), or null if none 351 * exists. 352 */ 353 private Fragment getInstalledFragment() { 354 if (isMailboxListInstalled()) { 355 return getMailboxListFragment(); 356 } else if (isMessageListInstalled()) { 357 return getMessageListFragment(); 358 } else if (isMessageViewInstalled()) { 359 return getMessageViewFragment(); 360 } 361 return null; 362 } 363 364 /** 365 * Show the mailbox list. 366 * 367 * This is the only way to open the mailbox list on 1-pane. 368 * {@link #open(MessageListContext, long)} will only open either the message list or the 369 * message view. 370 */ 371 private void openMailboxList(long accountId) { 372 setListContext(null); 373 showFragment(MailboxListFragment.newInstance(accountId, Mailbox.NO_MAILBOX, false)); 374 } 375 376 private void openMessage(long messageId) { 377 showFragment(MessageViewFragment.newInstance(messageId)); 378 } 379 380 /** 381 * Push the installed fragment into our custom back stack (or optionally 382 * {@link FragmentTransaction#remove} it) and {@link FragmentTransaction#add} {@code fragment}. 383 * 384 * @param fragment {@link Fragment} to be added. 385 * 386 * TODO Delay-call the whole method and use the synchronous transaction. 387 */ 388 private void showFragment(Fragment fragment) { 389 final FragmentTransaction ft = mFragmentManager.beginTransaction(); 390 final Fragment installed = getInstalledFragment(); 391 if ((installed instanceof MessageViewFragment) 392 && (fragment instanceof MessageViewFragment)) { 393 // Newer/older navigation, auto-advance, etc. 394 // In this case we want to keep the backstack untouched, so that after back navigation 395 // we can restore the message list, including scroll position and batch selection. 396 } else { 397 if (DEBUG_FRAGMENTS) { 398 Log.i(Logging.LOG_TAG, this + " backstack: [push] " + getInstalledFragment() 399 + " -> " + fragment); 400 } 401 if (mPreviousFragment != null) { 402 if (DEBUG_FRAGMENTS) { 403 Log.d(Logging.LOG_TAG, this + " showFragment: destroying previous fragment " 404 + mPreviousFragment); 405 } 406 removeFragment(ft, mPreviousFragment); 407 mPreviousFragment = null; 408 } 409 // Remove the current fragment or push it into the backstack. 410 if (installed != null) { 411 if (installed instanceof MessageViewFragment) { 412 // Message view should never be pushed to the backstack. 413 if (DEBUG_FRAGMENTS) { 414 Log.d(Logging.LOG_TAG, this + " showFragment: removing " + installed); 415 } 416 ft.remove(installed); 417 } else { 418 // Other fragments should be pushed. 419 mPreviousFragment = installed; 420 if (DEBUG_FRAGMENTS) { 421 Log.d(Logging.LOG_TAG, this + " showFragment: detaching " 422 + mPreviousFragment); 423 } 424 ft.detach(mPreviousFragment); 425 } 426 } 427 } 428 // Show the new one 429 if (DEBUG_FRAGMENTS) { 430 Log.d(Logging.LOG_TAG, this + " showFragment: replacing with " + fragment); 431 } 432 ft.replace(R.id.fragment_placeholder, fragment); 433 ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 434 commitFragmentTransaction(ft); 435 } 436 437 /** 438 * @param isSystemBackKey <code>true</code> if the system back key was pressed. 439 * <code>false</code> if it's caused by the "home" icon click on the action bar. 440 * @return true if we should pop from our custom back stack. 441 */ 442 private boolean shouldPopFromBackStack(boolean isSystemBackKey) { 443 if (mPreviousFragment == null) { 444 return false; // Nothing in the back stack 445 } 446 if (mPreviousFragment instanceof MessageViewFragment) { 447 throw new IllegalStateException("Message view should never be in backstack"); 448 } 449 final Fragment installed = getInstalledFragment(); 450 if (installed == null) { 451 // If no fragment is installed right now, do nothing. 452 return false; 453 } 454 455 // Okay now we have 2 fragments; the one in the back stack and the one that's currently 456 // installed. 457 if (mPreviousFragment.getClass() == installed.getClass()) { 458 // We never want to go back to the same kind of fragment, which happens when the user 459 // is on the message list, and selects another mailbox on the action bar. 460 return false; 461 } 462 463 if (isSystemBackKey) { 464 // In other cases, the system back key should always work. 465 return true; 466 } else { 467 // Home icon press -- there are cases where we don't want it to work. 468 469 // Disallow the Message list <-> mailbox list transition 470 if ((mPreviousFragment instanceof MailboxListFragment) 471 && (installed instanceof MessageListFragment)) { 472 return false; 473 } 474 if ((mPreviousFragment instanceof MessageListFragment) 475 && (installed instanceof MailboxListFragment)) { 476 return false; 477 } 478 return true; 479 } 480 } 481 482 /** 483 * Pop from our custom back stack. 484 * 485 * TODO Delay-call the whole method and use the synchronous transaction. 486 */ 487 private void popFromBackStack() { 488 if (mPreviousFragment == null) { 489 return; 490 } 491 final FragmentTransaction ft = mFragmentManager.beginTransaction(); 492 final Fragment installed = getInstalledFragment(); 493 if (DEBUG_FRAGMENTS) { 494 Log.i(Logging.LOG_TAG, this + " backstack: [pop] " + installed + " -> " 495 + mPreviousFragment); 496 } 497 removeFragment(ft, installed); 498 499 // Restore listContext. 500 if (mPreviousFragment instanceof MailboxListFragment) { 501 setListContext(null); 502 } else if (mPreviousFragment instanceof MessageListFragment) { 503 setListContext(((MessageListFragment) mPreviousFragment).getListContext()); 504 } else { 505 throw new IllegalStateException("Message view should never be in backstack"); 506 } 507 508 ft.attach(mPreviousFragment); 509 ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE); 510 mPreviousFragment = null; 511 commitFragmentTransaction(ft); 512 return; 513 } 514 515 private void showAllMailboxes() { 516 if (!isAccountSelected()) { 517 return; // Can happen because of asynchronous fragment transactions. 518 } 519 520 openMailboxList(getUIAccountId()); 521 } 522 523 @Override 524 protected void installMailboxListFragment(MailboxListFragment fragment) { 525 stopMessageOrderManager(); 526 super.installMailboxListFragment(fragment); 527 } 528 529 @Override 530 protected void installMessageListFragment(MessageListFragment fragment) { 531 stopMessageOrderManager(); 532 super.installMessageListFragment(fragment); 533 } 534 535 @Override 536 protected long getMailboxSettingsMailboxId() { 537 return isMessageListInstalled() 538 ? getMessageListFragment().getMailboxId() 539 : Mailbox.NO_MAILBOX; 540 } 541 542 @Override 543 public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) { 544 // First, let the base class do what it has to do. 545 super.onPrepareOptionsMenu(inflater, menu); 546 547 // Then override 548 final boolean messageListVisible = isMessageListInstalled(); 549 if (!messageListVisible) { 550 menu.findItem(R.id.search).setVisible(false); 551 menu.findItem(R.id.compose).setVisible(false); 552 menu.findItem(R.id.refresh).setVisible(false); 553 menu.findItem(R.id.show_all_mailboxes).setVisible(false); 554 menu.findItem(R.id.mailbox_settings).setVisible(false); 555 } 556 557 final boolean messageViewVisible = isMessageViewInstalled(); 558 if (messageViewVisible) { 559 final MessageOrderManager om = getMessageOrderManager(); 560 menu.findItem(R.id.newer).setVisible(true); 561 menu.findItem(R.id.older).setVisible(true); 562 // orderManager shouldn't be null when the message view is installed, but just in case.. 563 menu.findItem(R.id.newer).setEnabled((om != null) && om.canMoveToNewer()); 564 menu.findItem(R.id.older).setEnabled((om != null) && om.canMoveToOlder()); 565 } 566 return true; 567 } 568 569 @Override 570 public boolean onOptionsItemSelected(MenuItem item) { 571 switch (item.getItemId()) { 572 case R.id.newer: 573 moveToNewer(); 574 return true; 575 case R.id.older: 576 moveToOlder(); 577 return true; 578 case R.id.show_all_mailboxes: 579 showAllMailboxes(); 580 return true; 581 } 582 return super.onOptionsItemSelected(item); 583 } 584 585 @Override 586 protected boolean isRefreshEnabled() { 587 // Refreshable only when an actual account is selected, and message view isn't shown. 588 // (i.e. only available on the mailbox list or the message view, but not on the combined 589 // one) 590 return isActualAccountSelected() && !isMessageViewInstalled(); 591 } 592 593 @Override 594 protected void onRefresh() { 595 if (!isRefreshEnabled()) { 596 return; 597 } 598 if (isMessageListInstalled()) { 599 mRefreshManager.refreshMessageList(getActualAccountId(), getMailboxId(), true); 600 } else { 601 mRefreshManager.refreshMailboxList(getActualAccountId()); 602 } 603 } 604 605 @Override 606 protected boolean isRefreshInProgress() { 607 if (!isRefreshEnabled()) { 608 return false; 609 } 610 if (isMessageListInstalled()) { 611 return mRefreshManager.isMessageListRefreshing(getMailboxId()); 612 } else { 613 return mRefreshManager.isMailboxListRefreshing(getActualAccountId()); 614 } 615 } 616 617 @Override protected void navigateToMessage(long messageId) { 618 openMessage(messageId); 619 } 620 621 @Override protected void updateNavigationArrows() { 622 refreshActionBar(); 623 } 624} 625