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