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