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 = "ConversationList";
     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         DraftCache.getInstance().removeOnDraftChangedListener(this);
    412 
    413         // Simply setting the choice mode causes the previous choice mode to finish and we exit
    414         // multi-select mode (if we're in it) and remove all the selections.
    415         getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
    416 
    417         // Close the cursor in the ListAdapter if the activity stopped.
    418         Cursor cursor = mListAdapter.getCursor();
    419 
    420         if (cursor != null && !cursor.isClosed()) {
    421             cursor.close();
    422         }
    423 
    424         mListAdapter.changeCursor(null);
    425     }
    426 
    427     @Override
    428     public void onDraftChanged(final long threadId, final boolean hasDraft) {
    429         // Run notifyDataSetChanged() on the main thread.
    430         mQueryHandler.post(new Runnable() {
    431             @Override
    432             public void run() {
    433                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    434                     log("onDraftChanged: threadId=" + threadId + ", hasDraft=" + hasDraft);
    435                 }
    436                 mListAdapter.notifyDataSetChanged();
    437             }
    438         });
    439     }
    440 
    441     private void startAsyncQuery() {
    442         try {
    443             ((TextView)(getListView().getEmptyView())).setText(R.string.loading_conversations);
    444 
    445             Conversation.startQueryForAll(mQueryHandler, THREAD_LIST_QUERY_TOKEN);
    446             Conversation.startQuery(mQueryHandler, UNREAD_THREADS_QUERY_TOKEN, Threads.READ + "=0");
    447         } catch (SQLiteException e) {
    448             SqliteWrapper.checkSQLiteException(this, e);
    449         }
    450     }
    451 
    452     SearchView.OnQueryTextListener mQueryTextListener = new SearchView.OnQueryTextListener() {
    453         @Override
    454         public boolean onQueryTextSubmit(String query) {
    455             Intent intent = new Intent();
    456             intent.setClass(ConversationList.this, SearchActivity.class);
    457             intent.putExtra(SearchManager.QUERY, query);
    458             startActivity(intent);
    459             mSearchItem.collapseActionView();
    460             return true;
    461         }
    462 
    463         @Override
    464         public boolean onQueryTextChange(String newText) {
    465             return false;
    466         }
    467     };
    468 
    469     @Override
    470     public boolean onCreateOptionsMenu(Menu menu) {
    471         getMenuInflater().inflate(R.menu.conversation_list_menu, menu);
    472 
    473         mSearchItem = menu.findItem(R.id.search);
    474         mSearchView = (SearchView) mSearchItem.getActionView();
    475 
    476         mSearchView.setOnQueryTextListener(mQueryTextListener);
    477         mSearchView.setQueryHint(getString(R.string.search_hint));
    478         mSearchView.setIconifiedByDefault(true);
    479         SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
    480 
    481         if (searchManager != null) {
    482             SearchableInfo info = searchManager.getSearchableInfo(this.getComponentName());
    483             mSearchView.setSearchableInfo(info);
    484         }
    485 
    486         MenuItem cellBroadcastItem = menu.findItem(R.id.action_cell_broadcasts);
    487         if (cellBroadcastItem != null) {
    488             // Enable link to Cell broadcast activity depending on the value in config.xml.
    489             boolean isCellBroadcastAppLinkEnabled = this.getResources().getBoolean(
    490                     com.android.internal.R.bool.config_cellBroadcastAppLinks);
    491             try {
    492                 if (isCellBroadcastAppLinkEnabled) {
    493                     PackageManager pm = getPackageManager();
    494                     if (pm.getApplicationEnabledSetting("com.android.cellbroadcastreceiver")
    495                             == PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
    496                         isCellBroadcastAppLinkEnabled = false;  // CMAS app disabled
    497                     }
    498                 }
    499             } catch (IllegalArgumentException ignored) {
    500                 isCellBroadcastAppLinkEnabled = false;  // CMAS app not installed
    501             }
    502             if (!isCellBroadcastAppLinkEnabled) {
    503                 cellBroadcastItem.setVisible(false);
    504             }
    505         }
    506 
    507         return true;
    508     }
    509 
    510     @Override
    511     public boolean onPrepareOptionsMenu(Menu menu) {
    512         MenuItem item = menu.findItem(R.id.action_delete_all);
    513         if (item != null) {
    514             item.setVisible((mListAdapter.getCount() > 0) && mIsSmsEnabled);
    515         }
    516         item = menu.findItem(R.id.action_compose_new);
    517         if (item != null ){
    518             // Dim compose if SMS is disabled because it will not work (will show a toast)
    519             item.getIcon().setAlpha(mIsSmsEnabled ? 255 : 127);
    520         }
    521         if (!LogTag.DEBUG_DUMP) {
    522             item = menu.findItem(R.id.action_debug_dump);
    523             if (item != null) {
    524                 item.setVisible(false);
    525             }
    526         }
    527         return true;
    528     }
    529 
    530     @Override
    531     public boolean onSearchRequested() {
    532         if (mSearchItem != null) {
    533             mSearchItem.expandActionView();
    534         }
    535         return true;
    536     }
    537 
    538     @Override
    539     public boolean onOptionsItemSelected(MenuItem item) {
    540         switch(item.getItemId()) {
    541             case R.id.action_compose_new:
    542                 if (mIsSmsEnabled) {
    543                     createNewMessage();
    544                 } else {
    545                     // Display a toast letting the user know they can not compose.
    546                     if (mComposeDisabledToast == null) {
    547                         mComposeDisabledToast = Toast.makeText(this,
    548                                 R.string.compose_disabled_toast, Toast.LENGTH_SHORT);
    549                     }
    550                     mComposeDisabledToast.show();
    551                 }
    552                 break;
    553             case R.id.action_delete_all:
    554                 // The invalid threadId of -1 means all threads here.
    555                 confirmDeleteThread(-1L, mQueryHandler);
    556                 break;
    557             case R.id.action_settings:
    558                 Intent intent = new Intent(this, MessagingPreferenceActivity.class);
    559                 startActivityIfNeeded(intent, -1);
    560                 break;
    561             case R.id.action_debug_dump:
    562                 LogTag.dumpInternalTables(this);
    563                 break;
    564             case R.id.action_cell_broadcasts:
    565                 Intent cellBroadcastIntent = new Intent(Intent.ACTION_MAIN);
    566                 cellBroadcastIntent.setComponent(new ComponentName(
    567                         "com.android.cellbroadcastreceiver",
    568                         "com.android.cellbroadcastreceiver.CellBroadcastListActivity"));
    569                 cellBroadcastIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    570                 try {
    571                     startActivity(cellBroadcastIntent);
    572                 } catch (ActivityNotFoundException ignored) {
    573                     Log.e(TAG, "ActivityNotFoundException for CellBroadcastListActivity");
    574                 }
    575                 return true;
    576             default:
    577                 return true;
    578         }
    579         return false;
    580     }
    581 
    582     @Override
    583     protected void onListItemClick(ListView l, View v, int position, long id) {
    584         // Note: don't read the thread id data from the ConversationListItem view passed in.
    585         // It's unreliable to read the cached data stored in the view because the ListItem
    586         // can be recycled, and the same view could be assigned to a different position
    587         // if you click the list item fast enough. Instead, get the cursor at the position
    588         // clicked and load the data from the cursor.
    589         // (ConversationListAdapter extends CursorAdapter, so getItemAtPosition() should
    590         // return the cursor object, which is moved to the position passed in)
    591         Cursor cursor  = (Cursor) getListView().getItemAtPosition(position);
    592         Conversation conv = Conversation.from(this, cursor);
    593         long tid = conv.getThreadId();
    594 
    595         if (LogTag.VERBOSE) {
    596             Log.d(TAG, "onListItemClick: pos=" + position + ", view=" + v + ", tid=" + tid);
    597         }
    598 
    599         openThread(tid);
    600     }
    601 
    602     private void createNewMessage() {
    603         startActivity(ComposeMessageActivity.createIntent(this, 0));
    604     }
    605 
    606     private void openThread(long threadId) {
    607         startActivity(ComposeMessageActivity.createIntent(this, threadId));
    608     }
    609 
    610     public static Intent createAddContactIntent(String address) {
    611         // address must be a single recipient
    612         Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
    613         intent.setType(Contacts.CONTENT_ITEM_TYPE);
    614         if (Mms.isEmailAddress(address)) {
    615             intent.putExtra(ContactsContract.Intents.Insert.EMAIL, address);
    616         } else {
    617             intent.putExtra(ContactsContract.Intents.Insert.PHONE, address);
    618             intent.putExtra(ContactsContract.Intents.Insert.PHONE_TYPE,
    619                     ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
    620         }
    621         intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
    622 
    623         return intent;
    624     }
    625 
    626     private final OnCreateContextMenuListener mConvListOnCreateContextMenuListener =
    627         new OnCreateContextMenuListener() {
    628         @Override
    629         public void onCreateContextMenu(ContextMenu menu, View v,
    630                 ContextMenuInfo menuInfo) {
    631             Cursor cursor = mListAdapter.getCursor();
    632             if (cursor == null || cursor.getPosition() < 0) {
    633                 return;
    634             }
    635             Conversation conv = Conversation.from(ConversationList.this, cursor);
    636             ContactList recipients = conv.getRecipients();
    637             menu.setHeaderTitle(recipients.formatNames(","));
    638 
    639             AdapterView.AdapterContextMenuInfo info =
    640                 (AdapterView.AdapterContextMenuInfo) menuInfo;
    641             menu.add(0, MENU_VIEW, 0, R.string.menu_view);
    642 
    643             // Only show if there's a single recipient
    644             if (recipients.size() == 1) {
    645                 // do we have this recipient in contacts?
    646                 if (recipients.get(0).existsInDatabase()) {
    647                     menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact);
    648                 } else {
    649                     menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts);
    650                 }
    651             }
    652             if (mIsSmsEnabled) {
    653                 menu.add(0, MENU_DELETE, 0, R.string.menu_delete);
    654             }
    655         }
    656     };
    657 
    658     @Override
    659     public boolean onContextItemSelected(MenuItem item) {
    660         Cursor cursor = mListAdapter.getCursor();
    661         if (cursor != null && cursor.getPosition() >= 0) {
    662             Conversation conv = Conversation.from(ConversationList.this, cursor);
    663             long threadId = conv.getThreadId();
    664             switch (item.getItemId()) {
    665             case MENU_DELETE: {
    666                 confirmDeleteThread(threadId, mQueryHandler);
    667                 break;
    668             }
    669             case MENU_VIEW: {
    670                 openThread(threadId);
    671                 break;
    672             }
    673             case MENU_VIEW_CONTACT: {
    674                 Contact contact = conv.getRecipients().get(0);
    675                 Intent intent = new Intent(Intent.ACTION_VIEW, contact.getUri());
    676                 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
    677                 startActivity(intent);
    678                 break;
    679             }
    680             case MENU_ADD_TO_CONTACTS: {
    681                 String address = conv.getRecipients().get(0).getNumber();
    682                 startActivity(createAddContactIntent(address));
    683                 break;
    684             }
    685             default:
    686                 break;
    687             }
    688         }
    689         return super.onContextItemSelected(item);
    690     }
    691 
    692     @Override
    693     public void onConfigurationChanged(Configuration newConfig) {
    694         // We override this method to avoid restarting the entire
    695         // activity when the keyboard is opened (declared in
    696         // AndroidManifest.xml).  Because the only translatable text
    697         // in this activity is "New Message", which has the full width
    698         // of phone to work with, localization shouldn't be a problem:
    699         // no abbreviated alternate words should be needed even in
    700         // 'wide' languages like German or Russian.
    701 
    702         super.onConfigurationChanged(newConfig);
    703         if (DEBUG) Log.v(TAG, "onConfigurationChanged: " + newConfig);
    704     }
    705 
    706     /**
    707      * Start the process of putting up a dialog to confirm deleting a thread,
    708      * but first start a background query to see if any of the threads or thread
    709      * contain locked messages so we'll know how detailed of a UI to display.
    710      * @param threadId id of the thread to delete or -1 for all threads
    711      * @param handler query handler to do the background locked query
    712      */
    713     public static void confirmDeleteThread(long threadId, AsyncQueryHandler handler) {
    714         ArrayList<Long> threadIds = null;
    715         if (threadId != -1) {
    716             threadIds = new ArrayList<Long>();
    717             threadIds.add(threadId);
    718         }
    719         confirmDeleteThreads(threadIds, handler);
    720     }
    721 
    722     /**
    723      * Start the process of putting up a dialog to confirm deleting threads,
    724      * but first start a background query to see if any of the threads
    725      * contain locked messages so we'll know how detailed of a UI to display.
    726      * @param threadIds list of threadIds to delete or null for all threads
    727      * @param handler query handler to do the background locked query
    728      */
    729     public static void confirmDeleteThreads(Collection<Long> threadIds, AsyncQueryHandler handler) {
    730         Conversation.startQueryHaveLockedMessages(handler, threadIds,
    731                 HAVE_LOCKED_MESSAGES_TOKEN);
    732     }
    733 
    734     /**
    735      * Build and show the proper delete thread dialog. The UI is slightly different
    736      * depending on whether there are locked messages in the thread(s) and whether we're
    737      * deleting single/multiple threads or all threads.
    738      * @param listener gets called when the delete button is pressed
    739      * @param threadIds the thread IDs to be deleted (pass null for all threads)
    740      * @param hasLockedMessages whether the thread(s) contain locked messages
    741      * @param context used to load the various UI elements
    742      */
    743     public static void confirmDeleteThreadDialog(final DeleteThreadListener listener,
    744             Collection<Long> threadIds,
    745             boolean hasLockedMessages,
    746             Context context) {
    747         View contents = View.inflate(context, R.layout.delete_thread_dialog_view, null);
    748         TextView msg = (TextView)contents.findViewById(R.id.message);
    749 
    750         if (threadIds == null) {
    751             msg.setText(R.string.confirm_delete_all_conversations);
    752         } else {
    753             // Show the number of threads getting deleted in the confirmation dialog.
    754             int cnt = threadIds.size();
    755             msg.setText(context.getResources().getQuantityString(
    756                 R.plurals.confirm_delete_conversation, cnt, cnt));
    757         }
    758 
    759         final CheckBox checkbox = (CheckBox)contents.findViewById(R.id.delete_locked);
    760         if (!hasLockedMessages) {
    761             checkbox.setVisibility(View.GONE);
    762         } else {
    763             listener.setDeleteLockedMessage(checkbox.isChecked());
    764             checkbox.setOnClickListener(new View.OnClickListener() {
    765                 @Override
    766                 public void onClick(View v) {
    767                     listener.setDeleteLockedMessage(checkbox.isChecked());
    768                 }
    769             });
    770         }
    771 
    772         AlertDialog.Builder builder = new AlertDialog.Builder(context);
    773         builder.setTitle(R.string.confirm_dialog_title)
    774             .setIconAttribute(android.R.attr.alertDialogIcon)
    775             .setCancelable(true)
    776             .setPositiveButton(R.string.delete, listener)
    777             .setNegativeButton(R.string.no, null)
    778             .setView(contents)
    779             .show();
    780     }
    781 
    782     private final OnKeyListener mThreadListKeyListener = new OnKeyListener() {
    783         @Override
    784         public boolean onKey(View v, int keyCode, KeyEvent event) {
    785             if (event.getAction() == KeyEvent.ACTION_DOWN) {
    786                 switch (keyCode) {
    787                     case KeyEvent.KEYCODE_DEL: {
    788                         long id = getListView().getSelectedItemId();
    789                         if (id > 0) {
    790                             confirmDeleteThread(id, mQueryHandler);
    791                         }
    792                         return true;
    793                     }
    794                 }
    795             }
    796             return false;
    797         }
    798     };
    799 
    800     public static class DeleteThreadListener implements OnClickListener {
    801         private final Collection<Long> mThreadIds;
    802         private final ConversationQueryHandler mHandler;
    803         private final Context mContext;
    804         private boolean mDeleteLockedMessages;
    805 
    806         public DeleteThreadListener(Collection<Long> threadIds, ConversationQueryHandler handler,
    807                 Context context) {
    808             mThreadIds = threadIds;
    809             mHandler = handler;
    810             mContext = context;
    811         }
    812 
    813         public void setDeleteLockedMessage(boolean deleteLockedMessages) {
    814             mDeleteLockedMessages = deleteLockedMessages;
    815         }
    816 
    817         @Override
    818         public void onClick(DialogInterface dialog, final int whichButton) {
    819             MessageUtils.handleReadReport(mContext, mThreadIds,
    820                     PduHeaders.READ_STATUS__DELETED_WITHOUT_BEING_READ, new Runnable() {
    821                 @Override
    822                 public void run() {
    823                     int token = DELETE_CONVERSATION_TOKEN;
    824                     if (mThreadIds == null) {
    825                         Conversation.startDeleteAll(mHandler, token, mDeleteLockedMessages);
    826                         DraftCache.getInstance().refresh();
    827                     } else {
    828                         Conversation.startDelete(mHandler, token, mDeleteLockedMessages,
    829                                 mThreadIds);
    830                     }
    831                 }
    832             });
    833             dialog.dismiss();
    834         }
    835     }
    836 
    837     private final Runnable mDeleteObsoleteThreadsRunnable = new Runnable() {
    838         @Override
    839         public void run() {
    840             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    841                 LogTag.debug("mDeleteObsoleteThreadsRunnable getSavingDraft(): " +
    842                         DraftCache.getInstance().getSavingDraft());
    843             }
    844             if (DraftCache.getInstance().getSavingDraft()) {
    845                 // We're still saving a draft. Try again in a second. We don't want to delete
    846                 // any threads out from under the draft.
    847                 if (DEBUGCLEANUP) {
    848                     LogTag.debug("mDeleteObsoleteThreadsRunnable saving draft, trying again");
    849                 }
    850                 mHandler.postDelayed(mDeleteObsoleteThreadsRunnable, 1000);
    851             } else {
    852                 if (DEBUGCLEANUP) {
    853                     LogTag.debug("mDeleteObsoleteThreadsRunnable calling " +
    854                             "asyncDeleteObsoleteThreads");
    855                 }
    856                 Conversation.asyncDeleteObsoleteThreads(mQueryHandler,
    857                         DELETE_OBSOLETE_THREADS_TOKEN);
    858             }
    859         }
    860     };
    861 
    862     private final class ThreadListQueryHandler extends ConversationQueryHandler {
    863         public ThreadListQueryHandler(ContentResolver contentResolver) {
    864             super(contentResolver);
    865         }
    866 
    867         // Test code used for various scenarios where its desirable to insert a delay in
    868         // responding to query complete. To use, uncomment out the block below and then
    869         // comment out the @Override and onQueryComplete line.
    870 //        @Override
    871 //        protected void onQueryComplete(final int token, final Object cookie, final Cursor cursor) {
    872 //            mHandler.postDelayed(new Runnable() {
    873 //                public void run() {
    874 //                    myonQueryComplete(token, cookie, cursor);
    875 //                    }
    876 //            }, 2000);
    877 //        }
    878 //
    879 //        protected void myonQueryComplete(int token, Object cookie, Cursor cursor) {
    880 
    881         @Override
    882         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
    883             switch (token) {
    884             case THREAD_LIST_QUERY_TOKEN:
    885                 mListAdapter.changeCursor(cursor);
    886 
    887                 if (mListAdapter.getCount() == 0) {
    888                     ((TextView)(getListView().getEmptyView())).setText(R.string.no_conversations);
    889                 }
    890 
    891                 if (mDoOnceAfterFirstQuery) {
    892                     mDoOnceAfterFirstQuery = false;
    893                     // Delay doing a couple of DB operations until we've initially queried the DB
    894                     // for the list of conversations to display. We don't want to slow down showing
    895                     // the initial UI.
    896 
    897                     // 1. Delete any obsolete threads. Obsolete threads are threads that aren't
    898                     // referenced by at least one message in the pdu or sms tables.
    899                     mHandler.post(mDeleteObsoleteThreadsRunnable);
    900 
    901                     // 2. Mark all the conversations as seen.
    902                     Conversation.markAllConversationsAsSeen(getApplicationContext());
    903                 }
    904                 if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
    905                     // Restore the list to its previous position.
    906                     getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
    907                             mSavedFirstItemOffset);
    908                     mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
    909                 }
    910                 break;
    911 
    912             case UNREAD_THREADS_QUERY_TOKEN:
    913                 int count = 0;
    914                 if (cursor != null) {
    915                     count = cursor.getCount();
    916                     cursor.close();
    917                 }
    918                 mUnreadConvCount.setText(count > 0 ? Integer.toString(count) : null);
    919                 break;
    920 
    921             case HAVE_LOCKED_MESSAGES_TOKEN:
    922                 @SuppressWarnings("unchecked")
    923                 Collection<Long> threadIds = (Collection<Long>)cookie;
    924                 confirmDeleteThreadDialog(new DeleteThreadListener(threadIds, mQueryHandler,
    925                         ConversationList.this), threadIds,
    926                         cursor != null && cursor.getCount() > 0,
    927                         ConversationList.this);
    928                 if (cursor != null) {
    929                     cursor.close();
    930                 }
    931                 break;
    932 
    933             default:
    934                 Log.e(TAG, "onQueryComplete called with unknown token " + token);
    935             }
    936         }
    937 
    938         @Override
    939         protected void onDeleteComplete(int token, Object cookie, int result) {
    940             super.onDeleteComplete(token, cookie, result);
    941             switch (token) {
    942             case DELETE_CONVERSATION_TOKEN:
    943                 long threadId = cookie != null ? (Long)cookie : -1;     // default to all threads
    944 
    945                 if (threadId == -1) {
    946                     // Rebuild the contacts cache now that all threads and their associated unique
    947                     // recipients have been deleted.
    948                     Contact.init(ConversationList.this);
    949                 } else {
    950                     // Remove any recipients referenced by this single thread from the
    951                     // contacts cache. It's possible for two or more threads to reference
    952                     // the same contact. That's ok if we remove it. We'll recreate that contact
    953                     // when we init all Conversations below.
    954                     Conversation conv = Conversation.get(ConversationList.this, threadId, false);
    955                     if (conv != null) {
    956                         ContactList recipients = conv.getRecipients();
    957                         for (Contact contact : recipients) {
    958                             contact.removeFromCache();
    959                         }
    960                     }
    961                 }
    962                 // Make sure the conversation cache reflects the threads in the DB.
    963                 Conversation.init(ConversationList.this);
    964 
    965                 // Update the notification for new messages since they
    966                 // may be deleted.
    967                 MessagingNotification.nonBlockingUpdateNewMessageIndicator(ConversationList.this,
    968                         MessagingNotification.THREAD_NONE, false);
    969                 // Update the notification for failed messages since they
    970                 // may be deleted.
    971                 MessagingNotification.nonBlockingUpdateSendFailedNotification(ConversationList.this);
    972 
    973                 // Make sure the list reflects the delete
    974                 startAsyncQuery();
    975 
    976                 MmsWidgetProvider.notifyDatasetChanged(getApplicationContext());
    977                 break;
    978 
    979             case DELETE_OBSOLETE_THREADS_TOKEN:
    980                 if (DEBUGCLEANUP) {
    981                     LogTag.debug("onQueryComplete finished DELETE_OBSOLETE_THREADS_TOKEN");
    982                 }
    983                 break;
    984             }
    985         }
    986     }
    987 
    988     private class ModeCallback implements ListView.MultiChoiceModeListener {
    989         private View mMultiSelectActionBarView;
    990         private TextView mSelectedConvCount;
    991         private HashSet<Long> mSelectedThreadIds;
    992 
    993         @Override
    994         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    995             MenuInflater inflater = getMenuInflater();
    996             mSelectedThreadIds = new HashSet<Long>();
    997             inflater.inflate(R.menu.conversation_multi_select_menu, menu);
    998 
    999             if (mMultiSelectActionBarView == null) {
   1000                 mMultiSelectActionBarView = LayoutInflater.from(ConversationList.this)
   1001                     .inflate(R.layout.conversation_list_multi_select_actionbar, null);
   1002 
   1003                 mSelectedConvCount =
   1004                     (TextView)mMultiSelectActionBarView.findViewById(R.id.selected_conv_count);
   1005             }
   1006             mode.setCustomView(mMultiSelectActionBarView);
   1007             ((TextView)mMultiSelectActionBarView.findViewById(R.id.title))
   1008                 .setText(R.string.select_conversations);
   1009             return true;
   1010         }
   1011 
   1012         @Override
   1013         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
   1014             if (mMultiSelectActionBarView == null) {
   1015                 ViewGroup v = (ViewGroup)LayoutInflater.from(ConversationList.this)
   1016                     .inflate(R.layout.conversation_list_multi_select_actionbar, null);
   1017                 mode.setCustomView(v);
   1018 
   1019                 mSelectedConvCount = (TextView)v.findViewById(R.id.selected_conv_count);
   1020             }
   1021             return true;
   1022         }
   1023 
   1024         @Override
   1025         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
   1026             switch (item.getItemId()) {
   1027                 case R.id.delete:
   1028                     if (mSelectedThreadIds.size() > 0) {
   1029                         confirmDeleteThreads(mSelectedThreadIds, mQueryHandler);
   1030                     }
   1031                     mode.finish();
   1032                     break;
   1033 
   1034                 default:
   1035                     break;
   1036             }
   1037             return true;
   1038         }
   1039 
   1040         @Override
   1041         public void onDestroyActionMode(ActionMode mode) {
   1042             ConversationListAdapter adapter = (ConversationListAdapter)getListView().getAdapter();
   1043             adapter.uncheckAll();
   1044             mSelectedThreadIds = null;
   1045         }
   1046 
   1047         @Override
   1048         public void onItemCheckedStateChanged(ActionMode mode,
   1049                 int position, long id, boolean checked) {
   1050             ListView listView = getListView();
   1051             final int checkedCount = listView.getCheckedItemCount();
   1052             mSelectedConvCount.setText(Integer.toString(checkedCount));
   1053 
   1054             Cursor cursor  = (Cursor)listView.getItemAtPosition(position);
   1055             Conversation conv = Conversation.from(ConversationList.this, cursor);
   1056             conv.setIsChecked(checked);
   1057             long threadId = conv.getThreadId();
   1058 
   1059             if (checked) {
   1060                 mSelectedThreadIds.add(threadId);
   1061             } else {
   1062                 mSelectedThreadIds.remove(threadId);
   1063             }
   1064         }
   1065 
   1066     }
   1067 
   1068     private void log(String format, Object... args) {
   1069         String s = String.format(format, args);
   1070         Log.d(TAG, "[" + Thread.currentThread().getId() + "] " + s);
   1071     }
   1072 }
   1073