ConversationList.java revision b51ea8318ca2e4019b666d938e3e7efdf6e643b3
1/* 2 * Copyright (C) 2008 Esmertec AG. 3 * Copyright (C) 2008 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.mms.ui; 19 20import java.util.ArrayList; 21 22import com.android.mms.LogTag; 23import com.android.mms.R; 24import com.android.mms.data.Contact; 25import com.android.mms.data.ContactList; 26import com.android.mms.data.Conversation; 27import com.android.mms.transaction.MessagingNotification; 28import com.android.mms.transaction.SmsRejectedReceiver; 29import com.android.mms.util.DraftCache; 30import com.android.mms.util.Recycler; 31import com.google.android.mms.pdu.PduHeaders; 32import android.database.sqlite.SqliteWrapper; 33 34import android.animation.LayoutTransition; 35import android.app.ActionBar; 36import android.app.AlertDialog; 37import android.app.ListActivity; 38import android.content.AsyncQueryHandler; 39import android.content.ContentResolver; 40import android.content.Context; 41import android.content.DialogInterface; 42import android.content.Intent; 43import android.content.SharedPreferences; 44import android.content.DialogInterface.OnClickListener; 45import android.content.res.Configuration; 46import android.database.Cursor; 47import android.database.sqlite.SQLiteException; 48import android.os.Bundle; 49import android.os.Handler; 50import android.preference.PreferenceManager; 51import android.provider.ContactsContract; 52import android.provider.ContactsContract.Contacts; 53import android.provider.Telephony.Mms; 54import android.provider.Telephony.Threads; 55import android.util.Log; 56import android.util.SparseBooleanArray; 57import android.view.ActionMode; 58import android.view.ContextMenu; 59import android.view.KeyEvent; 60import android.view.LayoutInflater; 61import android.view.Menu; 62import android.view.MenuInflater; 63import android.view.MenuItem; 64import android.view.View; 65import android.view.ViewGroup; 66import android.view.Window; 67import android.view.ContextMenu.ContextMenuInfo; 68import android.view.View.OnCreateContextMenuListener; 69import android.view.View.OnKeyListener; 70import android.widget.AdapterView; 71import android.widget.CheckBox; 72import android.widget.ListView; 73import android.widget.TextView; 74import android.widget.Toast; 75 76/** 77 * This activity provides a list view of existing conversations. 78 */ 79public class ConversationList extends ListActivity 80 implements DraftCache.OnDraftChangedListener { 81 private static final String TAG = "ConversationList"; 82 private static final boolean DEBUG = false; 83 private static final boolean LOCAL_LOGV = DEBUG; 84 85 private static final int THREAD_LIST_QUERY_TOKEN = 1701; 86 private static final int UNREAD_THREADS_QUERY_TOKEN = 1702; 87 public static final int DELETE_CONVERSATION_TOKEN = 1801; 88 public static final int HAVE_LOCKED_MESSAGES_TOKEN = 1802; 89 private static final int DELETE_OBSOLETE_THREADS_TOKEN = 1803; 90 91 // IDs of the main menu items. 92 public static final int MENU_COMPOSE_NEW = 0; 93 public static final int MENU_SEARCH = 1; 94 public static final int MENU_DELETE_ALL = 3; 95 public static final int MENU_PREFERENCES = 4; 96 97 // IDs of the context menu items for the list of conversations. 98 public static final int MENU_DELETE = 0; 99 public static final int MENU_VIEW = 1; 100 public static final int MENU_VIEW_CONTACT = 2; 101 public static final int MENU_ADD_TO_CONTACTS = 3; 102 103 private ThreadListQueryHandler mQueryHandler; 104 private ConversationListAdapter mListAdapter; 105 private CharSequence mTitle; 106 private SharedPreferences mPrefs; 107 private Handler mHandler; 108 private boolean mNeedToMarkAsSeen; 109 private TextView mUnreadConvCount; 110 111 static private final String CHECKED_MESSAGE_LIMITS = "checked_message_limits"; 112 113 @Override 114 protected void onCreate(Bundle savedInstanceState) { 115 super.onCreate(savedInstanceState); 116 117 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 118 setContentView(R.layout.conversation_list_screen); 119 120 mQueryHandler = new ThreadListQueryHandler(getContentResolver()); 121 122 ListView listView = getListView(); 123 listView.setOnCreateContextMenuListener(mConvListOnCreateContextMenuListener); 124 listView.setOnKeyListener(mThreadListKeyListener); 125 listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); 126 listView.setMultiChoiceModeListener(new ModeCallback()); 127 128 initListAdapter(); 129 130 setupActionBar(); 131 132 mTitle = getString(R.string.app_label); 133 134 mHandler = new Handler(); 135 mPrefs = PreferenceManager.getDefaultSharedPreferences(this); 136 boolean checkedMessageLimits = mPrefs.getBoolean(CHECKED_MESSAGE_LIMITS, false); 137 if (DEBUG) Log.v(TAG, "checkedMessageLimits: " + checkedMessageLimits); 138 if (!checkedMessageLimits || DEBUG) { 139 runOneTimeStorageLimitCheckForLegacyMessages(); 140 } 141 } 142 143 private void setupActionBar() { 144 ActionBar actionBar = getActionBar(); 145 146 ViewGroup v = (ViewGroup)LayoutInflater.from(this) 147 .inflate(R.layout.conversation_list_actionbar, null); 148 actionBar.setCustomView(v, 149 new ActionBar.LayoutParams( 150 ActionBar.LayoutParams.MATCH_PARENT, 151 ActionBar.LayoutParams.MATCH_PARENT)); 152 153 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP, ActionBar.DISPLAY_HOME_AS_UP); 154 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, ActionBar.DISPLAY_SHOW_CUSTOM); 155 156 // This results in a fade out -> fade in when switching the action bar between 157 // showing the account spinner and the search box. 158 v.setLayoutTransition(new LayoutTransition()); 159 160 mUnreadConvCount = (TextView)v.findViewById(R.id.unread_conv_count); 161 } 162 163 private final ConversationListAdapter.OnContentChangedListener mContentChangedListener = 164 new ConversationListAdapter.OnContentChangedListener() { 165 public void onContentChanged(ConversationListAdapter adapter) { 166 startAsyncQuery(); 167 } 168 }; 169 170 private void initListAdapter() { 171 mListAdapter = new ConversationListAdapter(this, null); 172 mListAdapter.setOnContentChangedListener(mContentChangedListener); 173 setListAdapter(mListAdapter); 174 getListView().setRecyclerListener(mListAdapter); 175 } 176 177 /** 178 * Checks to see if the number of MMS and SMS messages are under the limits for the 179 * recycler. If so, it will automatically turn on the recycler setting. If not, it 180 * will prompt the user with a message and point them to the setting to manually 181 * turn on the recycler. 182 */ 183 public synchronized void runOneTimeStorageLimitCheckForLegacyMessages() { 184 if (Recycler.isAutoDeleteEnabled(this)) { 185 if (DEBUG) Log.v(TAG, "recycler is already turned on"); 186 // The recycler is already turned on. We don't need to check anything or warn 187 // the user, just remember that we've made the check. 188 markCheckedMessageLimit(); 189 return; 190 } 191 new Thread(new Runnable() { 192 public void run() { 193 if (Recycler.checkForThreadsOverLimit(ConversationList.this)) { 194 if (DEBUG) Log.v(TAG, "checkForThreadsOverLimit TRUE"); 195 // Dang, one or more of the threads are over the limit. Show an activity 196 // that'll encourage the user to manually turn on the setting. Delay showing 197 // this activity until a couple of seconds after the conversation list appears. 198 mHandler.postDelayed(new Runnable() { 199 public void run() { 200 Intent intent = new Intent(ConversationList.this, 201 WarnOfStorageLimitsActivity.class); 202 startActivity(intent); 203 } 204 }, 2000); 205 } else { 206 if (DEBUG) Log.v(TAG, "checkForThreadsOverLimit silently turning on recycler"); 207 // No threads were over the limit. Turn on the recycler by default. 208 runOnUiThread(new Runnable() { 209 public void run() { 210 SharedPreferences.Editor editor = mPrefs.edit(); 211 editor.putBoolean(MessagingPreferenceActivity.AUTO_DELETE, true); 212 editor.apply(); 213 } 214 }); 215 } 216 // Remember that we don't have to do the check anymore when starting MMS. 217 runOnUiThread(new Runnable() { 218 public void run() { 219 markCheckedMessageLimit(); 220 } 221 }); 222 } 223 }).start(); 224 } 225 226 /** 227 * Mark in preferences that we've checked the user's message limits. Once checked, we'll 228 * never check them again, unless the user wipe-data or resets the device. 229 */ 230 private void markCheckedMessageLimit() { 231 if (DEBUG) Log.v(TAG, "markCheckedMessageLimit"); 232 SharedPreferences.Editor editor = mPrefs.edit(); 233 editor.putBoolean(CHECKED_MESSAGE_LIMITS, true); 234 editor.apply(); 235 } 236 237 @Override 238 protected void onNewIntent(Intent intent) { 239 // Handle intents that occur after the activity has already been created. 240 startAsyncQuery(); 241 } 242 243 @Override 244 protected void onStart() { 245 super.onStart(); 246 247 MessagingNotification.cancelNotification(getApplicationContext(), 248 SmsRejectedReceiver.SMS_REJECTED_NOTIFICATION_ID); 249 250 DraftCache.getInstance().addOnDraftChangedListener(this); 251 252 mNeedToMarkAsSeen = true; 253 254 startAsyncQuery(); 255 256 // We used to refresh the DraftCache here, but 257 // refreshing the DraftCache each time we go to the ConversationList seems overly 258 // aggressive. We already update the DraftCache when leaving CMA in onStop() and 259 // onNewIntent(), and when we delete threads or delete all in CMA or this activity. 260 // I hope we don't have to do such a heavy operation each time we enter here. 261 262 // we invalidate the contact cache here because we want to get updated presence 263 // and any contact changes. We don't invalidate the cache by observing presence and contact 264 // changes (since that's too untargeted), so as a tradeoff we do it here. 265 // If we're in the middle of the app initialization where we're loading the conversation 266 // threads, don't invalidate the cache because we're in the process of building it. 267 // TODO: think of a better way to invalidate cache more surgically or based on actual 268 // TODO: changes we care about 269 if (!Conversation.loadingThreads()) { 270 Contact.invalidateCache(); 271 } 272 } 273 274 @Override 275 protected void onStop() { 276 super.onStop(); 277 278 DraftCache.getInstance().removeOnDraftChangedListener(this); 279 mListAdapter.changeCursor(null); 280 } 281 282 public void onDraftChanged(final long threadId, final boolean hasDraft) { 283 // Run notifyDataSetChanged() on the main thread. 284 mQueryHandler.post(new Runnable() { 285 public void run() { 286 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 287 log("onDraftChanged: threadId=" + threadId + ", hasDraft=" + hasDraft); 288 } 289 mListAdapter.notifyDataSetChanged(); 290 } 291 }); 292 } 293 294 private void startAsyncQuery() { 295 try { 296 setTitle(getString(R.string.refreshing)); 297 setProgressBarIndeterminateVisibility(true); 298 299 Conversation.startQueryForAll(mQueryHandler, THREAD_LIST_QUERY_TOKEN); 300 Conversation.startQuery(mQueryHandler, UNREAD_THREADS_QUERY_TOKEN, Threads.READ + "=0"); 301 } catch (SQLiteException e) { 302 SqliteWrapper.checkSQLiteException(this, e); 303 } 304 } 305 306 @Override 307 public boolean onPrepareOptionsMenu(Menu menu) { 308 menu.clear(); 309 310 menu.add(0, MENU_COMPOSE_NEW, 0, R.string.menu_compose_new) 311 .setIcon(com.android.internal.R.drawable.ic_menu_compose) 312 .setTitle(R.string.new_message) 313 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); 314 315 if (mListAdapter.getCount() > 0) { 316 menu.add(0, MENU_DELETE_ALL, 0, R.string.menu_delete_all).setIcon( 317 android.R.drawable.ic_menu_delete); 318 } 319 320 menu.add(0, MENU_SEARCH, 0, android.R.string.search_go) 321 .setIcon(android.R.drawable.ic_menu_search) 322 .setAlphabeticShortcut(android.app.SearchManager.MENU_KEY) 323 .setTitle(R.string.menu_search) 324 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); 325 326 menu.add(0, MENU_PREFERENCES, 0, R.string.menu_preferences).setIcon( 327 android.R.drawable.ic_menu_preferences); 328 329 return true; 330 } 331 332 @Override 333 public boolean onSearchRequested() { 334 startSearch(null, false, null /*appData*/, false); 335 return true; 336 } 337 338 @Override 339 public boolean onOptionsItemSelected(MenuItem item) { 340 switch(item.getItemId()) { 341 case MENU_COMPOSE_NEW: 342 createNewMessage(); 343 break; 344 case MENU_SEARCH: 345 onSearchRequested(); 346 break; 347 case MENU_DELETE_ALL: 348 // The invalid threadId of -1 means all threads here. 349 confirmDeleteThread(-1L, mQueryHandler); 350 break; 351 case MENU_PREFERENCES: { 352 Intent intent = new Intent(this, MessagingPreferenceActivity.class); 353 startActivityIfNeeded(intent, -1); 354 break; 355 } 356 default: 357 return true; 358 } 359 return false; 360 } 361 362 @Override 363 protected void onListItemClick(ListView l, View v, int position, long id) { 364 // Note: don't read the thread id data from the ConversationListItem view passed in. 365 // It's unreliable to read the cached data stored in the view because the ListItem 366 // can be recycled, and the same view could be assigned to a different position 367 // if you click the list item fast enough. Instead, get the cursor at the position 368 // clicked and load the data from the cursor. 369 // (ConversationListAdapter extends CursorAdapter, so getItemAtPosition() should 370 // return the cursor object, which is moved to the position passed in) 371 Cursor cursor = (Cursor) getListView().getItemAtPosition(position); 372 Conversation conv = Conversation.from(this, cursor); 373 long tid = conv.getThreadId(); 374 375 if (LogTag.VERBOSE) { 376 Log.d(TAG, "onListItemClick: pos=" + position + ", view=" + v + ", tid=" + tid); 377 } 378 379 openThread(tid); 380 } 381 382 private void createNewMessage() { 383 startActivity(ComposeMessageActivity.createIntent(this, 0)); 384 } 385 386 private void openThread(long threadId) { 387 startActivity(ComposeMessageActivity.createIntent(this, threadId)); 388 } 389 390 public static Intent createAddContactIntent(String address) { 391 // address must be a single recipient 392 Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 393 intent.setType(Contacts.CONTENT_ITEM_TYPE); 394 if (Mms.isEmailAddress(address)) { 395 intent.putExtra(ContactsContract.Intents.Insert.EMAIL, address); 396 } else { 397 intent.putExtra(ContactsContract.Intents.Insert.PHONE, address); 398 intent.putExtra(ContactsContract.Intents.Insert.PHONE_TYPE, 399 ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE); 400 } 401 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 402 403 return intent; 404 } 405 406 private final OnCreateContextMenuListener mConvListOnCreateContextMenuListener = 407 new OnCreateContextMenuListener() { 408 public void onCreateContextMenu(ContextMenu menu, View v, 409 ContextMenuInfo menuInfo) { 410 Cursor cursor = mListAdapter.getCursor(); 411 if (cursor == null || cursor.getPosition() < 0) { 412 return; 413 } 414 Conversation conv = Conversation.from(ConversationList.this, cursor); 415 ContactList recipients = conv.getRecipients(); 416 menu.setHeaderTitle(recipients.formatNames(",")); 417 418 AdapterView.AdapterContextMenuInfo info = 419 (AdapterView.AdapterContextMenuInfo) menuInfo; 420 menu.add(0, MENU_VIEW, 0, R.string.menu_view); 421 422 // Only show if there's a single recipient 423 if (recipients.size() == 1) { 424 // do we have this recipient in contacts? 425 if (recipients.get(0).existsInDatabase()) { 426 menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact); 427 } else { 428 menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts); 429 } 430 } 431 menu.add(0, MENU_DELETE, 0, R.string.menu_delete); 432 } 433 }; 434 435 @Override 436 public boolean onContextItemSelected(MenuItem item) { 437 Cursor cursor = mListAdapter.getCursor(); 438 if (cursor != null && cursor.getPosition() >= 0) { 439 Conversation conv = Conversation.from(ConversationList.this, cursor); 440 long threadId = conv.getThreadId(); 441 switch (item.getItemId()) { 442 case MENU_DELETE: { 443 confirmDeleteThread(threadId, mQueryHandler); 444 break; 445 } 446 case MENU_VIEW: { 447 openThread(threadId); 448 break; 449 } 450 case MENU_VIEW_CONTACT: { 451 Contact contact = conv.getRecipients().get(0); 452 Intent intent = new Intent(Intent.ACTION_VIEW, contact.getUri()); 453 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 454 startActivity(intent); 455 break; 456 } 457 case MENU_ADD_TO_CONTACTS: { 458 String address = conv.getRecipients().get(0).getNumber(); 459 startActivity(createAddContactIntent(address)); 460 break; 461 } 462 default: 463 break; 464 } 465 } 466 return super.onContextItemSelected(item); 467 } 468 469 @Override 470 public void onConfigurationChanged(Configuration newConfig) { 471 // We override this method to avoid restarting the entire 472 // activity when the keyboard is opened (declared in 473 // AndroidManifest.xml). Because the only translatable text 474 // in this activity is "New Message", which has the full width 475 // of phone to work with, localization shouldn't be a problem: 476 // no abbreviated alternate words should be needed even in 477 // 'wide' languages like German or Russian. 478 479 super.onConfigurationChanged(newConfig); 480 if (DEBUG) Log.v(TAG, "onConfigurationChanged: " + newConfig); 481 } 482 483 /** 484 * Start the process of putting up a dialog to confirm deleting a thread, 485 * but first start a background query to see if any of the threads or thread 486 * contain locked messages so we'll know how detailed of a UI to display. 487 * @param threadId id of the thread to delete or -1 for all threads 488 * @param handler query handler to do the background locked query 489 */ 490 public static void confirmDeleteThread(long threadId, AsyncQueryHandler handler) { 491 ArrayList<Long> threadIds = null; 492 if (threadId != -1) { 493 threadIds = new ArrayList<Long>(); 494 threadIds.add(threadId); 495 } 496 confirmDeleteThreads(threadIds, handler); 497 } 498 499 /** 500 * Start the process of putting up a dialog to confirm deleting threads, 501 * but first start a background query to see if any of the threads 502 * contain locked messages so we'll know how detailed of a UI to display. 503 * @param threadIds list of threadIds to delete or null for all threads 504 * @param handler query handler to do the background locked query 505 */ 506 public static void confirmDeleteThreads(ArrayList<Long> threadIds, AsyncQueryHandler handler) { 507 Conversation.startQueryHaveLockedMessages(handler, threadIds, 508 HAVE_LOCKED_MESSAGES_TOKEN); 509 } 510 511 /** 512 * Build and show the proper delete thread dialog. The UI is slightly different 513 * depending on whether there are locked messages in the thread(s) and whether we're 514 * deleting single/multiple threads or all threads. 515 * @param listener gets called when the delete button is pressed 516 * @param deleteAll whether to show a single thread or all threads UI 517 * @param hasLockedMessages whether the thread(s) contain locked messages 518 * @param context used to load the various UI elements 519 */ 520 public static void confirmDeleteThreadDialog(final DeleteThreadListener listener, 521 ArrayList<Long> threadIds, 522 boolean hasLockedMessages, 523 Context context) { 524 View contents = View.inflate(context, R.layout.delete_thread_dialog_view, null); 525 TextView msg = (TextView)contents.findViewById(R.id.message); 526 527 if (threadIds == null) { 528 msg.setText(R.string.confirm_delete_all_conversations); 529 } else { 530 // Show the number of threads getting deleted in the confirmation dialog. 531 int cnt = threadIds.size(); 532 msg.setText(context.getResources().getQuantityString( 533 R.plurals.confirm_delete_conversation, cnt, cnt)); 534 } 535 536 final CheckBox checkbox = (CheckBox)contents.findViewById(R.id.delete_locked); 537 if (!hasLockedMessages) { 538 checkbox.setVisibility(View.GONE); 539 } else { 540 listener.setDeleteLockedMessage(checkbox.isChecked()); 541 checkbox.setOnClickListener(new View.OnClickListener() { 542 public void onClick(View v) { 543 listener.setDeleteLockedMessage(checkbox.isChecked()); 544 } 545 }); 546 } 547 548 AlertDialog.Builder builder = new AlertDialog.Builder(context); 549 builder.setTitle(R.string.confirm_dialog_title) 550 .setIcon(android.R.drawable.ic_dialog_alert) 551 .setCancelable(true) 552 .setPositiveButton(R.string.delete, listener) 553 .setNegativeButton(R.string.no, null) 554 .setView(contents) 555 .show(); 556 } 557 558 private final OnKeyListener mThreadListKeyListener = new OnKeyListener() { 559 public boolean onKey(View v, int keyCode, KeyEvent event) { 560 if (event.getAction() == KeyEvent.ACTION_DOWN) { 561 switch (keyCode) { 562 case KeyEvent.KEYCODE_DEL: { 563 long id = getListView().getSelectedItemId(); 564 if (id > 0) { 565 confirmDeleteThread(id, mQueryHandler); 566 } 567 return true; 568 } 569 } 570 } 571 return false; 572 } 573 }; 574 575 public static class DeleteThreadListener implements OnClickListener { 576 private final ArrayList<Long> mThreadIds; 577 private final AsyncQueryHandler mHandler; 578 private final Context mContext; 579 private boolean mDeleteLockedMessages; 580 581 public DeleteThreadListener(ArrayList<Long> threadIds, AsyncQueryHandler handler, 582 Context context) { 583 mThreadIds = threadIds; 584 mHandler = handler; 585 mContext = context; 586 } 587 588 public void setDeleteLockedMessage(boolean deleteLockedMessages) { 589 mDeleteLockedMessages = deleteLockedMessages; 590 } 591 592 public void onClick(DialogInterface dialog, final int whichButton) { 593 MessageUtils.handleReadReport(mContext, mThreadIds, 594 PduHeaders.READ_STATUS__DELETED_WITHOUT_BEING_READ, new Runnable() { 595 public void run() { 596 int token = DELETE_CONVERSATION_TOKEN; 597 if (mThreadIds == null) { 598 Conversation.startDeleteAll(mHandler, token, mDeleteLockedMessages); 599 DraftCache.getInstance().refresh(); 600 } else { 601 for (long threadId : mThreadIds) { 602 Conversation.startDelete(mHandler, token, mDeleteLockedMessages, 603 threadId); 604 DraftCache.getInstance().setDraftState(threadId, false); 605 } 606 } 607 } 608 }); 609 dialog.dismiss(); 610 } 611 } 612 613 private final class ThreadListQueryHandler extends AsyncQueryHandler { 614 public ThreadListQueryHandler(ContentResolver contentResolver) { 615 super(contentResolver); 616 } 617 618 @Override 619 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 620 switch (token) { 621 case THREAD_LIST_QUERY_TOKEN: 622 mListAdapter.changeCursor(cursor); 623 setTitle(mTitle); 624 setProgressBarIndeterminateVisibility(false); 625 626 if (mNeedToMarkAsSeen) { 627 mNeedToMarkAsSeen = false; 628 Conversation.markAllConversationsAsSeen(getApplicationContext()); 629 630 // Delete any obsolete threads. Obsolete threads are threads that aren't 631 // referenced by at least one message in the pdu or sms tables. 632 Conversation.asyncDeleteObsoleteThreads(mQueryHandler, 633 DELETE_OBSOLETE_THREADS_TOKEN); 634 } 635 break; 636 637 case UNREAD_THREADS_QUERY_TOKEN: 638 int count = cursor.getCount(); 639 mUnreadConvCount.setText(count > 0 ? Integer.toString(count) : null); 640 break; 641 642 case HAVE_LOCKED_MESSAGES_TOKEN: 643 ArrayList<Long> threadIds = (ArrayList<Long>)cookie; 644 confirmDeleteThreadDialog(new DeleteThreadListener(threadIds, mQueryHandler, 645 ConversationList.this), threadIds, 646 cursor != null && cursor.getCount() > 0, 647 ConversationList.this); 648 break; 649 650 default: 651 Log.e(TAG, "onQueryComplete called with unknown token " + token); 652 } 653 } 654 655 @Override 656 protected void onDeleteComplete(int token, Object cookie, int result) { 657 switch (token) { 658 case DELETE_CONVERSATION_TOKEN: 659 // Make sure the conversation cache reflects the threads in the DB. 660 Conversation.init(ConversationList.this); 661 662 // Update the notification for new messages since they 663 // may be deleted. 664 MessagingNotification.nonBlockingUpdateNewMessageIndicator(ConversationList.this, 665 false, false); 666 // Update the notification for failed messages since they 667 // may be deleted. 668 MessagingNotification.updateSendFailedNotification(ConversationList.this); 669 670 // Make sure the list reflects the delete 671 startAsyncQuery(); 672 break; 673 674 case DELETE_OBSOLETE_THREADS_TOKEN: 675 // Nothing to do here. 676 break; 677 } 678 } 679 } 680 681 private class ModeCallback implements ListView.MultiChoiceModeListener { 682 private View mMultiSelectActionBarView; 683 private TextView mSelectedConvCount; 684 685 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 686 MenuInflater inflater = getMenuInflater(); 687 inflater.inflate(R.menu.conversation_multi_select_menu, menu); 688 689 if (mMultiSelectActionBarView == null) { 690 mMultiSelectActionBarView = (ViewGroup)LayoutInflater.from(ConversationList.this) 691 .inflate(R.layout.conversation_list_multi_select_actionbar, null); 692 693 mSelectedConvCount = 694 (TextView)mMultiSelectActionBarView.findViewById(R.id.selected_conv_count); 695 } 696 mode.setCustomView(mMultiSelectActionBarView); 697 ((TextView)mMultiSelectActionBarView.findViewById(R.id.title)) 698 .setText(R.string.select_conversations); 699 return true; 700 } 701 702 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 703 if (mMultiSelectActionBarView == null) { 704 ViewGroup v = (ViewGroup)LayoutInflater.from(ConversationList.this) 705 .inflate(R.layout.conversation_list_multi_select_actionbar, null); 706 mode.setCustomView(v); 707 708 mSelectedConvCount = (TextView)v.findViewById(R.id.selected_conv_count); 709 } 710 return true; 711 } 712 713 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 714 switch (item.getItemId()) { 715 case R.id.delete: 716 ListView listView = getListView(); 717 int numSelected = listView.getCheckedItemCount(); 718 if (numSelected > 0) { 719 ArrayList<Long> threadIds = new ArrayList<Long>(); 720 int numConvs = mAdapter.getCount(); 721 SparseBooleanArray selectedItems = listView.getCheckedItemPositions(); 722 for (int i = 0; i < numConvs; i++) { 723 if (selectedItems.get(i)) { 724 Cursor cursor = (Cursor) getListView().getItemAtPosition(i); 725 Conversation conv = Conversation.from(ConversationList.this, 726 cursor); 727 threadIds.add(conv.getThreadId()); 728 } 729 } 730 confirmDeleteThreads(threadIds, mQueryHandler); 731 } 732 mode.finish(); 733 break; 734 735 default: 736 break; 737 } 738 return true; 739 } 740 741 public void onDestroyActionMode(ActionMode mode) { 742 } 743 744 public void onItemCheckedStateChanged(ActionMode mode, 745 int position, long id, boolean checked) { 746 final int checkedCount = getListView().getCheckedItemCount(); 747 mSelectedConvCount.setText(Integer.toString(checkedCount)); 748 } 749 750 } 751 752 private void log(String format, Object... args) { 753 String s = String.format(format, args); 754 Log.d(TAG, "[" + Thread.currentThread().getId() + "] " + s); 755 } 756} 757