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