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