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