AbstractActivityController.java revision f9323cdb22acca9ed7873da90407863517ebd90b
1/******************************************************************************* 2 * Copyright (C) 2012 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 *******************************************************************************/ 17 18package com.android.mail.ui; 19 20import android.app.ActionBar; 21import android.app.ActionBar.LayoutParams; 22import android.app.Activity; 23import android.app.Dialog; 24import android.content.ContentResolver; 25import android.content.Context; 26import android.content.CursorLoader; 27import android.content.Intent; 28import android.content.Loader; 29import android.database.Cursor; 30import android.os.AsyncTask; 31import android.os.Bundle; 32import android.os.Handler; 33import android.view.ActionMode; 34import android.view.KeyEvent; 35import android.view.LayoutInflater; 36import android.view.Menu; 37import android.view.MenuInflater; 38import android.view.MenuItem; 39import android.view.MotionEvent; 40import android.view.View; 41import android.widget.LinearLayout; 42import android.widget.Toast; 43 44import com.android.mail.R; 45import com.android.mail.ConversationListContext; 46import com.android.mail.compose.ComposeActivity; 47import com.android.mail.providers.Account; 48import com.android.mail.providers.AccountCacheProvider; 49import com.android.mail.providers.Conversation; 50import com.android.mail.providers.Folder; 51import com.android.mail.providers.UIProvider; 52import com.android.mail.providers.UIProvider.AccountCapabilities; 53import com.android.mail.providers.UIProvider.LastSyncResult; 54import com.android.mail.ui.AsyncRefreshTask; 55import com.android.mail.utils.LogUtils; 56import com.android.mail.utils.Utils; 57 58/** 59 * This is an abstract implementation of the Activity Controller. This class 60 * knows how to respond to menu items, state changes, layout changes, etc. It 61 * weaves together the views and listeners, dispatching actions to the 62 * respective underlying classes. 63 * <p> 64 * Even though this class is abstract, it should provide default implementations 65 * for most, if not all the methods in the ActivityController interface. This 66 * makes the task of the subclasses easier: OnePaneActivityController and 67 * TwoPaneActivityController can be concise when the common functionality is in 68 * AbstractActivityController. 69 * </p> 70 * <p> 71 * In the Gmail codebase, this was called BaseActivityController 72 * </p> 73 */ 74public abstract class AbstractActivityController implements ActivityController { 75 private static final String SAVED_CONVERSATION = "saved-conversation"; 76 private static final String SAVED_CONVERSATION_POSITION = "saved-conv-pos"; 77 // Keys for serialization of various information in Bundles. 78 private static final String SAVED_LIST_CONTEXT = "saved-list-context"; 79 80 /** 81 * Are we on a tablet device or not. 82 */ 83 public final boolean IS_TABLET_DEVICE; 84 85 protected Account mAccount; 86 protected Folder mFolder; 87 protected ActionBarView mActionBarView; 88 protected final RestrictedActivity mActivity; 89 protected final Context mContext; 90 protected ConversationListContext mConvListContext; 91 private FetchAccountFolderTask mFetchAccountFolderTask; 92 protected Conversation mCurrentConversation; 93 94 protected ConversationListFragment mConversationListFragment; 95 /** 96 * The current mode of the application. All changes in mode are initiated by 97 * the activity controller. View mode changes are propagated to classes that 98 * attach themselves as listeners of view mode changes. 99 */ 100 protected final ViewMode mViewMode; 101 protected ContentResolver mResolver; 102 protected FolderListFragment mFolderListFragment; 103 protected ConversationViewFragment mConversationViewFragment; 104 protected boolean isLoaderInitialized = false; 105 private AsyncRefreshTask mAsyncRefreshTask; 106 107 private MenuItem mRefreshItem; 108 private MenuItem mHelpItem; 109 private View mRefreshActionView; 110 private boolean mRefreshInProgress; 111 private final Handler mHandler = new Handler(); 112 private final Runnable mInvalidateMenu = new Runnable() { 113 @Override 114 public void run() { 115 mActivity.invalidateOptionsMenu(); 116 } 117 }; 118 protected static final String LOG_TAG = new LogUtils().getLogTag(); 119 private static final int ACCOUNT_CURSOR_LOADER = 0; 120 private static final int FOLDER_CURSOR_LOADER = 1; 121 122 public AbstractActivityController(MailActivity activity, ViewMode viewMode) { 123 mActivity = activity; 124 mViewMode = viewMode; 125 mContext = activity.getApplicationContext(); 126 IS_TABLET_DEVICE = Utils.useTabletUI(mContext); 127 } 128 129 @Override 130 public synchronized void attachConversationList(ConversationListFragment fragment) { 131 // If there is an existing fragment, unregister it 132 if (mConversationListFragment != null) { 133 mViewMode.removeListener(mConversationListFragment); 134 } 135 mConversationListFragment = fragment; 136 // If the current fragment is non-null, add it as a listener. 137 if (fragment != null) { 138 mViewMode.addListener(mConversationListFragment); 139 } 140 } 141 142 @Override 143 public synchronized void attachFolderList(FolderListFragment fragment) { 144 // If there is an existing fragment, unregister it 145 if (mFolderListFragment != null) { 146 mViewMode.removeListener(mFolderListFragment); 147 } 148 mFolderListFragment = fragment; 149 if (fragment != null) { 150 mViewMode.addListener(mFolderListFragment); 151 } 152 } 153 154 @Override 155 public void attachConversationView(ConversationViewFragment conversationViewFragment) { 156 mConversationViewFragment = conversationViewFragment; 157 } 158 159 @Override 160 public void clearSubject() { 161 // TODO(viki): Auto-generated method stub 162 } 163 164 @Override 165 public void enterSearchMode() { 166 // TODO(viki): Auto-generated method stub 167 } 168 169 @Override 170 public void exitSearchMode() { 171 // TODO(viki): Auto-generated method stub 172 } 173 174 @Override 175 public Account getCurrentAccount() { 176 return mAccount; 177 } 178 179 @Override 180 public ConversationListContext getCurrentListContext() { 181 return mConvListContext; 182 } 183 184 @Override 185 public String getHelpContext() { 186 return "Mail"; 187 } 188 189 @Override 190 public int getMode() { 191 return mViewMode.getMode(); 192 } 193 194 @Override 195 public String getUnshownSubject(String subject) { 196 // Calculate how much of the subject is shown, and return the remaining. 197 return null; 198 } 199 200 @Override 201 public void handleConversationLoadError() { 202 // TODO(viki): Auto-generated method stub 203 } 204 205 @Override 206 public void handleSearchRequested() { 207 // TODO(viki): Auto-generated method stub 208 } 209 210 /** 211 * Initialize the action bar. This is not visible to OnePaneController and 212 * TwoPaneController so they cannot override this behavior. 213 */ 214 private void initCustomActionBarView() { 215 ActionBar actionBar = mActivity.getActionBar(); 216 mActionBarView = (MailActionBar) LayoutInflater.from(mContext).inflate( 217 R.layout.actionbar_view, null); 218 219 if (actionBar != null && mActionBarView != null) { 220 // Why have a different variable for the same thing? We should apply 221 // the same actions 222 // on mActionBarView instead. 223 mActionBarView.initialize(mActivity, this, mViewMode, actionBar); 224 actionBar.setCustomView((LinearLayout) mActionBarView, new ActionBar.LayoutParams( 225 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 226 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, 227 ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE); 228 } 229 } 230 231 /** 232 * Returns whether the conversation list fragment is visible or not. 233 * Different layouts will have their own notion on the visibility of 234 * fragments, so this method needs to be overriden. 235 * 236 * @return 237 */ 238 protected abstract boolean isConversationListVisible(); 239 240 @Override 241 public void onAccountChanged(Account account) { 242 if (!account.equals(mAccount)) { 243 mAccount = account; 244 // Account changed; existing folder is invalid. 245 mFolder = null; 246 fetchAccountFolderInfo(); 247 248 updateHelpMenuItem(); 249 } 250 } 251 252 private void fetchAccountFolderInfo() { 253 if (mFetchAccountFolderTask != null) { 254 mFetchAccountFolderTask.cancel(true); 255 } 256 mFetchAccountFolderTask = new FetchAccountFolderTask(); 257 mFetchAccountFolderTask.execute(); 258 } 259 260 @Override 261 public void onFolderChanged(Folder folder) { 262 if (!folder.equals(mFolder)) { 263 setFolder(folder); 264 mConvListContext = ConversationListContext.forFolder(mContext, mAccount, mFolder); 265 showConversationList(mConvListContext); 266 } 267 } 268 269 private void setFolder(Folder folder) { 270 // Start watching folder for sync status. 271 if (!folder.equals(mFolder)) { 272 mRefreshInProgress = false; 273 mFolder = folder; 274 mActionBarView.setFolder(mFolder); 275 mActivity.getLoaderManager().restartLoader(FOLDER_CURSOR_LOADER, null, this); 276 } 277 } 278 279 @Override 280 public void onActionModeFinished(ActionMode mode) { 281 // TODO(viki): Auto-generated method stub 282 } 283 284 @Override 285 public void onActionModeStarted(ActionMode mode) { 286 // TODO(viki): Auto-generated method stub 287 } 288 289 @Override 290 public void onActivityResult(int requestCode, int resultCode, Intent data) { 291 // TODO(viki): Auto-generated method stub 292 } 293 294 @Override 295 public void onConversationListVisibilityChanged(boolean visible) { 296 // TODO(viki): Auto-generated method stub 297 } 298 299 /** 300 * By default, doing nothing is right. A two-pane controller will need to 301 * override this. 302 */ 303 @Override 304 public void onConversationVisibilityChanged(boolean visible) { 305 // Do nothing. 306 return; 307 } 308 309 @Override 310 public boolean onCreate(Bundle savedState) { 311 // Initialize the action bar view. 312 initCustomActionBarView(); 313 // Allow shortcut keys to function for the ActionBar and menus. 314 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT); 315 mResolver = mActivity.getContentResolver(); 316 317 // All the individual UI components listen for ViewMode changes. This 318 // simplifies the 319 // amount of logic in the AbstractActivityController, but increases the 320 // possibility of 321 // timing-related bugs. 322 mViewMode.addListener(this); 323 assert (mActionBarView != null); 324 mViewMode.addListener(mActionBarView); 325 326 restoreState(savedState); 327 return true; 328 } 329 330 @Override 331 public Dialog onCreateDialog(int id, Bundle bundle) { 332 // TODO(viki): Auto-generated method stub 333 return null; 334 } 335 336 @Override 337 public boolean onCreateOptionsMenu(Menu menu) { 338 MenuInflater inflater = mActivity.getMenuInflater(); 339 inflater.inflate(mActionBarView.getOptionsMenuId(), menu); 340 mRefreshItem = menu.findItem(R.id.refresh); 341 mHelpItem = menu.findItem(R.id.help_info_menu_item); 342 return true; 343 } 344 345 @Override 346 public void onEndBulkOperation() { 347 // TODO(viki): Auto-generated method stub 348 349 } 350 351 @Override 352 public boolean onKeyDown(int keyCode, KeyEvent event) { 353 // TODO(viki): Auto-generated method stub 354 return false; 355 } 356 357 @Override 358 public boolean onOptionsItemSelected(MenuItem item) { 359 int id = item.getItemId(); 360 boolean handled = true; 361 switch (id) { 362 case android.R.id.home: 363 onUpPressed(); 364 break; 365 case R.id.compose: 366 ComposeActivity.compose(mActivity.getActivityContext(), mAccount); 367 break; 368 case R.id.show_all_folders: 369 showFolderList(); 370 break; 371 case R.id.refresh: 372 requestFolderRefresh(); 373 break; 374 case R.id.preferences: 375 showPreferences(); 376 break; 377 case R.id.help_info_menu_item: 378 // TODO: enable context sensitive help 379 Utils.showHelp(mActivity.getActivityContext(), mAccount.helpIntentUri, null); 380 break; 381 default: 382 handled = false; 383 break; 384 } 385 return handled; 386 } 387 388 private void requestFolderRefresh() { 389 if (mFolder != null) { 390 if (mAsyncRefreshTask != null) { 391 mAsyncRefreshTask.cancel(true); 392 } 393 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder); 394 mAsyncRefreshTask.execute(); 395 } 396 } 397 398 public void onRefreshStarted() { 399 if (!mRefreshInProgress) { 400 mRefreshInProgress = true; 401 mHandler.post(mInvalidateMenu); 402 } 403 } 404 405 public void onRefreshStopped(int status) { 406 if (mRefreshInProgress) { 407 mRefreshInProgress = false; 408 switch (status) { 409 case LastSyncResult.SUCCESS: 410 break; 411 default: 412 Context context = mActivity.getActivityContext(); 413 Toast.makeText(context, Utils.getSyncStatusText(context, status), 414 Toast.LENGTH_LONG).show(); 415 break; 416 } 417 mHandler.post(mInvalidateMenu); 418 } 419 } 420 421 @Override 422 public void onPause() { 423 isLoaderInitialized = false; 424 } 425 426 @Override 427 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) { 428 // TODO(viki): Auto-generated method stub 429 430 } 431 432 @Override 433 public boolean onPrepareOptionsMenu(Menu menu) { 434 if (mRefreshInProgress) { 435 if (mRefreshItem != null) { 436 if (mRefreshActionView == null) { 437 mRefreshItem.setActionView(R.layout.action_bar_indeterminate_progress); 438 mRefreshActionView = mRefreshItem.getActionView(); 439 } else { 440 mRefreshItem.setActionView(mRefreshActionView); 441 } 442 } 443 } else { 444 if (mRefreshItem != null) { 445 mRefreshItem.setActionView(null); 446 } 447 } 448 449 // Show/hide the help menu item 450 updateHelpMenuItem(); 451 return true; 452 } 453 454 private void updateHelpMenuItem() { 455 if (mHelpItem != null) { 456 mHelpItem.setVisible(mAccount != null 457 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT)); 458 } 459 } 460 461 @Override 462 public void onResume() { 463 if (mActionBarView != null) { 464 mActionBarView.onResume(); 465 } 466 } 467 468 @Override 469 public void onSaveInstanceState(Bundle outState) { 470 if (mConvListContext != null) { 471 outState.putBundle(SAVED_LIST_CONTEXT, mConvListContext.toBundle()); 472 } 473 } 474 475 @Override 476 public void showPreferences() { 477 final Intent preferenceIntent = new Intent(Intent.ACTION_EDIT, mAccount.settingIntentUri); 478 mActivity.startActivity(preferenceIntent); 479 } 480 481 @Override 482 public void onSearchRequested() { 483 // TODO(viki): Auto-generated method stub 484 } 485 486 @Override 487 public void onStartBulkOperation() { 488 // TODO(viki): Auto-generated method stub 489 } 490 491 @Override 492 public void onStartDragMode() { 493 // TODO(viki): Auto-generated method stub 494 } 495 496 @Override 497 public void onStop() { 498 // TODO(viki): Auto-generated method stub 499 } 500 501 @Override 502 public void onStopDragMode() { 503 // TODO(viki): Auto-generated method stub 504 } 505 506 /** 507 * {@inheritDoc} Subclasses must override this to listen to mode changes 508 * from the ViewMode. Subclasses <b>must</b> call the parent's 509 * onViewModeChanged since the parent will handle common state changes. 510 */ 511 @Override 512 public void onViewModeChanged(int newMode) { 513 // Perform any mode specific work here. 514 // reset the action bar icon based on the mode. Why don't the individual 515 // controllers do 516 // this themselves? 517 518 // In conversation list mode, clean up the conversation. 519 if (newMode == ViewMode.CONVERSATION_LIST) { 520 // Clean up the conversation here. 521 } 522 523 // We don't want to invalidate the options menu when switching to 524 // conversation 525 // mode, as it will happen when the conversation finishes loading. 526 if (newMode != ViewMode.CONVERSATION) { 527 mActivity.invalidateOptionsMenu(); 528 } 529 } 530 531 @Override 532 public void onWindowFocusChanged(boolean hasFocus) { 533 // TODO(viki): Auto-generated method stub 534 } 535 536 @Override 537 public void reloadSearch(String string) { 538 // TODO(viki): Auto-generated method stub 539 } 540 541 /** 542 * @param savedState 543 */ 544 protected void restoreListContext(Bundle savedState) { 545 // TODO(viki): Auto-generated method stub 546 Bundle listContextBundle = savedState.getBundle(SAVED_LIST_CONTEXT); 547 if (listContextBundle != null) { 548 mConvListContext = ConversationListContext.forBundle(listContextBundle); 549 } 550 } 551 552 /** 553 * Restore the state from the previous bundle. Subclasses should call this 554 * method from the parent class, since it performs important UI 555 * initialization. 556 * 557 * @param savedState 558 */ 559 protected void restoreState(Bundle savedState) { 560 if (savedState != null) { 561 restoreListContext(savedState); 562 // Attach the menu handler here. 563 564 // Restore the view mode 565 mViewMode.handleRestore(savedState); 566 } else { 567 final Intent intent = mActivity.getIntent(); 568 if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction())) { 569 if (intent.hasExtra(Utils.EXTRA_CONVERSATION)) { 570 // Open the conversation. 571 LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s", 572 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION)); 573 mAccount = ((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT)); 574 updateHelpMenuItem(); 575 mFolder = ((Folder) intent.getParcelableExtra(Utils.EXTRA_FOLDER)); 576 mConvListContext = ConversationListContext.forIntent(mContext, mAccount, 577 mActivity.getIntent()); 578 showConversationList(mConvListContext); 579 showConversation((Conversation) intent 580 .getParcelableExtra(Utils.EXTRA_CONVERSATION)); 581 } else if (intent.hasExtra(Utils.EXTRA_FOLDER)) { 582 // Open the folder. 583 LogUtils.d(LOG_TAG, "SHOW THE FOLDER at %s", 584 intent.getParcelableExtra(Utils.EXTRA_FOLDER)); 585 mAccount = ((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT)); 586 updateHelpMenuItem(); 587 onFolderChanged((Folder) intent.getParcelableExtra(Utils.EXTRA_FOLDER)); 588 } 589 } 590 // Update the active folder in the action bar. 591 mActionBarView.setFolder(mFolder); 592 } 593 // Create the accounts loader; this loads the acount switch spinner. 594 mActivity.getLoaderManager().initLoader(ACCOUNT_CURSOR_LOADER, null, this); 595 } 596 597 @Override 598 public void setSubject(String subject) { 599 // Do something useful with the subject. This requires changing the 600 // conversation view's subject text. 601 } 602 603 @Override 604 public void startActionBarStatusCursorLoader(String account) { 605 // TODO(viki): Auto-generated method stub 606 } 607 608 @Override 609 public void stopActionBarStatusCursorLoader(String account) { 610 // TODO(viki): Auto-generated method stub 611 } 612 613 @Override 614 public void toggleStar(boolean toggleOn, long conversationId, long maxMessageId) { 615 // TODO(viki): Auto-generated method stub 616 } 617 618 @Override 619 public void onConversationSelected(Conversation conversation) { 620 mCurrentConversation = conversation; 621 showConversation(mCurrentConversation); 622 mViewMode.enterConversationMode(); 623 } 624 625 /** 626 * {@inheritDoc} 627 */ 628 @Override 629 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 630 // Create a loader to listen in on account changes. 631 if (id == ACCOUNT_CURSOR_LOADER) { 632 return new CursorLoader(mContext, AccountCacheProvider.getAccountsUri(), 633 UIProvider.ACCOUNTS_PROJECTION, null, null, null); 634 } else if (id == FOLDER_CURSOR_LOADER) { 635 return new CursorLoader(mActivity.getActivityContext(), mFolder.uri, 636 UIProvider.FOLDERS_PROJECTION, null, null, null); 637 } 638 return null; 639 } 640 641 /** 642 * Return whether the given account exists in the cursor. 643 * 644 * @param accountCursor 645 * @param account 646 * @return true if the account exists in the account cursor, false 647 * otherwise. 648 */ 649 private boolean existsInCursor(Cursor accountCursor, Account account) { 650 accountCursor.moveToFirst(); 651 do { 652 if (account.equals(new Account(accountCursor))) 653 return true; 654 } while (accountCursor.moveToNext()); 655 return false; 656 } 657 658 /** 659 * Update the accounts on the device. This currently loads the first account 660 * in the list. 661 * 662 * @param loader 663 * @param data 664 * @return true if the update was successful, false otherwise 665 */ 666 private boolean updateAccounts(Loader<Cursor> loader, Cursor accounts) { 667 // Load the first account in the absence of any other information. 668 if (accounts == null || !accounts.moveToFirst()) { 669 return false; 670 } 671 Account newAccount = mAccount == null ? new Account(accounts) : mAccount; 672 onAccountChanged(newAccount); 673 final Account[] allAccounts = Account.getAllAccounts(accounts); 674 mActionBarView.setAccounts(allAccounts); 675 return (allAccounts.length > 0); 676 } 677 678 /** 679 * {@inheritDoc} 680 */ 681 @Override 682 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 683 // We want to reinitialize only if we haven't ever been initialized, or 684 // if the current account has vanished. 685 final int id = loader.getId(); 686 if (id == ACCOUNT_CURSOR_LOADER) { 687 if (!isLoaderInitialized || !existsInCursor(data, mAccount)) { 688 isLoaderInitialized = updateAccounts(loader, data); 689 } 690 } else if (id == FOLDER_CURSOR_LOADER) { 691 // Check status of the cursor. 692 if (data != null) { 693 data.moveToFirst(); 694 Folder folder = new Folder(data); 695 if (folder.isSyncInProgress()) { 696 onRefreshStarted(); 697 } else { 698 // Stop the spinner here. 699 onRefreshStopped(folder.lastSyncResult); 700 } 701 LogUtils.v(LOG_TAG, "FOLDER STATUS = " + folder.syncStatus); 702 } 703 } 704 } 705 706 /** 707 * {@inheritDoc} 708 */ 709 @Override 710 public void onLoaderReset(Loader<Cursor> loader) { 711 // Do nothing for now, since we don't have any state. When a load is 712 // finished, the 713 // onLoadFinished will be called and we will be fine. 714 } 715 716 @Override 717 public void onTouchEvent(MotionEvent event) { 718 if (event.getAction() == MotionEvent.ACTION_DOWN) { 719 int mode = mViewMode.getMode(); 720 if (mode == ViewMode.CONVERSATION_LIST) { 721 mConversationListFragment.onTouchEvent(event); 722 } else if (mode == ViewMode.CONVERSATION) { 723 mConversationViewFragment.onTouchEvent(event); 724 } 725 } 726 } 727 728 private class FetchAccountFolderTask extends AsyncTask<Void, Void, ConversationListContext> { 729 @Override 730 public ConversationListContext doInBackground(Void... params) { 731 return ConversationListContext.forFolder(mContext, mAccount, mFolder); 732 } 733 734 @Override 735 public void onPostExecute(ConversationListContext result) { 736 mConvListContext = result; 737 setFolder(mConvListContext.mFolder); 738 showConversationList(mConvListContext); 739 mFetchAccountFolderTask = null; 740 } 741 } 742} 743