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