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