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