Home | History | Annotate | Download | only in ui
      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 
     18 package com.android.mms.ui;
     19 
     20 import java.util.ArrayList;
     21 import java.util.Collection;
     22 import java.util.HashSet;
     23 
     24 import com.android.mms.LogTag;
     25 import com.android.mms.R;
     26 import com.android.mms.data.Contact;
     27 import com.android.mms.data.ContactList;
     28 import com.android.mms.data.Conversation;
     29 import com.android.mms.transaction.MessagingNotification;
     30 import com.android.mms.transaction.SmsRejectedReceiver;
     31 import com.android.mms.util.DraftCache;
     32 import com.android.mms.util.Recycler;
     33 import com.google.android.mms.pdu.PduHeaders;
     34 import android.database.sqlite.SqliteWrapper;
     35 
     36 import android.app.ActionBar;
     37 import android.app.AlertDialog;
     38 import android.app.ListActivity;
     39 import android.app.SearchManager;
     40 import android.app.SearchManager.OnDismissListener;
     41 import android.app.SearchableInfo;
     42 import android.content.AsyncQueryHandler;
     43 import android.content.ContentResolver;
     44 import android.content.Context;
     45 import android.content.DialogInterface;
     46 import android.content.Intent;
     47 import android.content.SharedPreferences;
     48 import android.content.DialogInterface.OnClickListener;
     49 import android.content.res.Configuration;
     50 import android.database.Cursor;
     51 import android.database.sqlite.SQLiteException;
     52 import android.os.Bundle;
     53 import android.os.Handler;
     54 import android.preference.PreferenceManager;
     55 import android.provider.ContactsContract;
     56 import android.provider.ContactsContract.Contacts;
     57 import android.provider.Telephony.Mms;
     58 import android.provider.Telephony.Threads;
     59 import android.util.Log;
     60 import android.util.SparseBooleanArray;
     61 import android.view.ActionMode;
     62 import android.view.ContextMenu;
     63 import android.view.Gravity;
     64 import android.view.KeyEvent;
     65 import android.view.LayoutInflater;
     66 import android.view.Menu;
     67 import android.view.MenuInflater;
     68 import android.view.MenuItem;
     69 import android.view.View;
     70 import android.view.ViewGroup;
     71 import android.view.Window;
     72 import android.view.ContextMenu.ContextMenuInfo;
     73 import android.view.View.OnCreateContextMenuListener;
     74 import android.view.View.OnKeyListener;
     75 import android.widget.AdapterView;
     76 import android.widget.CheckBox;
     77 import android.widget.ListView;
     78 import android.widget.SearchView;
     79 import android.widget.TextView;
     80 
     81 /**
     82  * This activity provides a list view of existing conversations.
     83  */
     84 public class ConversationList extends ListActivity implements DraftCache.OnDraftChangedListener {
     85     private static final String TAG = "ConversationList";
     86     private static final boolean DEBUG = false;
     87     private static final boolean LOCAL_LOGV = DEBUG;
     88 
     89     private static final int THREAD_LIST_QUERY_TOKEN       = 1701;
     90     private static final int UNREAD_THREADS_QUERY_TOKEN    = 1702;
     91     public static final int DELETE_CONVERSATION_TOKEN      = 1801;
     92     public static final int HAVE_LOCKED_MESSAGES_TOKEN     = 1802;
     93     private static final int DELETE_OBSOLETE_THREADS_TOKEN = 1803;
     94 
     95     // IDs of the context menu items for the list of conversations.
     96     public static final int MENU_DELETE               = 0;
     97     public static final int MENU_VIEW                 = 1;
     98     public static final int MENU_VIEW_CONTACT         = 2;
     99     public static final int MENU_ADD_TO_CONTACTS      = 3;
    100 
    101     private ThreadListQueryHandler mQueryHandler;
    102     private ConversationListAdapter mListAdapter;
    103     private CharSequence mTitle;
    104     private SharedPreferences mPrefs;
    105     private Handler mHandler;
    106     private boolean mNeedToMarkAsSeen;
    107     private TextView mUnreadConvCount;
    108 
    109     private MenuItem mSearchItem;
    110     private SearchView mSearchView;
    111 
    112     static private final String CHECKED_MESSAGE_LIMITS = "checked_message_limits";
    113 
    114     @Override
    115     protected void onCreate(Bundle savedInstanceState) {
    116         super.onCreate(savedInstanceState);
    117 
    118         requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
    119         setContentView(R.layout.conversation_list_screen);
    120 
    121         mQueryHandler = new ThreadListQueryHandler(getContentResolver());
    122 
    123         ListView listView = getListView();
    124         listView.setOnCreateContextMenuListener(mConvListOnCreateContextMenuListener);
    125         listView.setOnKeyListener(mThreadListKeyListener);
    126         listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
    127         listView.setMultiChoiceModeListener(new ModeCallback());
    128 
    129         // Tell the list view which view to display when the list is empty
    130         View emptyView = findViewById(R.id.empty);
    131         listView.setEmptyView(emptyView);
    132 
    133         initListAdapter();
    134 
    135         setupActionBar();
    136 
    137         mTitle = getString(R.string.app_label);
    138 
    139         mHandler = new Handler();
    140         mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
    141         boolean checkedMessageLimits = mPrefs.getBoolean(CHECKED_MESSAGE_LIMITS, false);
    142         if (DEBUG) Log.v(TAG, "checkedMessageLimits: " + checkedMessageLimits);
    143         if (!checkedMessageLimits || DEBUG) {
    144             runOneTimeStorageLimitCheckForLegacyMessages();
    145         }
    146     }
    147 
    148     private void setupActionBar() {
    149         ActionBar actionBar = getActionBar();
    150 
    151         ViewGroup v = (ViewGroup)LayoutInflater.from(this)
    152             .inflate(R.layout.conversation_list_actionbar, null);
    153         actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM,
    154                 ActionBar.DISPLAY_SHOW_CUSTOM);
    155         actionBar.setCustomView(v,
    156                 new ActionBar.LayoutParams(ActionBar.LayoutParams.WRAP_CONTENT,
    157                         ActionBar.LayoutParams.WRAP_CONTENT,
    158                         Gravity.CENTER_VERTICAL | Gravity.RIGHT));
    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 
    280         // Simply setting the choice mode causes the previous choice mode to finish and we exit
    281         // multi-select mode (if we're in it) and remove all the selections.
    282         getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
    283 
    284         mListAdapter.changeCursor(null);
    285     }
    286 
    287     public void onDraftChanged(final long threadId, final boolean hasDraft) {
    288         // Run notifyDataSetChanged() on the main thread.
    289         mQueryHandler.post(new Runnable() {
    290             public void run() {
    291                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    292                     log("onDraftChanged: threadId=" + threadId + ", hasDraft=" + hasDraft);
    293                 }
    294                 mListAdapter.notifyDataSetChanged();
    295             }
    296         });
    297     }
    298 
    299     private void startAsyncQuery() {
    300         try {
    301             setTitle(getString(R.string.refreshing));
    302             setProgressBarIndeterminateVisibility(true);
    303 
    304             Conversation.startQueryForAll(mQueryHandler, THREAD_LIST_QUERY_TOKEN);
    305             Conversation.startQuery(mQueryHandler, UNREAD_THREADS_QUERY_TOKEN, Threads.READ + "=0");
    306         } catch (SQLiteException e) {
    307             SqliteWrapper.checkSQLiteException(this, e);
    308         }
    309     }
    310 
    311     SearchView.OnQueryTextListener mQueryTextListener = new SearchView.OnQueryTextListener() {
    312         public boolean onQueryTextSubmit(String query) {
    313             Intent intent = new Intent();
    314             intent.setClass(ConversationList.this, SearchActivity.class);
    315             intent.putExtra(SearchManager.QUERY, query);
    316             startActivity(intent);
    317             mSearchItem.collapseActionView();
    318             return true;
    319         }
    320 
    321         public boolean onQueryTextChange(String newText) {
    322             return false;
    323         }
    324     };
    325 
    326     @Override
    327     public boolean onCreateOptionsMenu(Menu menu) {
    328         getMenuInflater().inflate(R.menu.conversation_list_menu, menu);
    329 
    330         mSearchItem = menu.findItem(R.id.search);
    331         mSearchView = (SearchView) mSearchItem.getActionView();
    332 
    333         mSearchView.setOnQueryTextListener(mQueryTextListener);
    334         mSearchView.setQueryHint(getString(R.string.search_hint));
    335         mSearchView.setIconifiedByDefault(true);
    336         SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
    337 
    338         if (searchManager != null) {
    339             SearchableInfo info = searchManager.getSearchableInfo(this.getComponentName());
    340             mSearchView.setSearchableInfo(info);
    341         }
    342 
    343         return true;
    344     }
    345 
    346     @Override
    347     public boolean onPrepareOptionsMenu(Menu menu) {
    348         MenuItem item = menu.findItem(R.id.action_delete_all);
    349         if (item != null) {
    350             item.setVisible(mListAdapter.getCount() > 0);
    351         }
    352         if (!LogTag.DEBUG_DUMP) {
    353             item = menu.findItem(R.id.action_debug_dump);
    354             if (item != null) {
    355                 item.setVisible(false);
    356             }
    357         }
    358         return true;
    359     }
    360 
    361     @Override
    362     public boolean onSearchRequested() {
    363         mSearchItem.expandActionView();
    364         return true;
    365     }
    366 
    367     @Override
    368     public boolean onOptionsItemSelected(MenuItem item) {
    369         switch(item.getItemId()) {
    370             case R.id.action_compose_new:
    371                 createNewMessage();
    372                 break;
    373             case R.id.action_delete_all:
    374                 // The invalid threadId of -1 means all threads here.
    375                 confirmDeleteThread(-1L, mQueryHandler);
    376                 break;
    377             case R.id.action_settings:
    378                 Intent intent = new Intent(this, MessagingPreferenceActivity.class);
    379                 startActivityIfNeeded(intent, -1);
    380                 break;
    381             case R.id.action_debug_dump:
    382                 LogTag.dumpInternalTables(this);
    383                 break;
    384             default:
    385                 return true;
    386         }
    387         return false;
    388     }
    389 
    390     @Override
    391     protected void onListItemClick(ListView l, View v, int position, long id) {
    392         // Note: don't read the thread id data from the ConversationListItem view passed in.
    393         // It's unreliable to read the cached data stored in the view because the ListItem
    394         // can be recycled, and the same view could be assigned to a different position
    395         // if you click the list item fast enough. Instead, get the cursor at the position
    396         // clicked and load the data from the cursor.
    397         // (ConversationListAdapter extends CursorAdapter, so getItemAtPosition() should
    398         // return the cursor object, which is moved to the position passed in)
    399         Cursor cursor  = (Cursor) getListView().getItemAtPosition(position);
    400         Conversation conv = Conversation.from(this, cursor);
    401         long tid = conv.getThreadId();
    402 
    403         if (LogTag.VERBOSE) {
    404             Log.d(TAG, "onListItemClick: pos=" + position + ", view=" + v + ", tid=" + tid);
    405         }
    406 
    407         openThread(tid);
    408     }
    409 
    410     private void createNewMessage() {
    411         startActivity(ComposeMessageActivity.createIntent(this, 0));
    412     }
    413 
    414     private void openThread(long threadId) {
    415         startActivity(ComposeMessageActivity.createIntent(this, threadId));
    416     }
    417 
    418     public static Intent createAddContactIntent(String address) {
    419         // address must be a single recipient
    420         Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
    421         intent.setType(Contacts.CONTENT_ITEM_TYPE);
    422         if (Mms.isEmailAddress(address)) {
    423             intent.putExtra(ContactsContract.Intents.Insert.EMAIL, address);
    424         } else {
    425             intent.putExtra(ContactsContract.Intents.Insert.PHONE, address);
    426             intent.putExtra(ContactsContract.Intents.Insert.PHONE_TYPE,
    427                     ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
    428         }
    429         intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
    430 
    431         return intent;
    432     }
    433 
    434     private final OnCreateContextMenuListener mConvListOnCreateContextMenuListener =
    435         new OnCreateContextMenuListener() {
    436         public void onCreateContextMenu(ContextMenu menu, View v,
    437                 ContextMenuInfo menuInfo) {
    438             Cursor cursor = mListAdapter.getCursor();
    439             if (cursor == null || cursor.getPosition() < 0) {
    440                 return;
    441             }
    442             Conversation conv = Conversation.from(ConversationList.this, cursor);
    443             ContactList recipients = conv.getRecipients();
    444             menu.setHeaderTitle(recipients.formatNames(","));
    445 
    446             AdapterView.AdapterContextMenuInfo info =
    447                 (AdapterView.AdapterContextMenuInfo) menuInfo;
    448             menu.add(0, MENU_VIEW, 0, R.string.menu_view);
    449 
    450             // Only show if there's a single recipient
    451             if (recipients.size() == 1) {
    452                 // do we have this recipient in contacts?
    453                 if (recipients.get(0).existsInDatabase()) {
    454                     menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact);
    455                 } else {
    456                     menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts);
    457                 }
    458             }
    459             menu.add(0, MENU_DELETE, 0, R.string.menu_delete);
    460         }
    461     };
    462 
    463     @Override
    464     public boolean onContextItemSelected(MenuItem item) {
    465         Cursor cursor = mListAdapter.getCursor();
    466         if (cursor != null && cursor.getPosition() >= 0) {
    467             Conversation conv = Conversation.from(ConversationList.this, cursor);
    468             long threadId = conv.getThreadId();
    469             switch (item.getItemId()) {
    470             case MENU_DELETE: {
    471                 confirmDeleteThread(threadId, mQueryHandler);
    472                 break;
    473             }
    474             case MENU_VIEW: {
    475                 openThread(threadId);
    476                 break;
    477             }
    478             case MENU_VIEW_CONTACT: {
    479                 Contact contact = conv.getRecipients().get(0);
    480                 Intent intent = new Intent(Intent.ACTION_VIEW, contact.getUri());
    481                 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
    482                 startActivity(intent);
    483                 break;
    484             }
    485             case MENU_ADD_TO_CONTACTS: {
    486                 String address = conv.getRecipients().get(0).getNumber();
    487                 startActivity(createAddContactIntent(address));
    488                 break;
    489             }
    490             default:
    491                 break;
    492             }
    493         }
    494         return super.onContextItemSelected(item);
    495     }
    496 
    497     @Override
    498     public void onConfigurationChanged(Configuration newConfig) {
    499         // We override this method to avoid restarting the entire
    500         // activity when the keyboard is opened (declared in
    501         // AndroidManifest.xml).  Because the only translatable text
    502         // in this activity is "New Message", which has the full width
    503         // of phone to work with, localization shouldn't be a problem:
    504         // no abbreviated alternate words should be needed even in
    505         // 'wide' languages like German or Russian.
    506 
    507         super.onConfigurationChanged(newConfig);
    508         if (DEBUG) Log.v(TAG, "onConfigurationChanged: " + newConfig);
    509     }
    510 
    511     /**
    512      * Start the process of putting up a dialog to confirm deleting a thread,
    513      * but first start a background query to see if any of the threads or thread
    514      * contain locked messages so we'll know how detailed of a UI to display.
    515      * @param threadId id of the thread to delete or -1 for all threads
    516      * @param handler query handler to do the background locked query
    517      */
    518     public static void confirmDeleteThread(long threadId, AsyncQueryHandler handler) {
    519         ArrayList<Long> threadIds = null;
    520         if (threadId != -1) {
    521             threadIds = new ArrayList<Long>();
    522             threadIds.add(threadId);
    523         }
    524         confirmDeleteThreads(threadIds, handler);
    525     }
    526 
    527     /**
    528      * Start the process of putting up a dialog to confirm deleting threads,
    529      * but first start a background query to see if any of the threads
    530      * contain locked messages so we'll know how detailed of a UI to display.
    531      * @param threadIds list of threadIds to delete or null for all threads
    532      * @param handler query handler to do the background locked query
    533      */
    534     public static void confirmDeleteThreads(Collection<Long> threadIds, AsyncQueryHandler handler) {
    535         Conversation.startQueryHaveLockedMessages(handler, threadIds,
    536                 HAVE_LOCKED_MESSAGES_TOKEN);
    537     }
    538 
    539     /**
    540      * Build and show the proper delete thread dialog. The UI is slightly different
    541      * depending on whether there are locked messages in the thread(s) and whether we're
    542      * deleting single/multiple threads or all threads.
    543      * @param listener gets called when the delete button is pressed
    544      * @param deleteAll whether to show a single thread or all threads UI
    545      * @param hasLockedMessages whether the thread(s) contain locked messages
    546      * @param context used to load the various UI elements
    547      */
    548     public static void confirmDeleteThreadDialog(final DeleteThreadListener listener,
    549             Collection<Long> threadIds,
    550             boolean hasLockedMessages,
    551             Context context) {
    552         View contents = View.inflate(context, R.layout.delete_thread_dialog_view, null);
    553         TextView msg = (TextView)contents.findViewById(R.id.message);
    554 
    555         if (threadIds == null) {
    556             msg.setText(R.string.confirm_delete_all_conversations);
    557         } else {
    558             // Show the number of threads getting deleted in the confirmation dialog.
    559             int cnt = threadIds.size();
    560             msg.setText(context.getResources().getQuantityString(
    561                 R.plurals.confirm_delete_conversation, cnt, cnt));
    562         }
    563 
    564         final CheckBox checkbox = (CheckBox)contents.findViewById(R.id.delete_locked);
    565         if (!hasLockedMessages) {
    566             checkbox.setVisibility(View.GONE);
    567         } else {
    568             listener.setDeleteLockedMessage(checkbox.isChecked());
    569             checkbox.setOnClickListener(new View.OnClickListener() {
    570                 public void onClick(View v) {
    571                     listener.setDeleteLockedMessage(checkbox.isChecked());
    572                 }
    573             });
    574         }
    575 
    576         AlertDialog.Builder builder = new AlertDialog.Builder(context);
    577         builder.setTitle(R.string.confirm_dialog_title)
    578             .setIconAttribute(android.R.attr.alertDialogIcon)
    579             .setCancelable(true)
    580             .setPositiveButton(R.string.delete, listener)
    581             .setNegativeButton(R.string.no, null)
    582             .setView(contents)
    583             .show();
    584     }
    585 
    586     private final OnKeyListener mThreadListKeyListener = new OnKeyListener() {
    587         public boolean onKey(View v, int keyCode, KeyEvent event) {
    588             if (event.getAction() == KeyEvent.ACTION_DOWN) {
    589                 switch (keyCode) {
    590                     case KeyEvent.KEYCODE_DEL: {
    591                         long id = getListView().getSelectedItemId();
    592                         if (id > 0) {
    593                             confirmDeleteThread(id, mQueryHandler);
    594                         }
    595                         return true;
    596                     }
    597                 }
    598             }
    599             return false;
    600         }
    601     };
    602 
    603     public static class DeleteThreadListener implements OnClickListener {
    604         private final Collection<Long> mThreadIds;
    605         private final AsyncQueryHandler mHandler;
    606         private final Context mContext;
    607         private boolean mDeleteLockedMessages;
    608 
    609         public DeleteThreadListener(Collection<Long> threadIds, AsyncQueryHandler handler,
    610                 Context context) {
    611             mThreadIds = threadIds;
    612             mHandler = handler;
    613             mContext = context;
    614         }
    615 
    616         public void setDeleteLockedMessage(boolean deleteLockedMessages) {
    617             mDeleteLockedMessages = deleteLockedMessages;
    618         }
    619 
    620         public void onClick(DialogInterface dialog, final int whichButton) {
    621             MessageUtils.handleReadReport(mContext, mThreadIds,
    622                     PduHeaders.READ_STATUS__DELETED_WITHOUT_BEING_READ, new Runnable() {
    623                 public void run() {
    624                     int token = DELETE_CONVERSATION_TOKEN;
    625                     if (mThreadIds == null) {
    626                         Conversation.startDeleteAll(mHandler, token, mDeleteLockedMessages);
    627                         DraftCache.getInstance().refresh();
    628                     } else {
    629                         for (long threadId : mThreadIds) {
    630                             Conversation.startDelete(mHandler, token, mDeleteLockedMessages,
    631                                     threadId);
    632                             DraftCache.getInstance().setDraftState(threadId, false);
    633                         }
    634                     }
    635                 }
    636             });
    637             dialog.dismiss();
    638         }
    639     }
    640 
    641     private final Runnable mDeleteObsoleteThreadsRunnable = new Runnable() {
    642         public void run() {
    643             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    644                 LogTag.debug("mDeleteObsoleteThreadsRunnable getSavingDraft(): " +
    645                         DraftCache.getInstance().getSavingDraft());
    646             }
    647             if (DraftCache.getInstance().getSavingDraft()) {
    648                 // We're still saving a draft. Try again in a second. We don't want to delete
    649                 // any threads out from under the draft.
    650                 mHandler.postDelayed(mDeleteObsoleteThreadsRunnable, 1000);
    651             } else {
    652                 Conversation.asyncDeleteObsoleteThreads(mQueryHandler,
    653                         DELETE_OBSOLETE_THREADS_TOKEN);
    654             }
    655         }
    656     };
    657 
    658     private final class ThreadListQueryHandler extends AsyncQueryHandler {
    659         public ThreadListQueryHandler(ContentResolver contentResolver) {
    660             super(contentResolver);
    661         }
    662 
    663         @Override
    664         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
    665             switch (token) {
    666             case THREAD_LIST_QUERY_TOKEN:
    667                 mListAdapter.changeCursor(cursor);
    668                 setTitle(mTitle);
    669                 setProgressBarIndeterminateVisibility(false);
    670 
    671                 if (mNeedToMarkAsSeen) {
    672                     mNeedToMarkAsSeen = false;
    673                     Conversation.markAllConversationsAsSeen(getApplicationContext());
    674 
    675                     // Delete any obsolete threads. Obsolete threads are threads that aren't
    676                     // referenced by at least one message in the pdu or sms tables. We only call
    677                     // this on the first query (because of mNeedToMarkAsSeen).
    678                     mHandler.post(mDeleteObsoleteThreadsRunnable);
    679                 }
    680                 break;
    681 
    682             case UNREAD_THREADS_QUERY_TOKEN:
    683                 int count = cursor.getCount();
    684                 mUnreadConvCount.setText(count > 0 ? Integer.toString(count) : null);
    685                 break;
    686 
    687             case HAVE_LOCKED_MESSAGES_TOKEN:
    688                 Collection<Long> threadIds = (Collection<Long>)cookie;
    689                 confirmDeleteThreadDialog(new DeleteThreadListener(threadIds, mQueryHandler,
    690                         ConversationList.this), threadIds,
    691                         cursor != null && cursor.getCount() > 0,
    692                         ConversationList.this);
    693                 break;
    694 
    695             default:
    696                 Log.e(TAG, "onQueryComplete called with unknown token " + token);
    697             }
    698         }
    699 
    700         @Override
    701         protected void onDeleteComplete(int token, Object cookie, int result) {
    702             switch (token) {
    703             case DELETE_CONVERSATION_TOKEN:
    704                 // Rebuild the contacts cache now that a thread and its associated unique
    705                 // recipients have been deleted.
    706                 Contact.init(ConversationList.this);
    707 
    708                 // Make sure the conversation cache reflects the threads in the DB.
    709                 Conversation.init(ConversationList.this);
    710 
    711                 // Update the notification for new messages since they
    712                 // may be deleted.
    713                 MessagingNotification.nonBlockingUpdateNewMessageIndicator(ConversationList.this,
    714                         false, false);
    715                 // Update the notification for failed messages since they
    716                 // may be deleted.
    717                 MessagingNotification.updateSendFailedNotification(ConversationList.this);
    718 
    719                 // Make sure the list reflects the delete
    720                 startAsyncQuery();
    721                 break;
    722 
    723             case DELETE_OBSOLETE_THREADS_TOKEN:
    724                 // Nothing to do here.
    725                 break;
    726             }
    727         }
    728     }
    729 
    730     private class ModeCallback implements ListView.MultiChoiceModeListener {
    731         private View mMultiSelectActionBarView;
    732         private TextView mSelectedConvCount;
    733         private HashSet<Long> mSelectedThreadIds;
    734 
    735         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    736             MenuInflater inflater = getMenuInflater();
    737             mSelectedThreadIds = new HashSet<Long>();
    738             inflater.inflate(R.menu.conversation_multi_select_menu, menu);
    739 
    740             if (mMultiSelectActionBarView == null) {
    741                 mMultiSelectActionBarView = (ViewGroup)LayoutInflater.from(ConversationList.this)
    742                     .inflate(R.layout.conversation_list_multi_select_actionbar, null);
    743 
    744                 mSelectedConvCount =
    745                     (TextView)mMultiSelectActionBarView.findViewById(R.id.selected_conv_count);
    746             }
    747             mode.setCustomView(mMultiSelectActionBarView);
    748             ((TextView)mMultiSelectActionBarView.findViewById(R.id.title))
    749                 .setText(R.string.select_conversations);
    750             return true;
    751         }
    752 
    753         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
    754             if (mMultiSelectActionBarView == null) {
    755                 ViewGroup v = (ViewGroup)LayoutInflater.from(ConversationList.this)
    756                     .inflate(R.layout.conversation_list_multi_select_actionbar, null);
    757                 mode.setCustomView(v);
    758 
    759                 mSelectedConvCount = (TextView)v.findViewById(R.id.selected_conv_count);
    760             }
    761             return true;
    762         }
    763 
    764         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
    765             switch (item.getItemId()) {
    766                 case R.id.delete:
    767                     if (mSelectedThreadIds.size() > 0) {
    768                         confirmDeleteThreads(mSelectedThreadIds, mQueryHandler);
    769                     }
    770                     mode.finish();
    771                     break;
    772 
    773                 default:
    774                     break;
    775             }
    776             return true;
    777         }
    778 
    779         public void onDestroyActionMode(ActionMode mode) {
    780             ConversationListAdapter adapter = (ConversationListAdapter)getListView().getAdapter();
    781             adapter.uncheckAll();
    782             mSelectedThreadIds = null;
    783         }
    784 
    785         public void onItemCheckedStateChanged(ActionMode mode,
    786                 int position, long id, boolean checked) {
    787             ListView listView = getListView();
    788             final int checkedCount = listView.getCheckedItemCount();
    789             mSelectedConvCount.setText(Integer.toString(checkedCount));
    790 
    791             Cursor cursor  = (Cursor)listView.getItemAtPosition(position);
    792             Conversation conv = Conversation.from(ConversationList.this, cursor);
    793             conv.setIsChecked(checked);
    794             long threadId = conv.getThreadId();
    795 
    796             if (checked) {
    797                 mSelectedThreadIds.add(threadId);
    798             } else {
    799                 mSelectedThreadIds.remove(threadId);
    800             }
    801         }
    802 
    803     }
    804 
    805     private void log(String format, Object... args) {
    806         String s = String.format(format, args);
    807         Log.d(TAG, "[" + Thread.currentThread().getId() + "] " + s);
    808     }
    809 }
    810