UIControllerBase.java revision deb345acebce19996726262f7825c39a784b9fa8
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.FragmentManager; 22import android.app.FragmentTransaction; 23import android.os.Bundle; 24import android.util.Log; 25import android.view.Menu; 26import android.view.MenuInflater; 27import android.view.MenuItem; 28 29import com.android.email.Email; 30import com.android.email.MessageListContext; 31import com.android.email.R; 32import com.android.email.RefreshManager; 33import com.android.email.activity.setup.AccountSettings; 34import com.android.emailcommon.Logging; 35import com.android.emailcommon.provider.Account; 36import com.android.emailcommon.provider.EmailContent.Message; 37import com.android.emailcommon.provider.HostAuth; 38import com.android.emailcommon.provider.Mailbox; 39import com.android.emailcommon.utility.EmailAsyncTask; 40 41import java.util.LinkedList; 42import java.util.List; 43 44/** 45 * Base class for the UI controller. 46 * 47 * TODO Remove all the {@link MailboxFinder} stuff. It's now done in {@link Welcome}. 48 */ 49abstract class UIControllerBase implements MailboxListFragment.Callback, 50 MessageListFragment.Callback, MessageViewFragment.Callback { 51 static final boolean DEBUG_FRAGMENTS = false; // DO NOT SUBMIT WITH TRUE 52 53 /** The owner activity */ 54 final EmailActivity mActivity; 55 final FragmentManager mFragmentManager; 56 57 protected final ActionBarController mActionBarController; 58 59 final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); 60 61 final RefreshManager mRefreshManager; 62 63 /** 64 * Fragments that are installed. 65 * 66 * A fragment is installed in {@link Fragment#onActivityCreated} and uninstalled in 67 * {@link Fragment#onDestroyView}, using {@link FragmentInstallable} callbacks. 68 * 69 * This means fragments in the back stack are *not* installed. 70 * 71 * We set callbacks to fragments only when they are installed. 72 * 73 * @see FragmentInstallable 74 */ 75 private MailboxListFragment mMailboxListFragment; 76 private MessageListFragment mMessageListFragment; 77 private MessageViewFragment mMessageViewFragment; 78 79 /** 80 * To avoid double-deleting a fragment (which will cause a runtime exception), 81 * we put a fragment in this list when we {@link FragmentTransaction#remove(Fragment)} it, 82 * and remove from the list when we actually uninstall it. 83 */ 84 private final List<Fragment> mRemovedFragments = new LinkedList<Fragment>(); 85 86 /** 87 * The active context for the current MessageList. 88 * In some UI layouts such as the one-pane view, the message list may not be visible, but is 89 * on the backstack. This list context will still be accessible in those cases. 90 */ 91 protected MessageListContext mListContext; 92 93 private final RefreshManager.Listener mRefreshListener 94 = new RefreshManager.Listener() { 95 @Override 96 public void onMessagingError(final long accountId, long mailboxId, final String message) { 97 refreshActionBar(); 98 } 99 100 @Override 101 public void onRefreshStatusChanged(long accountId, long mailboxId) { 102 refreshActionBar(); 103 } 104 }; 105 106 public UIControllerBase(EmailActivity activity) { 107 mActivity = activity; 108 mFragmentManager = activity.getFragmentManager(); 109 mRefreshManager = RefreshManager.getInstance(mActivity); 110 mActionBarController = createActionBarController(activity); 111 if (DEBUG_FRAGMENTS) { 112 FragmentManager.enableDebugLogging(true); 113 } 114 } 115 116 /** 117 * Called by the base class to let a subclass create an {@link ActionBarController}. 118 */ 119 protected abstract ActionBarController createActionBarController(Activity activity); 120 121 /** @return the layout ID for the activity. */ 122 public abstract int getLayoutId(); 123 124 /** 125 * Must be called just after the activity sets up the content view. Used to initialize views. 126 * 127 * (Due to the complexity regarding class/activity initialization order, we can't do this in 128 * the constructor.) 129 */ 130 public void onActivityViewReady() { 131 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 132 Log.d(Logging.LOG_TAG, this + " onActivityViewReady"); 133 } 134 } 135 136 /** 137 * Called at the end of {@link EmailActivity#onCreate}. 138 */ 139 public void onActivityCreated() { 140 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 141 Log.d(Logging.LOG_TAG, this + " onActivityCreated"); 142 } 143 mRefreshManager.registerListener(mRefreshListener); 144 mActionBarController.onActivityCreated(); 145 } 146 147 /** 148 * Handles the {@link android.app.Activity#onStart} callback. 149 */ 150 public void onActivityStart() { 151 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 152 Log.d(Logging.LOG_TAG, this + " onActivityStart"); 153 } 154 } 155 156 /** 157 * Handles the {@link android.app.Activity#onResume} callback. 158 */ 159 public void onActivityResume() { 160 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 161 Log.d(Logging.LOG_TAG, this + " onActivityResume"); 162 } 163 refreshActionBar(); 164 } 165 166 /** 167 * Handles the {@link android.app.Activity#onPause} callback. 168 */ 169 public void onActivityPause() { 170 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 171 Log.d(Logging.LOG_TAG, this + " onActivityPause"); 172 } 173 } 174 175 /** 176 * Handles the {@link android.app.Activity#onStop} callback. 177 */ 178 public void onActivityStop() { 179 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 180 Log.d(Logging.LOG_TAG, this + " onActivityStop"); 181 } 182 } 183 184 /** 185 * Handles the {@link android.app.Activity#onDestroy} callback. 186 */ 187 public void onActivityDestroy() { 188 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 189 Log.d(Logging.LOG_TAG, this + " onActivityDestroy"); 190 } 191 mActionBarController.onActivityDestroy(); 192 mRefreshManager.unregisterListener(mRefreshListener); 193 mTaskTracker.cancellAllInterrupt(); 194 } 195 196 /** 197 * Handles the {@link android.app.Activity#onSaveInstanceState} callback. 198 */ 199 public void onSaveInstanceState(Bundle outState) { 200 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 201 Log.d(Logging.LOG_TAG, this + " onSaveInstanceState"); 202 } 203 mActionBarController.onSaveInstanceState(outState); 204 } 205 206 /** 207 * Handles the {@link android.app.Activity#onRestoreInstanceState} callback. 208 */ 209 public void onRestoreInstanceState(Bundle savedInstanceState) { 210 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 211 Log.d(Logging.LOG_TAG, this + " restoreInstanceState"); 212 } 213 mActionBarController.onRestoreInstanceState(savedInstanceState); 214 } 215 216 /** 217 * Install a fragment. Must be caleld from the host activity's 218 * {@link FragmentInstallable#onInstallFragment}. 219 */ 220 public final void onInstallFragment(Fragment fragment) { 221 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 222 Log.d(Logging.LOG_TAG, this + " onInstallFragment fragment=" + fragment); 223 } 224 if (fragment instanceof MailboxListFragment) { 225 installMailboxListFragment((MailboxListFragment) fragment); 226 } else if (fragment instanceof MessageListFragment) { 227 installMessageListFragment((MessageListFragment) fragment); 228 } else if (fragment instanceof MessageViewFragment) { 229 installMessageViewFragment((MessageViewFragment) fragment); 230 } else { 231 throw new IllegalArgumentException("Tried to install unknown fragment"); 232 } 233 } 234 235 /** Install fragment */ 236 protected void installMailboxListFragment(MailboxListFragment fragment) { 237 mMailboxListFragment = fragment; 238 mMailboxListFragment.setCallback(this); 239 refreshActionBar(); 240 } 241 242 /** Install fragment */ 243 protected void installMessageListFragment(MessageListFragment fragment) { 244 mMessageListFragment = fragment; 245 mMessageListFragment.setCallback(this); 246 refreshActionBar(); 247 } 248 249 /** Install fragment */ 250 protected void installMessageViewFragment(MessageViewFragment fragment) { 251 mMessageViewFragment = fragment; 252 mMessageViewFragment.setCallback(this); 253 refreshActionBar(); 254 } 255 256 /** 257 * Uninstall a fragment. Must be caleld from the host activity's 258 * {@link FragmentInstallable#onUninstallFragment}. 259 */ 260 public final void onUninstallFragment(Fragment fragment) { 261 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 262 Log.d(Logging.LOG_TAG, this + " onUninstallFragment fragment=" + fragment); 263 } 264 mRemovedFragments.remove(fragment); 265 if (fragment == mMailboxListFragment) { 266 uninstallMailboxListFragment(); 267 } else if (fragment == mMessageListFragment) { 268 uninstallMessageListFragment(); 269 } else if (fragment == mMessageViewFragment) { 270 uninstallMessageViewFragment(); 271 } else { 272 throw new IllegalArgumentException("Tried to uninstall unknown fragment"); 273 } 274 } 275 276 /** Uninstall {@link MailboxListFragment} */ 277 protected void uninstallMailboxListFragment() { 278 mMailboxListFragment.setCallback(null); 279 mMailboxListFragment = null; 280 } 281 282 /** Uninstall {@link MessageListFragment} */ 283 protected void uninstallMessageListFragment() { 284 mMessageListFragment.setCallback(null); 285 mMessageListFragment = null; 286 } 287 288 /** Uninstall {@link MessageViewFragment} */ 289 protected void uninstallMessageViewFragment() { 290 mMessageViewFragment.setCallback(null); 291 mMessageViewFragment = null; 292 } 293 294 /** 295 * If a {@link Fragment} is not already in {@link #mRemovedFragments}, 296 * {@link FragmentTransaction#remove} it and add to the list. 297 * 298 * Do nothing if {@code fragment} is null. 299 */ 300 protected final void removeFragment(FragmentTransaction ft, Fragment fragment) { 301 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 302 Log.d(Logging.LOG_TAG, this + " removeFragment fragment=" + fragment); 303 } 304 if (fragment == null) { 305 return; 306 } 307 if (!mRemovedFragments.contains(fragment)) { 308 ft.remove(fragment); 309 addFragmentToRemovalList(fragment); 310 } 311 } 312 313 /** 314 * Remove a {@link Fragment} from {@link #mRemovedFragments}. No-op if {@code fragment} is 315 * null. 316 * 317 * {@link #removeMailboxListFragment}, {@link #removeMessageListFragment} and 318 * {@link #removeMessageViewFragment} all call this, so subclasses don't have to do this when 319 * using them. 320 * 321 * However, unfortunately, subclasses have to call this manually when popping from the 322 * back stack to avoid double-delete. 323 */ 324 protected void addFragmentToRemovalList(Fragment fragment) { 325 if (fragment != null) { 326 mRemovedFragments.add(fragment); 327 } 328 } 329 330 /** 331 * Remove the fragment if it's installed. 332 */ 333 protected FragmentTransaction removeMailboxListFragment(FragmentTransaction ft) { 334 removeFragment(ft, mMailboxListFragment); 335 return ft; 336 } 337 338 /** 339 * Remove the fragment if it's installed. 340 */ 341 protected FragmentTransaction removeMessageListFragment(FragmentTransaction ft) { 342 removeFragment(ft, mMessageListFragment); 343 return ft; 344 } 345 346 /** 347 * Remove the fragment if it's installed. 348 */ 349 protected FragmentTransaction removeMessageViewFragment(FragmentTransaction ft) { 350 removeFragment(ft, mMessageViewFragment); 351 return ft; 352 } 353 354 /** @return true if a {@link MailboxListFragment} is installed. */ 355 protected final boolean isMailboxListInstalled() { 356 return mMailboxListFragment != null; 357 } 358 359 /** @return true if a {@link MessageListFragment} is installed. */ 360 protected final boolean isMessageListInstalled() { 361 return mMessageListFragment != null; 362 } 363 364 /** @return true if a {@link MessageViewFragment} is installed. */ 365 protected final boolean isMessageViewInstalled() { 366 return mMessageViewFragment != null; 367 } 368 369 /** @return the installed {@link MailboxListFragment} or null. */ 370 protected final MailboxListFragment getMailboxListFragment() { 371 return mMailboxListFragment; 372 } 373 374 /** @return the installed {@link MessageListFragment} or null. */ 375 protected final MessageListFragment getMessageListFragment() { 376 return mMessageListFragment; 377 } 378 379 /** @return the installed {@link MessageViewFragment} or null. */ 380 protected final MessageViewFragment getMessageViewFragment() { 381 return mMessageViewFragment; 382 } 383 384 /** 385 * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 386 * 387 * @see #getActualAccountId() 388 */ 389 public abstract long getUIAccountId(); 390 391 /** 392 * @return true if an account is selected, or the current view is the combined view. 393 */ 394 public final boolean isAccountSelected() { 395 return getUIAccountId() != Account.NO_ACCOUNT; 396 } 397 398 /** 399 * @return if an actual account is selected. (i.e. {@link Account#ACCOUNT_ID_COMBINED_VIEW} 400 * is not considered "actual".s) 401 */ 402 public final boolean isActualAccountSelected() { 403 return isAccountSelected() && (getUIAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW); 404 } 405 406 /** 407 * @return the currently selected account ID. If the current view is the combined view, 408 * it'll return {@link Account#NO_ACCOUNT}. 409 * 410 * @see #getUIAccountId() 411 */ 412 public final long getActualAccountId() { 413 return isActualAccountSelected() ? getUIAccountId() : Account.NO_ACCOUNT; 414 } 415 416 /** 417 * Show the default view for the given account. 418 * 419 * No-op if the given account is already selected. 420 * 421 * @param accountId ID of the account to load. Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 422 * Must never be {@link Account#NO_ACCOUNT}. 423 */ 424 public final void switchAccount(long accountId) { 425 426 // STOPSHIP Do the security hold check here too. 427 428 if (accountId == getUIAccountId()) { 429 // Do nothing if the account is already selected. Not even going back to the inbox. 430 return; 431 } 432 433 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 434 openMailbox(accountId, Mailbox.QUERY_ALL_INBOXES); 435 } else { 436 long inboxId = Mailbox.findMailboxOfType(mActivity, accountId, Mailbox.TYPE_INBOX); 437 if (inboxId == Mailbox.NO_MAILBOX) { 438 // The account doesn't have Inbox yet... Redirect to Welcome and let it wait for 439 // the initial sync... 440 Log.w(Logging.LOG_TAG, "Account " + accountId +" doesn't have Inbox. Redirecting" 441 + " to Welcome..."); 442 Welcome.actionOpenAccountInbox(mActivity, accountId); 443 mActivity.finish(); 444 return; 445 } else { 446 openMailbox(accountId, inboxId); 447 } 448 } 449 } 450 451 /** 452 * Returns the id of the parent mailbox used for the mailbox list fragment. 453 * 454 * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with 455 * {@link #getMessageListMailboxId()} 456 */ 457 protected long getMailboxListMailboxId() { 458 return isMailboxListInstalled() ? getMailboxListFragment().getSelectedMailboxId() 459 : Mailbox.NO_MAILBOX; 460 } 461 462 /** 463 * Returns the id of the mailbox used for the message list fragment. 464 * 465 * IMPORTANT: Do not confuse {@link #getMailboxListMailboxId()} with 466 * {@link #getMessageListMailboxId()} 467 */ 468 protected long getMessageListMailboxId() { 469 return isMessageListInstalled() ? getMessageListFragment().getMailboxId() 470 : Mailbox.NO_MAILBOX; 471 } 472 473 /** 474 * Shortcut for {@link #open} with {@link Message#NO_MESSAGE}. 475 */ 476 protected final void openMailbox(long accountId, long mailboxId) { 477 open(MessageListContext.forMailbox(accountId, mailboxId), Message.NO_MESSAGE); 478 } 479 480 /** 481 * Opens a given list 482 * @param listContext the list context for the message list to open 483 * @param messageId if specified and not {@link Message#NO_MESSAGE}, will open the message 484 * in the message list. 485 */ 486 public final void open(final MessageListContext listContext, final long messageId) { 487 mListContext = listContext; 488 openInternal(listContext, messageId); 489 490 if (mListContext.isSearch()) { 491 mActionBarController.enterSearchMode(mListContext.getSearchParams().mFilter); 492 } 493 } 494 495 protected abstract void openInternal( 496 final MessageListContext listContext, final long messageId); 497 498 /** 499 * Performs the back action. 500 * 501 * NOTE The method in the base class has precedence. Subclasses overriding this method MUST 502 * call super's method first. 503 * 504 * @param isSystemBackKey <code>true</code> if the system back key was pressed. 505 * <code>false</code> if it's caused by the "home" icon click on the action bar. 506 */ 507 public boolean onBackPressed(boolean isSystemBackKey) { 508 if (mActionBarController.onBackPressed(isSystemBackKey)) { 509 return true; 510 } 511 return false; 512 } 513 514 /** 515 * Must be called from {@link Activity#onSearchRequested()}. 516 * This initiates the search entry mode - see {@link #onSearchSubmit} for when the search 517 * is actually submitted. 518 */ 519 public void onSearchRequested() { 520 mActionBarController.enterSearchMode(null); 521 } 522 523 /** @return true if the search menu option should be enabled based on the current UI. */ 524 protected boolean canSearch() { 525 return false; 526 } 527 528 /** 529 * Kicks off a search query, if the UI is in a state where a search is possible. 530 */ 531 protected void onSearchSubmit(final String queryTerm) { 532 final long accountId = getUIAccountId(); 533 if (!Account.isNormalAccount(accountId)) { 534 return; // Invalid account to search from. 535 } 536 537 // TODO: do a global search for EAS inbox. 538 // TODO: handle doing another search from a search result, in which case we should 539 // search the original mailbox that was searched, and not search in the search mailbox 540 final long mailboxId = getMessageListMailboxId(); 541 542 if (Email.DEBUG) { 543 Log.d(Logging.LOG_TAG, "Submitting search: " + queryTerm); 544 } 545 546 mActivity.startActivity(EmailActivity.createSearchIntent( 547 mActivity, accountId, mailboxId, queryTerm)); 548 549 550 // TODO: this causes a slight flicker. 551 // A new instance of the activity will sit on top. When the user exits search and 552 // returns to this activity, the search box should not be open then. 553 mActionBarController.exitSearchMode(); 554 } 555 556 /** 557 * Handles exiting of search entry mode. 558 */ 559 protected void onSearchExit() { 560 if ((mListContext != null) && mListContext.isSearch()) { 561 mActivity.finish(); 562 } 563 } 564 565 /** 566 * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback. 567 */ 568 public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) { 569 inflater.inflate(R.menu.email_activity_options, menu); 570 return true; 571 } 572 573 /** 574 * Handles the {@link android.app.Activity#onPrepareOptionsMenu} callback. 575 */ 576 public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) { 577 578 // Update the refresh button. 579 MenuItem item = menu.findItem(R.id.refresh); 580 if (isRefreshEnabled()) { 581 item.setVisible(true); 582 if (isRefreshInProgress()) { 583 item.setActionView(R.layout.action_bar_indeterminate_progress); 584 } else { 585 item.setActionView(null); 586 } 587 } else { 588 item.setVisible(false); 589 } 590 591 // Deal with protocol-specific menu options. 592 boolean isEas = false; 593 boolean accountSearchable = false; 594 long accountId = getActualAccountId(); 595 if (accountId > 0) { 596 Account account = Account.restoreAccountWithId(mActivity, accountId); 597 if (account != null) { 598 String protocol = account.getProtocol(mActivity); 599 if (HostAuth.SCHEME_EAS.equals(protocol)) { 600 isEas = true; 601 } 602 accountSearchable = (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0; 603 } 604 } 605 606 // TODO: Should use an isSyncable call to prevent drafts/outbox from allowing this 607 menu.findItem(R.id.search).setVisible(accountSearchable && canSearch()); 608 menu.findItem(R.id.sync_lookback).setVisible(isEas); 609 menu.findItem(R.id.sync_frequency).setVisible(isEas); 610 611 return true; 612 } 613 614 /** 615 * Handles the {@link android.app.Activity#onOptionsItemSelected} callback. 616 * 617 * @return true if the option item is handled. 618 */ 619 public boolean onOptionsItemSelected(MenuItem item) { 620 switch (item.getItemId()) { 621 case android.R.id.home: 622 // Comes from the action bar when the app icon on the left is pressed. 623 // It works like a back press, but it won't close the activity. 624 return onBackPressed(false); 625 case R.id.compose: 626 return onCompose(); 627 case R.id.refresh: 628 onRefresh(); 629 return true; 630 case R.id.account_settings: 631 return onAccountSettings(); 632 case R.id.search: 633 onSearchRequested(); 634 return true; 635 } 636 return false; 637 } 638 639 /** 640 * Opens the message compose activity. 641 */ 642 private boolean onCompose() { 643 if (!isAccountSelected()) { 644 return false; // this shouldn't really happen 645 } 646 MessageCompose.actionCompose(mActivity, getActualAccountId()); 647 return true; 648 } 649 650 /** 651 * Handles the "Settings" option item. Opens the settings activity. 652 */ 653 private boolean onAccountSettings() { 654 AccountSettings.actionSettings(mActivity, getActualAccountId()); 655 return true; 656 } 657 658 /** 659 * @return the ID of the message in focus and visible, if any. Returns 660 * {@link Message#NO_MESSAGE} if no message is opened. 661 */ 662 protected long getMessageId() { 663 return isMessageViewInstalled() 664 ? getMessageViewFragment().getMessageId() 665 : Message.NO_MESSAGE; 666 } 667 668 669 /** 670 * STOPSHIP For experimental UI. Remove this. 671 * 672 * @return mailbox ID for "mailbox settings" option. 673 */ 674 public abstract long getMailboxSettingsMailboxId(); 675 676 /** 677 * STOPSHIP For experimental UI. Make it abstract protected. 678 * 679 * Performs "refesh". 680 */ 681 public abstract void onRefresh(); 682 683 /** 684 * @return true if refresh is in progress for the current mailbox. 685 */ 686 protected abstract boolean isRefreshInProgress(); 687 688 /** 689 * @return true if the UI should enable the "refresh" command. 690 */ 691 protected abstract boolean isRefreshEnabled(); 692 693 /** 694 * Refresh the action bar and menu items, including the "refreshing" icon. 695 */ 696 protected void refreshActionBar() { 697 if (mActionBarController != null) { 698 mActionBarController.refresh(); 699 } 700 mActivity.invalidateOptionsMenu(); 701 } 702 703 704 @Override 705 public String toString() { 706 return getClass().getSimpleName(); // Shown on logcat 707 } 708} 709