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