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