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