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