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