AbstractActivityController.java revision e25998f8c3d20b37682cfe00aadb4a70c81eb8e4
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.AlertDialog; 24import android.app.Dialog; 25import android.app.LoaderManager; 26import android.app.SearchManager; 27import android.content.ClipData; 28import android.content.ContentResolver; 29import android.content.Context; 30import android.content.CursorLoader; 31import android.content.DialogInterface; 32import android.content.Intent; 33import android.content.Loader; 34import android.database.Cursor; 35import android.net.Uri; 36import android.os.Bundle; 37import android.os.Handler; 38import android.provider.SearchRecentSuggestions; 39import android.view.DragEvent; 40import android.view.KeyEvent; 41import android.view.LayoutInflater; 42import android.view.Menu; 43import android.view.MenuInflater; 44import android.view.MenuItem; 45import android.view.MotionEvent; 46import android.widget.Toast; 47 48import com.android.mail.ConversationListContext; 49import com.android.mail.R; 50import com.android.mail.browse.ConversationCursor; 51import com.android.mail.browse.ConversationCursor.ConversationListener; 52import com.android.mail.browse.SelectedConversationsActionMenu; 53import com.android.mail.compose.ComposeActivity; 54import com.android.mail.providers.Account; 55import com.android.mail.providers.Conversation; 56import com.android.mail.providers.Folder; 57import com.android.mail.providers.MailAppProvider; 58import com.android.mail.providers.Settings; 59import com.android.mail.providers.SuggestionsProvider; 60import com.android.mail.providers.UIProvider; 61import com.android.mail.providers.UIProvider.AccountCursorExtraKeys; 62import com.android.mail.providers.UIProvider.AutoAdvance; 63import com.android.mail.providers.UIProvider.ConversationColumns; 64import com.android.mail.providers.UIProvider.FolderCapabilities; 65import com.android.mail.utils.LogUtils; 66import com.android.mail.utils.Utils; 67import com.google.common.collect.ImmutableList; 68import com.google.common.collect.Lists; 69import com.google.common.collect.Sets; 70 71import java.util.ArrayList; 72import java.util.Collection; 73import java.util.HashSet; 74import java.util.Map; 75import java.util.Set; 76 77 78/** 79 * This is an abstract implementation of the Activity Controller. This class 80 * knows how to respond to menu items, state changes, layout changes, etc. It 81 * weaves together the views and listeners, dispatching actions to the 82 * respective underlying classes. 83 * <p> 84 * Even though this class is abstract, it should provide default implementations 85 * for most, if not all the methods in the ActivityController interface. This 86 * makes the task of the subclasses easier: OnePaneActivityController and 87 * TwoPaneActivityController can be concise when the common functionality is in 88 * AbstractActivityController. 89 * </p> 90 * <p> 91 * In the Gmail codebase, this was called BaseActivityController 92 * </p> 93 */ 94public abstract class AbstractActivityController implements ActivityController, ConversationListener { 95 // Keys for serialization of various information in Bundles. 96 private static final String SAVED_ACCOUNT = "saved-account"; 97 private static final String SAVED_FOLDER = "saved-folder"; 98 private static final String SAVED_CONVERSATION = "saved-conversation"; 99 // Batch conversations stored in the Bundle using this key. 100 private static final String SAVED_CONVERSATIONS = "saved-conversations"; 101 102 /** Are we on a tablet device or not. */ 103 public final boolean IS_TABLET_DEVICE; 104 105 protected Account mAccount; 106 protected Folder mFolder; 107 protected ActionBarView mActionBarView; 108 protected final RestrictedActivity mActivity; 109 protected final Context mContext; 110 protected final RecentFolderList mRecentFolderList; 111 protected ConversationListContext mConvListContext; 112 protected Conversation mCurrentConversation; 113 114 /** A {@link android.content.BroadcastReceiver} that suppresses new e-mail notifications. */ 115 private SuppressNotificationReceiver mNewEmailReceiver = null; 116 117 protected Handler mHandler = new Handler(); 118 protected ConversationListFragment mConversationListFragment; 119 /** 120 * The current mode of the application. All changes in mode are initiated by 121 * the activity controller. View mode changes are propagated to classes that 122 * attach themselves as listeners of view mode changes. 123 */ 124 protected final ViewMode mViewMode; 125 protected ContentResolver mResolver; 126 protected FolderListFragment mFolderListFragment; 127 protected ConversationViewFragment mConversationViewFragment; 128 protected boolean isLoaderInitialized = false; 129 private AsyncRefreshTask mAsyncRefreshTask; 130 131 private final Set<Uri> mCurrentAccountUris = Sets.newHashSet(); 132 protected Settings mCachedSettings; 133 protected ConversationCursor mConversationListCursor; 134 protected boolean mConversationListenerAdded = false; 135 /** 136 * Selected conversations, if any. 137 */ 138 private final ConversationSelectionSet mSelectedSet = new ConversationSelectionSet(); 139 140 /** 141 * Action menu associated with the selected set. 142 */ 143 SelectedConversationsActionMenu mCabActionMenu; 144 145 protected static final String LOG_TAG = new LogUtils().getLogTag(); 146 /** Constants used to differentiate between the types of loaders. */ 147 private static final int LOADER_ACCOUNT_CURSOR = 0; 148 private static final int LOADER_FOLDER_CURSOR = 2; 149 private static final int LOADER_RECENT_FOLDERS = 3; 150 private static final int LOADER_CONVERSATION_LIST = 4; 151 private static final int LOADER_ACCOUNT_INBOX = 5; 152 private static final int LOADER_SEARCH = 6; 153 154 155 private static final int ADD_ACCOUNT_REQUEST_CODE = 1; 156 157 public AbstractActivityController(MailActivity activity, ViewMode viewMode) { 158 mActivity = activity; 159 mViewMode = viewMode; 160 mContext = activity.getApplicationContext(); 161 IS_TABLET_DEVICE = Utils.useTabletUI(mContext); 162 mRecentFolderList = new RecentFolderList(mContext, this); 163 // Allow the fragment to observe changes to its own selection set. No other object is 164 // aware of the selected set. 165 mSelectedSet.addObserver(this); 166 } 167 168 @Override 169 public synchronized void attachConversationList(ConversationListFragment fragment) { 170 // If there is an existing fragment, unregister it 171 if (mConversationListFragment != null) { 172 mViewMode.removeListener(mConversationListFragment); 173 } 174 mConversationListFragment = fragment; 175 // If the current fragment is non-null, add it as a listener. 176 if (fragment != null) { 177 mViewMode.addListener(mConversationListFragment); 178 } 179 } 180 181 @Override 182 public synchronized void attachFolderList(FolderListFragment fragment) { 183 // If there is an existing fragment, unregister it 184 if (mFolderListFragment != null) { 185 mViewMode.removeListener(mFolderListFragment); 186 } 187 mFolderListFragment = fragment; 188 if (fragment != null) { 189 mViewMode.addListener(mFolderListFragment); 190 } 191 } 192 193 @Override 194 public void attachConversationView(ConversationViewFragment conversationViewFragment) { 195 mConversationViewFragment = conversationViewFragment; 196 } 197 198 @Override 199 public void clearSubject() { 200 // TODO(viki): Auto-generated method stub 201 } 202 203 @Override 204 public Account getCurrentAccount() { 205 return mAccount; 206 } 207 208 @Override 209 public ConversationListContext getCurrentListContext() { 210 return mConvListContext; 211 } 212 213 @Override 214 public String getHelpContext() { 215 return "Mail"; 216 } 217 218 @Override 219 public int getMode() { 220 return mViewMode.getMode(); 221 } 222 223 @Override 224 public String getUnshownSubject(String subject) { 225 // Calculate how much of the subject is shown, and return the remaining. 226 return null; 227 } 228 229 @Override 230 public void handleConversationLoadError() { 231 // TODO(viki): Auto-generated method stub 232 } 233 234 @Override 235 public ConversationCursor getConversationListCursor() { 236 return mConversationListCursor; 237 } 238 239 @Override 240 public void initConversationListCursor() { 241 mActivity.getLoaderManager().restartLoader(LOADER_CONVERSATION_LIST, Bundle.EMPTY, 242 new LoaderManager.LoaderCallbacks<ConversationCursor>() { 243 244 @Override 245 public void onLoadFinished(Loader<ConversationCursor> loader, 246 ConversationCursor data) { 247 mConversationListCursor = data; 248 if (mConversationListCursor.isRefreshReady()) { 249 mConversationListCursor.sync(); 250 refreshAdapter(); 251 } 252 if (mConversationListFragment != null) { 253 mConversationListFragment.onCursorUpdated(); 254 if (!mConversationListenerAdded) { 255 // TODO(mindyp): when we move to the cursor loader, we need 256 // to add/remove the listener when we create/ destroy loaders. 257 mConversationListCursor 258 .addListener(AbstractActivityController.this); 259 mConversationListenerAdded = true; 260 } 261 } 262 if (shouldShowFirstConversation()) { 263 if (mConversationListCursor.getCount() > 0) { 264 mConversationListCursor.moveToPosition(0); 265 mConversationListFragment.getListView().setItemChecked(0, true); 266 Conversation conv = new Conversation(mConversationListCursor); 267 conv.position = 0; 268 onConversationSelected(conv); 269 } 270 } 271 272 } 273 274 @Override 275 public void onLoaderReset(Loader<ConversationCursor> loader) { 276 if (mConversationListFragment == null) { 277 return; 278 } 279 mConversationListFragment.onCursorUpdated(); 280 } 281 282 @Override 283 public Loader<ConversationCursor> onCreateLoader(int id, Bundle args) { 284 return new ConversationCursorLoader((Activity) mActivity, mAccount, 285 UIProvider.CONVERSATION_PROJECTION, mFolder.conversationListUri); 286 } 287 288 }); 289 } 290 291 /** 292 * Initialize the action bar. This is not visible to OnePaneController and 293 * TwoPaneController so they cannot override this behavior. 294 */ 295 private void initCustomActionBarView() { 296 ActionBar actionBar = mActivity.getActionBar(); 297 mActionBarView = (ActionBarView) LayoutInflater.from(mContext).inflate( 298 R.layout.actionbar_view, null); 299 if (actionBar != null && mActionBarView != null) { 300 // Why have a different variable for the same thing? We should apply 301 // the same actions 302 // on mActionBarView instead. 303 mActionBarView.initialize(mActivity, this, mViewMode, actionBar, mRecentFolderList); 304 actionBar.setCustomView(mActionBarView, new ActionBar.LayoutParams( 305 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 306 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, 307 ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_TITLE); 308 } 309 } 310 311 /** 312 * Returns whether the conversation list fragment is visible or not. 313 * Different layouts will have their own notion on the visibility of 314 * fragments, so this method needs to be overriden. 315 * 316 * @return 317 */ 318 protected abstract boolean isConversationListVisible(); 319 320 @Override 321 public void onAccountChanged(Account account) { 322 if (!account.equals(mAccount)) { 323 // Current account is different from the new account, restart loaders and show 324 // the account Inbox. 325 mAccount = account; 326 mFolder = null; 327 onSettingsChanged(mAccount.settings); 328 mActionBarView.setAccount(mAccount); 329 loadAccountInbox(); 330 331 mRecentFolderList.setCurrentAccount(account); 332 restartOptionalLoader(LOADER_RECENT_FOLDERS); 333 mActivity.invalidateOptionsMenu(); 334 335 disableNotificationsOnAccountChange(mAccount); 336 337 MailAppProvider.getInstance().setLastViewedAccount(mAccount.uri.toString()); 338 } else { 339 // Current account is the same as the new account. Load the default inbox if the 340 // current inbox is not the same as the default inbox. 341 final Uri oldUri = mFolder != null ? mFolder.uri : Uri.EMPTY; 342 final Uri newUri = getDefaultInboxUri(mCachedSettings); 343 if ((mFolder == null || mFolder.type == UIProvider.FolderType.INBOX) 344 && !oldUri.equals(newUri)) { 345 loadAccountInbox(); 346 } 347 } 348 } 349 350 /** 351 * Returns the URI of the current account's default inbox if available, otherwise 352 * returns the empty URI {@link Uri#EMPTY} 353 * @return 354 */ 355 private Uri getDefaultInboxUri(Settings settings) { 356 if (settings != null && settings.defaultInbox != null) { 357 return settings.defaultInbox; 358 } 359 return Uri.EMPTY; 360 } 361 362 public void onSettingsChanged(Settings settings) { 363 final Uri oldUri = getDefaultInboxUri(mCachedSettings); 364 final Uri newUri = getDefaultInboxUri(settings); 365 mCachedSettings = settings; 366 resetActionBarIcon(); 367 // Only restart the loader if the defaultInboxUri is not the same as 368 // the folder we are already loading. 369 final boolean changed = !oldUri.equals(newUri); 370 if (settings != null 371 && settings.defaultInbox != null 372 && (mFolder == null 373 // we really only want CHANGES to the inbox setting, not just 374 // the first setting of it. 375 || (mFolder.type == UIProvider.FolderType.INBOX && !oldUri.equals(Uri.EMPTY)) 376 && changed)) { 377 loadAccountInbox(); 378 } 379 } 380 381 @Override 382 public Settings getSettings() { 383 return mCachedSettings; 384 } 385 386 private void fetchSearchFolder(Intent intent) { 387 Bundle args = new Bundle(); 388 args.putString(ConversationListContext.EXTRA_SEARCH_QUERY, intent 389 .getStringExtra(ConversationListContext.EXTRA_SEARCH_QUERY)); 390 mActivity.getLoaderManager().restartLoader(LOADER_SEARCH, args, this); 391 } 392 393 @Override 394 public void onFolderChanged(Folder folder) { 395 if (folder != null && !folder.equals(mFolder)) { 396 setFolder(folder); 397 mConvListContext = ConversationListContext.forFolder(mContext, mAccount, mFolder); 398 showConversationList(mConvListContext); 399 400 // Add the folder that we were viewing to the recent folders list. 401 // TODO: this may need to be fine tuned. If this is the signal that is indicating that 402 // the list is shown to the user, this could fire in one pane if the user goes directly 403 // to a conversation 404 updateRecentFolderList(); 405 } 406 } 407 408 /** 409 * Update the recent folders. This only needs to be done once when accessing a new folder. 410 */ 411 private void updateRecentFolderList() { 412 if (mFolder != null) { 413 mRecentFolderList.touchFolder(mFolder, mAccount); 414 } 415 } 416 417 // TODO(mindyp): set this up to store a copy of the folder as a transient 418 // field in the account. 419 protected void loadAccountInbox() { 420 restartOptionalLoader(LOADER_ACCOUNT_INBOX); 421 } 422 423 /** Set the current folder */ 424 private void setFolder(Folder folder) { 425 // Start watching folder for sync status. 426 if (folder != null && !folder.equals(mFolder)) { 427 mActionBarView.setRefreshInProgress(false); 428 mFolder = folder; 429 mActionBarView.setFolder(mFolder); 430 mActivity.getLoaderManager().restartLoader(LOADER_FOLDER_CURSOR, null, this); 431 initConversationListCursor(); 432 } else if (folder == null) { 433 LogUtils.wtf(LOG_TAG, "Folder in setFolder is null"); 434 } 435 } 436 437 @Override 438 public void onActivityResult(int requestCode, int resultCode, Intent data) { 439 if (requestCode == ADD_ACCOUNT_REQUEST_CODE) { 440 // We were waiting for the user to create an account 441 if (resultCode == Activity.RESULT_OK) { 442 // restart the loader to get the updated list of accounts 443 mActivity.getLoaderManager().initLoader( 444 LOADER_ACCOUNT_CURSOR, null, this); 445 } else { 446 // The user failed to create an account, just exit the app 447 mActivity.finish(); 448 } 449 } 450 } 451 452 @Override 453 public void onConversationListVisibilityChanged(boolean visible) { 454 // TODO(viki): Auto-generated method stub 455 } 456 457 /** 458 * By default, doing nothing is right. A two-pane controller will need to 459 * override this. 460 */ 461 @Override 462 public void onConversationVisibilityChanged(boolean visible) { 463 // Do nothing. 464 return; 465 } 466 467 @Override 468 public boolean onCreate(Bundle savedState) { 469 // Initialize the action bar view. 470 initCustomActionBarView(); 471 // Allow shortcut keys to function for the ActionBar and menus. 472 mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT); 473 mResolver = mActivity.getContentResolver(); 474 475 mNewEmailReceiver = new SuppressNotificationReceiver(); 476 477 // All the individual UI components listen for ViewMode changes. This 478 // simplifies the amount of logic in the AbstractActivityController, but increases the 479 // possibility of timing-related bugs. 480 mViewMode.addListener(this); 481 assert (mActionBarView != null); 482 mViewMode.addListener(mActionBarView); 483 484 restoreState(savedState); 485 return true; 486 } 487 488 @Override 489 public Dialog onCreateDialog(int id, Bundle bundle) { 490 return null; 491 } 492 493 @Override 494 public boolean onCreateOptionsMenu(Menu menu) { 495 MenuInflater inflater = mActivity.getMenuInflater(); 496 inflater.inflate(mActionBarView.getOptionsMenuId(), menu); 497 mActionBarView.onCreateOptionsMenu(menu); 498 return true; 499 } 500 501 @Override 502 public boolean onKeyDown(int keyCode, KeyEvent event) { 503 // TODO(viki): Auto-generated method stub 504 return false; 505 } 506 507 @Override 508 public boolean onOptionsItemSelected(MenuItem item) { 509 final int id = item.getItemId(); 510 boolean handled = true; 511 switch (id) { 512 case android.R.id.home: 513 onUpPressed(); 514 break; 515 case R.id.compose: 516 ComposeActivity.compose(mActivity.getActivityContext(), mAccount); 517 break; 518 case R.id.show_all_folders: 519 showFolderList(); 520 break; 521 case R.id.refresh: 522 requestFolderRefresh(); 523 break; 524 case R.id.settings: 525 Utils.showSettings(mActivity.getActivityContext(), mAccount); 526 break; 527 case R.id.folder_options: 528 Utils.showFolderSettings(mActivity.getActivityContext(), mAccount, mFolder); 529 break; 530 case R.id.help_info_menu_item: 531 // TODO: enable context sensitive help 532 Utils.showHelp(mActivity.getActivityContext(), mAccount.helpIntentUri, null); 533 break; 534 case R.id.feedback_menu_item: 535 Utils.sendFeedback(mActivity.getActivityContext(), mAccount); 536 break; 537 default: 538 handled = false; 539 break; 540 } 541 return handled; 542 } 543 544 /** 545 * Return the auto advance setting for the current account. 546 * @param activity 547 * @return the autoadvance setting, a constant from {@link AutoAdvance} 548 */ 549 static int getAutoAdvanceSetting(RestrictedActivity activity) { 550 final Settings settings = activity.getSettings(); 551 // TODO(mindyp): if this isn't set, then show the dialog telling the user to set it. 552 // Remove defaulting to AutoAdvance.LIST. 553 final int autoAdvance = (settings != null) ? 554 (settings.autoAdvance == AutoAdvance.UNSET ? 555 AutoAdvance.LIST : settings.autoAdvance) 556 : AutoAdvance.LIST; 557 return autoAdvance; 558 } 559 560 /** 561 * Implements folder changes. This class is a listener because folder changes need to be 562 * performed <b>after</b> the ConversationListFragment has finished animating away the 563 * removal of the conversation. 564 * 565 */ 566 protected abstract class FolderChangeListener implements ActionCompleteListener { 567 protected final String mFolderChangeList; 568 protected final boolean mDestructiveChange; 569 570 public FolderChangeListener(String changeList, boolean destructive) { 571 mFolderChangeList = changeList; 572 mDestructiveChange = destructive; 573 } 574 575 @Override 576 public abstract void onActionComplete(); 577 } 578 579 /** 580 * Update the specified column name in conversation for a boolean value. 581 * @param columnName 582 * @param value 583 */ 584 protected void updateCurrentConversation(String columnName, boolean value) { 585 Conversation.updateBoolean(mContext, ImmutableList.of(mCurrentConversation), columnName, 586 value); 587 if (mConversationListFragment != null) { 588 mConversationListFragment.requestListRefresh(); 589 } 590 } 591 592 /** 593 * Update the specified column name in conversation for an integer value. 594 * @param columnName 595 * @param value 596 */ 597 protected void updateCurrentConversation(String columnName, int value) { 598 Conversation.updateInt(mContext, ImmutableList.of(mCurrentConversation), columnName, value); 599 if (mConversationListFragment != null) { 600 mConversationListFragment.requestListRefresh(); 601 } 602 } 603 604 protected void updateCurrentConversation(String columnName, String value) { 605 Conversation.updateString(mContext, ImmutableList.of(mCurrentConversation), columnName, 606 value); 607 if (mConversationListFragment != null) { 608 mConversationListFragment.requestListRefresh(); 609 } 610 } 611 612 private void requestFolderRefresh() { 613 if (mFolder != null) { 614 if (mAsyncRefreshTask != null) { 615 mAsyncRefreshTask.cancel(true); 616 } 617 mAsyncRefreshTask = new AsyncRefreshTask(mContext, mFolder); 618 mAsyncRefreshTask.execute(); 619 } 620 } 621 622 /** 623 * Confirm (based on user's settings) and delete a conversation from the conversation list and 624 * from the database. 625 * @param showDialog 626 * @param confirmResource 627 * @param listener 628 */ 629 protected void confirmAndDelete(boolean showDialog, int confirmResource, 630 final ActionCompleteListener listener) { 631 final ArrayList<Conversation> single = new ArrayList<Conversation>(); 632 single.add(mCurrentConversation); 633 if (showDialog) { 634 final AlertDialog.OnClickListener onClick = new AlertDialog.OnClickListener() { 635 @Override 636 public void onClick(DialogInterface dialog, int which) { 637 requestDelete(listener); 638 } 639 }; 640 final CharSequence message = Utils.formatPlural(mContext, confirmResource, 1); 641 new AlertDialog.Builder(mActivity.getActivityContext()).setMessage(message) 642 .setPositiveButton(R.string.ok, onClick) 643 .setNegativeButton(R.string.cancel, null) 644 .create().show(); 645 } else { 646 requestDelete(listener); 647 } 648 } 649 650 651 protected abstract void requestDelete(ActionCompleteListener listener); 652 653 654 @Override 655 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) { 656 // TODO(viki): Auto-generated method stub 657 658 } 659 660 @Override 661 public boolean onPrepareOptionsMenu(Menu menu) { 662 mActionBarView.onPrepareOptionsMenu(menu); 663 return true; 664 } 665 666 @Override 667 public void onPause() { 668 isLoaderInitialized = false; 669 670 enableNotifications(); 671 } 672 673 @Override 674 public void onResume() { 675 // Register the receiver that will prevent the status receiver from 676 // displaying its notification icon as long as we're running. 677 // The SupressNotificationReceiver will block the broadcast if we're looking at the folder 678 // that the notification was received for. 679 disableNotifications(); 680 681 if (mActionBarView != null) { 682 mActionBarView.onResume(); 683 } 684 685 } 686 687 @Override 688 public void onSaveInstanceState(Bundle outState) { 689 if (mAccount != null) { 690 LogUtils.d(LOG_TAG, "Saving the account now"); 691 outState.putParcelable(SAVED_ACCOUNT, mAccount); 692 } 693 if (mFolder != null) { 694 outState.putParcelable(SAVED_FOLDER, mFolder); 695 } 696 if (mCurrentConversation != null && mViewMode.getMode() == ViewMode.CONVERSATION) { 697 outState.putParcelable(SAVED_CONVERSATION, mCurrentConversation); 698 } 699 if (!mSelectedSet.isEmpty()) { 700 outState.putParcelable(SAVED_CONVERSATIONS, mSelectedSet); 701 } 702 } 703 704 @Override 705 public void onSearchRequested(String query) { 706 Intent intent = new Intent(); 707 intent.setAction(Intent.ACTION_SEARCH); 708 intent.putExtra(ConversationListContext.EXTRA_SEARCH_QUERY, query); 709 intent.putExtra(Utils.EXTRA_ACCOUNT, mAccount); 710 intent.setComponent(mActivity.getComponentName()); 711 mActionBarView.collapseSearch(); 712 mActivity.startActivity(intent); 713 } 714 715 @Override 716 public void onStartDragMode() { 717 // TODO(viki): Auto-generated method stub 718 } 719 720 @Override 721 public void onStop() { 722 // TODO(viki): Auto-generated method stub 723 } 724 725 @Override 726 public void onStopDragMode() { 727 // TODO(viki): Auto-generated method stub 728 } 729 730 /** 731 * {@inheritDoc} Subclasses must override this to listen to mode changes 732 * from the ViewMode. Subclasses <b>must</b> call the parent's 733 * onViewModeChanged since the parent will handle common state changes. 734 */ 735 @Override 736 public void onViewModeChanged(int newMode) { 737 // Perform any mode specific work here. 738 // reset the action bar icon based on the mode. Why don't the individual 739 // controllers do 740 // this themselves? 741 742 // In conversation list mode, clean up the conversation. 743 if (newMode == ViewMode.CONVERSATION_LIST) { 744 // Clean up the conversation here. 745 } 746 747 // We don't want to invalidate the options menu when switching to 748 // conversation 749 // mode, as it will happen when the conversation finishes loading. 750 if (newMode != ViewMode.CONVERSATION) { 751 mActivity.invalidateOptionsMenu(); 752 } 753 } 754 755 @Override 756 public void onWindowFocusChanged(boolean hasFocus) { 757 // TODO(viki): Auto-generated method stub 758 } 759 760 /** 761 * Restore the state from the previous bundle. Subclasses should call this 762 * method from the parent class, since it performs important UI 763 * initialization. 764 * 765 * @param savedState 766 */ 767 protected void restoreState(Bundle savedState) { 768 final Intent intent = mActivity.getIntent(); 769 boolean handled = false; 770 if (savedState != null) { 771 if (savedState.containsKey(SAVED_ACCOUNT)) { 772 mAccount = ((Account) savedState.getParcelable(SAVED_ACCOUNT)); 773 mCachedSettings = mAccount.settings; 774 mActionBarView.setAccount(mAccount); 775 mActivity.invalidateOptionsMenu(); 776 } 777 if (savedState.containsKey(SAVED_FOLDER)) { 778 // Open the folder. 779 onFolderChanged((Folder) savedState.getParcelable(SAVED_FOLDER)); 780 handled = true; 781 } 782 if (savedState.containsKey(SAVED_CONVERSATION)) { 783 // Open the conversation. 784 setCurrentConversation((Conversation) savedState.getParcelable(SAVED_CONVERSATION)); 785 showConversation(mCurrentConversation); 786 handled = true; 787 } 788 } else if (intent != null) { 789 if (Intent.ACTION_VIEW.equals(intent.getAction())) { 790 if (intent.hasExtra(Utils.EXTRA_ACCOUNT)) { 791 mAccount = ((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT)); 792 } else if (intent.hasExtra(Utils.EXTRA_ACCOUNT_STRING)) { 793 mAccount = Account.newinstance(intent 794 .getStringExtra(Utils.EXTRA_ACCOUNT_STRING)); 795 } 796 if (mAccount != null) { 797 mActionBarView.setAccount(mAccount); 798 mCachedSettings = mAccount.settings; 799 mActivity.invalidateOptionsMenu(); 800 } 801 802 Folder folder = null; 803 if (intent.hasExtra(Utils.EXTRA_FOLDER)) { 804 // Open the folder. 805 LogUtils.d(LOG_TAG, "SHOW THE FOLDER at %s", 806 intent.getParcelableExtra(Utils.EXTRA_FOLDER)); 807 folder = (Folder) intent.getParcelableExtra(Utils.EXTRA_FOLDER); 808 809 } else if (intent.hasExtra(Utils.EXTRA_FOLDER_STRING)) { 810 // Open the folder. 811 folder = new Folder(intent.getStringExtra(Utils.EXTRA_FOLDER_STRING)); 812 } 813 if (folder != null) { 814 onFolderChanged(folder); 815 handled = true; 816 } 817 818 if (intent.hasExtra(Utils.EXTRA_CONVERSATION)) { 819 // Open the conversation. 820 LogUtils.d(LOG_TAG, "SHOW THE CONVERSATION at %s", 821 intent.getParcelableExtra(Utils.EXTRA_CONVERSATION)); 822 setCurrentConversation((Conversation) intent 823 .getParcelableExtra(Utils.EXTRA_CONVERSATION)); 824 showConversation(mCurrentConversation); 825 handled = true; 826 } 827 828 if (!handled) { 829 // Nothing was saved; just load the account inbox. 830 loadAccountInbox(); 831 } 832 } else if (Intent.ACTION_SEARCH.equals(intent.getAction())) { 833 // Save this search query for future suggestions. 834 final String query = intent.getStringExtra(SearchManager.QUERY); 835 final String authority = mContext.getString(R.string.suggestions_authority); 836 SearchRecentSuggestions suggestions = new SearchRecentSuggestions( 837 mContext, authority, SuggestionsProvider.MODE); 838 suggestions.saveRecentQuery(query, null); 839 840 mViewMode.enterSearchResultsListMode(); 841 mAccount = ((Account) intent.getParcelableExtra(Utils.EXTRA_ACCOUNT)); 842 mCachedSettings = mAccount.settings; 843 mActionBarView.setAccount(mAccount); 844 fetchSearchFolder(intent); 845 } 846 } 847 848 /** 849 * Restore the state of selected conversations. This needs to be done after the correct mode 850 * is set and the action bar is fully initialized. If not, several key pieces of state 851 * information will be missing, and the split views may not be initialized correctly. 852 * @param savedState 853 */ 854 restoreSelectedConversations(savedState); 855 // Create the accounts loader; this loads the account switch spinner. 856 mActivity.getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this); 857 } 858 859 /** 860 * Copy any selected conversations stored in the saved bundle into our selection set, 861 * triggering {@link ConversationSetObserver} callbacks as our selection set changes. 862 * 863 */ 864 private void restoreSelectedConversations(Bundle savedState) { 865 if (savedState == null) { 866 mSelectedSet.clear(); 867 return; 868 } 869 final ConversationSelectionSet selectedSet = savedState.getParcelable(SAVED_CONVERSATIONS); 870 if (selectedSet == null || selectedSet.isEmpty()) { 871 mSelectedSet.clear(); 872 return; 873 } 874 875 // putAll will take care of calling our registered onSetPopulated method 876 // FIXME: disabled until we correctly handle selection set bringup when list fragment 877 // does not yet exist (b/6268401) 878 //mSelectedSet.putAll(selectedSet); 879 } 880 881 @Override 882 public void setSubject(String subject) { 883 // Do something useful with the subject. This requires changing the 884 // conversation view's subject text. 885 } 886 887 /** 888 * Children can override this method, but they must call super.showConversation(). 889 * {@inheritDoc} 890 */ 891 @Override 892 public void showConversation(Conversation conversation) { 893 // Set the current conversation just in case it wasn't already set. 894 setCurrentConversation(conversation); 895 } 896 897 /** 898 * Children can override this method, but they must call super.showConversationList(). 899 * {@inheritDoc} 900 */ 901 @Override 902 public void showConversationList(ConversationListContext listContext) { 903 } 904 905 @Override 906 public void onConversationSelected(Conversation conversation) { 907 showConversation(conversation); 908 if (mConvListContext != null && mConvListContext.isSearchResult()) { 909 mViewMode.enterSearchResultsConversationMode(); 910 } else { 911 mViewMode.enterConversationMode(); 912 } 913 } 914 915 /** 916 * Set the current conversation. This is the conversation on which all actions are performed. 917 * Do not modify mCurrentConversation except through this method, which makes it easy to 918 * perform common actions associated with changing the current conversation. 919 * @param conversation 920 */ 921 protected void setCurrentConversation(Conversation conversation) { 922 mCurrentConversation = conversation; 923 } 924 925 /** 926 * {@inheritDoc} 927 */ 928 @Override 929 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 930 // Create a loader to listen in on account changes. 931 switch (id) { 932 case LOADER_ACCOUNT_CURSOR: 933 return new CursorLoader(mContext, MailAppProvider.getAccountsUri(), 934 UIProvider.ACCOUNTS_PROJECTION, null, null, null); 935 case LOADER_FOLDER_CURSOR: 936 return new CursorLoader(mContext, mFolder.uri, UIProvider.FOLDERS_PROJECTION, null, 937 null, null); 938 case LOADER_RECENT_FOLDERS: 939 if (mAccount.recentFolderListUri != null) { 940 return new CursorLoader(mContext, mAccount.recentFolderListUri, 941 UIProvider.FOLDERS_PROJECTION, null, null, null); 942 } 943 break; 944 case LOADER_ACCOUNT_INBOX: 945 Settings settings = getSettings(); 946 Uri inboxUri; 947 if (settings != null) { 948 inboxUri = settings.defaultInbox; 949 } else { 950 inboxUri = mAccount.folderListUri; 951 } 952 return new CursorLoader(mContext, inboxUri, UIProvider.FOLDERS_PROJECTION, null, 953 null, null); 954 case LOADER_SEARCH: 955 return Folder.forSearchResults(mAccount, 956 args.getString(ConversationListContext.EXTRA_SEARCH_QUERY), 957 mActivity.getActivityContext()); 958 default: 959 LogUtils.wtf(LOG_TAG, "Loader returned unexpected id: %d", id); 960 } 961 return null; 962 } 963 964 @Override 965 public void onLoaderReset(Loader<Cursor> loader) { 966 967 } 968 969 /** 970 * {@link LoaderManager} currently has a bug in 971 * {@link LoaderManager#restartLoader(int, Bundle, android.app.LoaderManager.LoaderCallbacks)} 972 * where, if a previous onCreateLoader returned a null loader, this method will NPE. Work around 973 * this bug by destroying any loaders that may have been created as null (essentially because 974 * they are optional loads, and may not apply to a particular account). 975 * <p> 976 * A simple null check before restarting a loader will not work, because that would not 977 * give the controller a chance to invalidate UI corresponding the prior loader result. 978 * 979 * @param id loader ID to safely restart 980 */ 981 private void restartOptionalLoader(int id) { 982 final LoaderManager lm = mActivity.getLoaderManager(); 983 lm.destroyLoader(id); 984 lm.restartLoader(id, Bundle.EMPTY, this); 985 } 986 987 /** 988 * Start a loader with the given id. This should be called when we know that the previous 989 * state of the application matches this state, and we are happy if we get the previously 990 * created loader with this id. If that is not true, consider calling 991 * {@link #restartOptionalLoader(int)} instead. 992 * @param id 993 */ 994 private void startLoader(int id) { 995 final LoaderManager lm = mActivity.getLoaderManager(); 996 lm.initLoader(id, Bundle.EMPTY, this); 997 } 998 999 private boolean accountsUpdated(Cursor accountCursor) { 1000 // Check to see if the current account hasn't been set, or the account cursor is empty 1001 if (mAccount == null || !accountCursor.moveToFirst()) { 1002 return true; 1003 } 1004 1005 // Check to see if the number of accounts are different, from the number we saw on the last 1006 // updated 1007 if (mCurrentAccountUris.size() != accountCursor.getCount()) { 1008 return true; 1009 } 1010 1011 // Check to see if the account list is different or if the current account is not found in 1012 // the cursor. 1013 boolean foundCurrentAccount = false; 1014 do { 1015 final Uri accountUri = 1016 Uri.parse(accountCursor.getString(UIProvider.ACCOUNT_URI_COLUMN)); 1017 if (!foundCurrentAccount && mAccount.uri.equals(accountUri)) { 1018 foundCurrentAccount = true; 1019 } 1020 1021 if (!mCurrentAccountUris.contains(accountUri)) { 1022 return true; 1023 } 1024 } while (accountCursor.moveToNext()); 1025 1026 // As long as we found the current account, the list hasn't been updated 1027 return !foundCurrentAccount; 1028 } 1029 1030 /** 1031 * Update the accounts on the device. This currently loads the first account 1032 * in the list. 1033 * 1034 * @param loader 1035 * @param accounts cursor into the AccountCache 1036 * @return true if the update was successful, false otherwise 1037 */ 1038 private boolean updateAccounts(Loader<Cursor> loader, Cursor accounts) { 1039 if (accounts == null || !accounts.moveToFirst()) { 1040 return false; 1041 } 1042 1043 final Account[] allAccounts = Account.getAllAccounts(accounts); 1044 1045 // Save the uris for the accounts 1046 mCurrentAccountUris.clear(); 1047 for (Account account : allAccounts) { 1048 mCurrentAccountUris.add(account.uri); 1049 } 1050 1051 // 1. current account is already set and is in allAccounts -> no-op 1052 // 2. current account is set and is not in allAccounts -> pick first (acct was deleted?) 1053 // 3. saved pref has an account -> pick that one 1054 // 4. otherwise just pick first 1055 1056 Account newAccount = null; 1057 1058 if (mAccount != null) { 1059 if (!mCurrentAccountUris.contains(mAccount.uri)) { 1060 newAccount = allAccounts[0]; 1061 } else { 1062 newAccount = mAccount; 1063 } 1064 } else { 1065 final String lastAccountUri = MailAppProvider.getInstance() 1066 .getLastViewedAccount(); 1067 if (lastAccountUri != null) { 1068 for (int i = 0; i < allAccounts.length; i++) { 1069 final Account acct = allAccounts[i]; 1070 if (lastAccountUri.equals(acct.uri.toString())) { 1071 newAccount = acct; 1072 break; 1073 } 1074 } 1075 } 1076 if (newAccount == null) { 1077 newAccount = allAccounts[0]; 1078 } 1079 } 1080 1081 onAccountChanged(newAccount); 1082 1083 mActionBarView.setAccounts(allAccounts); 1084 return (allAccounts.length > 0); 1085 } 1086 1087 private void disableNotifications() { 1088 mNewEmailReceiver.activate(mContext, this); 1089 } 1090 1091 private void enableNotifications() { 1092 mNewEmailReceiver.deactivate(); 1093 } 1094 1095 private void disableNotificationsOnAccountChange(Account account) { 1096 // If the new mail suppression receiver is activated for a different account, we want to 1097 // activate it for the new account. 1098 if (mNewEmailReceiver.activated() && 1099 !mNewEmailReceiver.notificationsDisabledForAccount(account)) { 1100 // Deactivate the current receiver, otherwise multiple receivers may be registered. 1101 mNewEmailReceiver.deactivate(); 1102 mNewEmailReceiver.activate(mContext, this); 1103 } 1104 } 1105 1106 /** 1107 * {@inheritDoc} 1108 */ 1109 @Override 1110 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 1111 // We want to reinitialize only if we haven't ever been initialized, or 1112 // if the current account has vanished. 1113 if (data == null) { 1114 LogUtils.e(LOG_TAG, "Received null cursor from loader id: %d", loader.getId()); 1115 } 1116 switch (loader.getId()) { 1117 case LOADER_ACCOUNT_CURSOR: 1118 // If the account list is not not null, and the account list cursor is empty, 1119 // we need to start the specified activity. 1120 if (data != null && data.getCount() == 0) { 1121 // If an empty cursor is returned, the MailAppProvider is indicating that 1122 // no accounts have been specified. We want to navigate to the "add account" 1123 // activity that will handle the intent returned by the MailAppProvider 1124 1125 // If the MailAppProvider believes that all accounts have been loaded, and the 1126 // account list is still empty, we want to prompt the user to add an account 1127 final Bundle extras = data.getExtras(); 1128 final boolean accountsLoaded = 1129 extras.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0; 1130 1131 if (accountsLoaded) { 1132 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(mContext); 1133 if (noAccountIntent != null) { 1134 mActivity.startActivityForResult(noAccountIntent, 1135 ADD_ACCOUNT_REQUEST_CODE); 1136 } 1137 } 1138 } else { 1139 final boolean accountListUpdated = accountsUpdated(data); 1140 if (!isLoaderInitialized || accountListUpdated) { 1141 isLoaderInitialized = updateAccounts(loader, data); 1142 } 1143 } 1144 break; 1145 case LOADER_FOLDER_CURSOR: 1146 // Check status of the cursor. 1147 data.moveToFirst(); 1148 Folder folder = new Folder(data); 1149 if (folder.isSyncInProgress()) { 1150 mActionBarView.onRefreshStarted(); 1151 } else { 1152 // Stop the spinner here. 1153 mActionBarView.onRefreshStopped(folder.lastSyncResult); 1154 } 1155 mActionBarView.onFolderUpdated(folder); 1156 if (mConversationListFragment != null) { 1157 mConversationListFragment.onFolderUpdated(folder); 1158 } 1159 LogUtils.v(LOG_TAG, "FOLDER STATUS = %d", folder.syncStatus); 1160 break; 1161 case LOADER_RECENT_FOLDERS: 1162 mRecentFolderList.loadFromUiProvider(data); 1163 break; 1164 case LOADER_ACCOUNT_INBOX: 1165 if (data.moveToFirst() && !data.isClosed()) { 1166 Folder inbox = new Folder(data); 1167 onFolderChanged(inbox); 1168 // Just want to get the inbox, don't care about updates to it 1169 // as this will be tracked by the folder change listener. 1170 mActivity.getLoaderManager().destroyLoader(LOADER_ACCOUNT_INBOX); 1171 } else { 1172 LogUtils.d(LOG_TAG, "Unable to get the account inbox for account %s", 1173 mAccount != null ? mAccount.name : ""); 1174 } 1175 break; 1176 case LOADER_SEARCH: 1177 data.moveToFirst(); 1178 Folder search = new Folder(data); 1179 setFolder(search); 1180 mConvListContext = ConversationListContext.forSearchQuery(mAccount, mFolder, 1181 mActivity.getIntent() 1182 .getStringExtra(UIProvider.SearchQueryParameters.QUERY)); 1183 showConversationList(mConvListContext); 1184 mActivity.invalidateOptionsMenu(); 1185 mActivity.getLoaderManager().destroyLoader(LOADER_SEARCH); 1186 break; 1187 } 1188 } 1189 1190 @Override 1191 public void onTouchEvent(MotionEvent event) { 1192 if (event.getAction() == MotionEvent.ACTION_DOWN) { 1193 int mode = mViewMode.getMode(); 1194 if (mode == ViewMode.CONVERSATION_LIST) { 1195 if (mConversationListFragment != null) { 1196 mConversationListFragment.onTouchEvent(event); 1197 } 1198 } else if (mode == ViewMode.CONVERSATION) { 1199 if (mConversationViewFragment != null) { 1200 mConversationViewFragment.onTouchEvent(event); 1201 } 1202 } 1203 } 1204 } 1205 1206 protected abstract class DestructiveActionListener implements ActionCompleteListener { 1207 protected final int mAction; 1208 1209 /** 1210 * Create a listener object. action is one of four constants: R.id.y_button (archive), 1211 * R.id.delete , R.id.mute, and R.id.report_spam. 1212 * @param action 1213 */ 1214 public DestructiveActionListener(int action) { 1215 mAction = action; 1216 } 1217 1218 public void performConversationAction(Collection<Conversation> single) { 1219 switch (mAction) { 1220 case R.id.archive: 1221 LogUtils.d(LOG_TAG, "Archiving conversation %s", mCurrentConversation); 1222 Conversation.archive(mContext, single); 1223 break; 1224 case R.id.delete: 1225 LogUtils.d(LOG_TAG, "Deleting conversation %s", mCurrentConversation); 1226 Conversation.delete(mContext, single); 1227 break; 1228 case R.id.mute: 1229 LogUtils.d(LOG_TAG, "Muting conversation %s", mCurrentConversation); 1230 if (mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)) 1231 mCurrentConversation.localDeleteOnUpdate = true; 1232 Conversation.mute(mContext, single); 1233 break; 1234 case R.id.report_spam: 1235 LogUtils.d(LOG_TAG, "reporting spam conversation %s", mCurrentConversation); 1236 Conversation.reportSpam(mContext, single); 1237 break; 1238 } 1239 } 1240 1241 public Conversation getNextConversation() { 1242 Conversation next = null; 1243 int pref = getAutoAdvanceSetting(mActivity); 1244 Cursor c = mConversationListCursor; 1245 if (c != null) { 1246 c.moveToPosition(mCurrentConversation.position); 1247 } 1248 switch (pref) { 1249 case AutoAdvance.NEWER: 1250 if (c.moveToPrevious()) { 1251 next = new Conversation(c); 1252 } 1253 break; 1254 case AutoAdvance.OLDER: 1255 if (c.moveToNext()) { 1256 next = new Conversation(c); 1257 } 1258 break; 1259 } 1260 return next; 1261 } 1262 1263 @Override 1264 public abstract void onActionComplete(); 1265 } 1266 1267 // Called from the FolderSelectionDialog after a user is done changing 1268 // folders. 1269 @Override 1270 public void onFolderChangesCommit(ArrayList<Folder> folderChangeList) { 1271 // Get currently active folder info and compare it to the list 1272 // these conversations have been given; if they no longer contain 1273 // the selected folder, delete them from the list. 1274 HashSet<String> folderUris = new HashSet<String>(); 1275 if (folderChangeList != null && !folderChangeList.isEmpty()) { 1276 for (Folder f : folderChangeList) { 1277 folderUris.add(f.uri.toString()); 1278 } 1279 } 1280 final boolean destructiveChange = !folderUris.contains(mFolder.uri.toString()); 1281 DestructiveActionListener listener = getFolderDestructiveActionListener(); 1282 StringBuilder foldersUrisString = new StringBuilder(); 1283 boolean first = true; 1284 for (Folder f : folderChangeList) { 1285 if (first) { 1286 first = false; 1287 } else { 1288 foldersUrisString.append(','); 1289 } 1290 foldersUrisString.append(f.uri.toString()); 1291 } 1292 updateCurrentConversation(ConversationColumns.FOLDER_LIST, foldersUrisString.toString()); 1293 updateCurrentConversation(ConversationColumns.RAW_FOLDERS, 1294 Folder.getSerializedFolderString(mFolder, folderChangeList)); 1295 // TODO: (mindyp): set ConversationColumns.RAW_FOLDERS like in 1296 // SelectedConversationsActionMenu 1297 if (destructiveChange) { 1298 mCurrentConversation.localDeleteOnUpdate = true; 1299 requestDelete(listener); 1300 } else if (mConversationListFragment != null) { 1301 mConversationListFragment.requestListRefresh(); 1302 } 1303 } 1304 1305 protected abstract DestructiveActionListener getFolderDestructiveActionListener(); 1306 1307 @Override 1308 public void onRefreshRequired() { 1309 // Refresh the query in the background 1310 getConversationListCursor().refresh(); 1311 } 1312 1313 @Override 1314 public void onRefreshReady() { 1315 ArrayList<Integer> deletedRows = mConversationListCursor.getRefreshDeletions(); 1316 // If we have any deletions from the server, animate them away 1317 if (!deletedRows.isEmpty() && mConversationListFragment != null) { 1318 AnimatedAdapter adapter = mConversationListFragment.getAnimatedAdapter(); 1319 if (adapter != null) { 1320 mConversationListFragment.getAnimatedAdapter().delete(deletedRows, 1321 this); 1322 } 1323 } else { 1324 // Swap cursors 1325 getConversationListCursor().sync(); 1326 refreshAdapter(); 1327 } 1328 } 1329 1330 @Override 1331 public void onDataSetChanged() { 1332 refreshAdapter(); 1333 } 1334 1335 private void refreshAdapter() { 1336 if (mConversationListFragment != null) { 1337 AnimatedAdapter adapter = mConversationListFragment.getAnimatedAdapter(); 1338 if (adapter != null) { 1339 adapter.notifyDataSetChanged(); 1340 } 1341 } 1342 } 1343 1344 @Override 1345 public void onSetEmpty() { 1346 } 1347 1348 @Override 1349 public void onSetPopulated(ConversationSelectionSet set) { 1350 mCabActionMenu = new SelectedConversationsActionMenu(mActivity, set, 1351 mConversationListFragment.getAnimatedAdapter(), this, mConversationListFragment, 1352 mAccount, mFolder, (SwipeableListView) mConversationListFragment.getListView()); 1353 enableCabMode(); 1354 } 1355 1356 1357 @Override 1358 public void onSetChanged(ConversationSelectionSet set) { 1359 // Do nothing. We don't care about changes to the set. 1360 } 1361 1362 @Override 1363 public ConversationSelectionSet getSelectedSet() { 1364 return mSelectedSet; 1365 } 1366 1367 /** 1368 * Disable the Contextual Action Bar (CAB). The selected set is not changed. 1369 */ 1370 protected void disableCabMode() { 1371 if (mCabActionMenu != null) { 1372 mCabActionMenu.deactivate(); 1373 } 1374 } 1375 1376 /** 1377 * Re-enable the CAB menu if required. The selection set is not changed. 1378 */ 1379 protected void enableCabMode() { 1380 if (mCabActionMenu != null) { 1381 mCabActionMenu.activate(); 1382 } 1383 } 1384 1385 @Override 1386 public void onActionComplete() { 1387 if (getConversationListCursor().isRefreshReady()) { 1388 refreshAdapter(); 1389 } 1390 } 1391 1392 @Override 1393 public void startSearch() { 1394 if (mAccount.supportsCapability(UIProvider.AccountCapabilities.LOCAL_SEARCH) 1395 | mAccount.supportsCapability(UIProvider.AccountCapabilities.SERVER_SEARCH)) { 1396 onSearchRequested(mActionBarView.getQuery()); 1397 } else { 1398 Toast.makeText(mActivity.getActivityContext(), mActivity.getActivityContext() 1399 .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show(); 1400 } 1401 } 1402 1403 /** 1404 * Supports dragging conversations to a folder. 1405 */ 1406 @Override 1407 public boolean supportsDrag(DragEvent event, Folder folder) { 1408 return (folder != null 1409 && event != null 1410 && event.getClipDescription() != null 1411 && folder.supportsCapability 1412 (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES) 1413 && folder.supportsCapability 1414 (UIProvider.FolderCapabilities.CAN_HOLD_MAIL) 1415 && !mFolder.uri.equals(folder.uri)); 1416 } 1417 1418 /** 1419 * Handles dropping conversations to a label. 1420 */ 1421 @Override 1422 public void handleDrop(DragEvent event, final Folder folder) { 1423 /* 1424 * Expect clip data has form: [conversations_uri, conversationId1, 1425 * maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...] 1426 */ 1427 if (!supportsDrag(event, folder)) { 1428 return; 1429 } 1430 ClipData data = event.getClipData(); 1431 ArrayList<Integer> conversationPositions = Lists.newArrayList(); 1432 for (int i = 1; i < data.getItemCount(); i += 3) { 1433 int position = Integer.parseInt(data.getItemAt(i).getText().toString()); 1434 conversationPositions.add(position); 1435 } 1436 final Collection<Conversation> conversations = mSelectedSet.values(); 1437 mConversationListFragment.requestDelete(conversations, 1438 new ActionCompleteListener() { 1439 @Override 1440 public void onActionComplete() { 1441 AbstractActivityController.this.onActionComplete(); 1442 ArrayList<Folder> changes = new ArrayList<Folder>(); 1443 changes.add(folder); 1444 Conversation.updateString(mContext, conversations, 1445 ConversationColumns.FOLDER_LIST, folder.uri.toString()); 1446 Conversation.updateString(mContext, conversations, 1447 ConversationColumns.RAW_FOLDERS, 1448 Folder.getSerializedFolderString(mFolder, changes)); 1449 mConversationListFragment.onUndoAvailable(new UndoOperation(conversations 1450 .size(), R.id.change_folder)); 1451 mSelectedSet.clear(); 1452 } 1453 }); 1454 } 1455} 1456