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