• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 Esmertec AG.
3  * Copyright (C) 2008 The Android Open Source Project
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.mms.ui;
19 
20 import com.android.mms.LogTag;
21 import com.android.mms.R;
22 import com.android.mms.data.Contact;
23 import com.android.mms.data.ContactList;
24 import com.android.mms.data.Conversation;
25 import com.android.mms.transaction.MessagingNotification;
26 import com.android.mms.transaction.SmsRejectedReceiver;
27 import com.android.mms.util.DraftCache;
28 import com.android.mms.util.Recycler;
29 import com.google.android.mms.pdu.PduHeaders;
30 import com.google.android.mms.util.SqliteWrapper;
31 
32 import android.app.AlertDialog;
33 import android.app.ListActivity;
34 import android.content.AsyncQueryHandler;
35 import android.content.ContentResolver;
36 import android.content.Context;
37 import android.content.DialogInterface;
38 import android.content.Intent;
39 import android.content.SharedPreferences;
40 import android.content.DialogInterface.OnClickListener;
41 import android.content.res.Configuration;
42 import android.database.Cursor;
43 import android.database.sqlite.SQLiteException;
44 import android.os.Bundle;
45 import android.os.Handler;
46 import android.preference.PreferenceManager;
47 import android.provider.ContactsContract;
48 import android.provider.ContactsContract.Contacts;
49 import android.provider.Telephony.Mms;
50 import android.util.Log;
51 import android.view.ContextMenu;
52 import android.view.KeyEvent;
53 import android.view.LayoutInflater;
54 import android.view.Menu;
55 import android.view.MenuItem;
56 import android.view.View;
57 import android.view.Window;
58 import android.view.ContextMenu.ContextMenuInfo;
59 import android.view.View.OnCreateContextMenuListener;
60 import android.view.View.OnKeyListener;
61 import android.widget.AdapterView;
62 import android.widget.CheckBox;
63 import android.widget.ListView;
64 import android.widget.TextView;
65 
66 /**
67  * This activity provides a list view of existing conversations.
68  */
69 public class ConversationList extends ListActivity
70             implements DraftCache.OnDraftChangedListener {
71     private static final String TAG = "ConversationList";
72     private static final boolean DEBUG = false;
73     private static final boolean LOCAL_LOGV = DEBUG;
74 
75     private static final int THREAD_LIST_QUERY_TOKEN = 1701;
76     public static final int DELETE_CONVERSATION_TOKEN = 1801;
77     public static final int HAVE_LOCKED_MESSAGES_TOKEN = 1802;
78 
79     // IDs of the main menu items.
80     public static final int MENU_COMPOSE_NEW          = 0;
81     public static final int MENU_SEARCH               = 1;
82     public static final int MENU_DELETE_ALL           = 3;
83     public static final int MENU_PREFERENCES          = 4;
84 
85     // IDs of the context menu items for the list of conversations.
86     public static final int MENU_DELETE               = 0;
87     public static final int MENU_VIEW                 = 1;
88     public static final int MENU_VIEW_CONTACT         = 2;
89     public static final int MENU_ADD_TO_CONTACTS      = 3;
90 
91     private ThreadListQueryHandler mQueryHandler;
92     private ConversationListAdapter mListAdapter;
93     private CharSequence mTitle;
94     private SharedPreferences mPrefs;
95     private Handler mHandler;
96 
97     static private final String CHECKED_MESSAGE_LIMITS = "checked_message_limits";
98 
99     @Override
onCreate(Bundle savedInstanceState)100     protected void onCreate(Bundle savedInstanceState) {
101         super.onCreate(savedInstanceState);
102 
103         requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
104         setContentView(R.layout.conversation_list_screen);
105 
106         mQueryHandler = new ThreadListQueryHandler(getContentResolver());
107 
108         ListView listView = getListView();
109         LayoutInflater inflater = LayoutInflater.from(this);
110         ConversationHeaderView headerView = (ConversationHeaderView)
111                 inflater.inflate(R.layout.conversation_header, listView, false);
112         headerView.bind(getString(R.string.new_message),
113                 getString(R.string.create_new_message));
114         listView.addHeaderView(headerView, null, true);
115 
116         listView.setOnCreateContextMenuListener(mConvListOnCreateContextMenuListener);
117         listView.setOnKeyListener(mThreadListKeyListener);
118 
119         initListAdapter();
120 
121         mTitle = getString(R.string.app_label);
122 
123         mHandler = new Handler();
124         mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
125         boolean checkedMessageLimits = mPrefs.getBoolean(CHECKED_MESSAGE_LIMITS, false);
126         if (DEBUG) Log.v(TAG, "checkedMessageLimits: " + checkedMessageLimits);
127         if (!checkedMessageLimits || DEBUG) {
128             runOneTimeStorageLimitCheckForLegacyMessages();
129         }
130     }
131 
132     private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
133         new ConversationListAdapter.OnContentChangedListener() {
134         public void onContentChanged(ConversationListAdapter adapter) {
135             startAsyncQuery();
136         }
137     };
138 
initListAdapter()139     private void initListAdapter() {
140         mListAdapter = new ConversationListAdapter(this, null);
141         mListAdapter.setOnContentChangedListener(mContentChangedListener);
142         setListAdapter(mListAdapter);
143         getListView().setRecyclerListener(mListAdapter);
144     }
145 
146     /**
147      * Checks to see if the number of MMS and SMS messages are under the limits for the
148      * recycler. If so, it will automatically turn on the recycler setting. If not, it
149      * will prompt the user with a message and point them to the setting to manually
150      * turn on the recycler.
151      */
runOneTimeStorageLimitCheckForLegacyMessages()152     public synchronized void runOneTimeStorageLimitCheckForLegacyMessages() {
153         if (Recycler.isAutoDeleteEnabled(this)) {
154             if (DEBUG) Log.v(TAG, "recycler is already turned on");
155             // The recycler is already turned on. We don't need to check anything or warn
156             // the user, just remember that we've made the check.
157             markCheckedMessageLimit();
158             return;
159         }
160         new Thread(new Runnable() {
161             public void run() {
162                 if (Recycler.checkForThreadsOverLimit(ConversationList.this)) {
163                     if (DEBUG) Log.v(TAG, "checkForThreadsOverLimit TRUE");
164                     // Dang, one or more of the threads are over the limit. Show an activity
165                     // that'll encourage the user to manually turn on the setting. Delay showing
166                     // this activity until a couple of seconds after the conversation list appears.
167                     mHandler.postDelayed(new Runnable() {
168                         public void run() {
169                             Intent intent = new Intent(ConversationList.this,
170                                     WarnOfStorageLimitsActivity.class);
171                             startActivity(intent);
172                         }
173                     }, 2000);
174                 } else {
175                     if (DEBUG) Log.v(TAG, "checkForThreadsOverLimit silently turning on recycler");
176                     // No threads were over the limit. Turn on the recycler by default.
177                     runOnUiThread(new Runnable() {
178                         public void run() {
179                             SharedPreferences.Editor editor = mPrefs.edit();
180                             editor.putBoolean(MessagingPreferenceActivity.AUTO_DELETE, true);
181                             editor.commit();
182                         }
183                     });
184                 }
185                 // Remember that we don't have to do the check anymore when starting MMS.
186                 runOnUiThread(new Runnable() {
187                     public void run() {
188                         markCheckedMessageLimit();
189                     }
190                 });
191             }
192         }).start();
193     }
194 
195     /**
196      * Mark in preferences that we've checked the user's message limits. Once checked, we'll
197      * never check them again, unless the user wipe-data or resets the device.
198      */
markCheckedMessageLimit()199     private void markCheckedMessageLimit() {
200         if (DEBUG) Log.v(TAG, "markCheckedMessageLimit");
201         SharedPreferences.Editor editor = mPrefs.edit();
202         editor.putBoolean(CHECKED_MESSAGE_LIMITS, true);
203         editor.commit();
204     }
205 
206     @Override
onNewIntent(Intent intent)207     protected void onNewIntent(Intent intent) {
208         // Handle intents that occur after the activity has already been created.
209         privateOnStart();
210     }
211 
212     @Override
onStart()213     protected void onStart() {
214         super.onStart();
215 
216         MessagingNotification.cancelNotification(getApplicationContext(),
217                 SmsRejectedReceiver.SMS_REJECTED_NOTIFICATION_ID);
218 
219         Conversation.cleanup(this);
220 
221         DraftCache.getInstance().addOnDraftChangedListener(this);
222 
223         // We used to refresh the DraftCache here, but
224         // refreshing the DraftCache each time we go to the ConversationList seems overly
225         // aggressive. We already update the DraftCache when leaving CMA in onStop() and
226         // onNewIntent(), and when we delete threads or delete all in CMA or this activity.
227         // I hope we don't have to do such a heavy operation each time we enter here.
228 
229         privateOnStart();
230 
231         // we invalidate the contact cache here because we want to get updated presence
232         // and any contact changes. We don't invalidate the cache by observing presence and contact
233         // changes (since that's too untargeted), so as a tradeoff we do it here.
234         // If we're in the middle of the app initialization where we're loading the conversation
235         // threads, don't invalidate the cache because we're in the process of building it.
236         // TODO: think of a better way to invalidate cache more surgically or based on actual
237         // TODO: changes we care about
238         if (!Conversation.loadingThreads()) {
239             Contact.invalidateCache();
240         }
241     }
242 
privateOnStart()243     protected void privateOnStart() {
244         startAsyncQuery();
245     }
246 
247 
248     @Override
onStop()249     protected void onStop() {
250         super.onStop();
251 
252         DraftCache.getInstance().removeOnDraftChangedListener(this);
253         mListAdapter.changeCursor(null);
254     }
255 
onDraftChanged(final long threadId, final boolean hasDraft)256     public void onDraftChanged(final long threadId, final boolean hasDraft) {
257         // Run notifyDataSetChanged() on the main thread.
258         mQueryHandler.post(new Runnable() {
259             public void run() {
260                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
261                     log("onDraftChanged: threadId=" + threadId + ", hasDraft=" + hasDraft);
262                 }
263                 mListAdapter.notifyDataSetChanged();
264             }
265         });
266     }
267 
startAsyncQuery()268     private void startAsyncQuery() {
269         try {
270             setTitle(getString(R.string.refreshing));
271             setProgressBarIndeterminateVisibility(true);
272 
273             Conversation.startQueryForAll(mQueryHandler, THREAD_LIST_QUERY_TOKEN);
274         } catch (SQLiteException e) {
275             SqliteWrapper.checkSQLiteException(this, e);
276         }
277     }
278 
279     @Override
onPrepareOptionsMenu(Menu menu)280     public boolean onPrepareOptionsMenu(Menu menu) {
281         menu.clear();
282 
283         menu.add(0, MENU_COMPOSE_NEW, 0, R.string.menu_compose_new).setIcon(
284                 com.android.internal.R.drawable.ic_menu_compose);
285 
286         if (mListAdapter.getCount() > 0) {
287             menu.add(0, MENU_DELETE_ALL, 0, R.string.menu_delete_all).setIcon(
288                     android.R.drawable.ic_menu_delete);
289         }
290 
291         menu.add(0, MENU_SEARCH, 0, android.R.string.search_go).
292             setIcon(android.R.drawable.ic_menu_search).
293             setAlphabeticShortcut(android.app.SearchManager.MENU_KEY);
294 
295         menu.add(0, MENU_PREFERENCES, 0, R.string.menu_preferences).setIcon(
296                 android.R.drawable.ic_menu_preferences);
297 
298         return true;
299     }
300 
301     @Override
onSearchRequested()302     public boolean onSearchRequested() {
303         startSearch(null, false, null /*appData*/, false);
304         return true;
305     }
306 
307     @Override
onOptionsItemSelected(MenuItem item)308     public boolean onOptionsItemSelected(MenuItem item) {
309         switch(item.getItemId()) {
310             case MENU_COMPOSE_NEW:
311                 createNewMessage();
312                 break;
313             case MENU_SEARCH:
314                 onSearchRequested();
315                 break;
316             case MENU_DELETE_ALL:
317                 // The invalid threadId of -1 means all threads here.
318                 confirmDeleteThread(-1L, mQueryHandler);
319                 break;
320             case MENU_PREFERENCES: {
321                 Intent intent = new Intent(this, MessagingPreferenceActivity.class);
322                 startActivityIfNeeded(intent, -1);
323                 break;
324             }
325             default:
326                 return true;
327         }
328         return false;
329     }
330 
331     @Override
onListItemClick(ListView l, View v, int position, long id)332     protected void onListItemClick(ListView l, View v, int position, long id) {
333         if (LOCAL_LOGV) {
334             Log.v(TAG, "onListItemClick: position=" + position + ", id=" + id);
335         }
336 
337         if (position == 0) {
338             createNewMessage();
339         } else if (v instanceof ConversationHeaderView) {
340             ConversationHeaderView headerView = (ConversationHeaderView) v;
341             ConversationHeader ch = headerView.getConversationHeader();
342             openThread(ch.getThreadId());
343         }
344     }
345 
createNewMessage()346     private void createNewMessage() {
347         startActivity(ComposeMessageActivity.createIntent(this, 0));
348     }
349 
openThread(long threadId)350     private void openThread(long threadId) {
351         startActivity(ComposeMessageActivity.createIntent(this, threadId));
352     }
353 
createAddContactIntent(String address)354     public static Intent createAddContactIntent(String address) {
355         // address must be a single recipient
356         Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
357         intent.setType(Contacts.CONTENT_ITEM_TYPE);
358         if (Mms.isEmailAddress(address)) {
359             intent.putExtra(ContactsContract.Intents.Insert.EMAIL, address);
360         } else {
361             intent.putExtra(ContactsContract.Intents.Insert.PHONE, address);
362             intent.putExtra(ContactsContract.Intents.Insert.PHONE_TYPE,
363                     ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
364         }
365         intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
366 
367         return intent;
368     }
369 
370     private final OnCreateContextMenuListener mConvListOnCreateContextMenuListener =
371         new OnCreateContextMenuListener() {
372         public void onCreateContextMenu(ContextMenu menu, View v,
373                 ContextMenuInfo menuInfo) {
374             Cursor cursor = mListAdapter.getCursor();
375             if (cursor.getPosition() < 0) {
376                 return;
377             }
378             Conversation conv = Conversation.from(ConversationList.this, cursor);
379             ContactList recipients = conv.getRecipients();
380             menu.setHeaderTitle(recipients.formatNames(","));
381 
382             AdapterView.AdapterContextMenuInfo info =
383                 (AdapterView.AdapterContextMenuInfo) menuInfo;
384             if (info.position > 0) {
385                 menu.add(0, MENU_VIEW, 0, R.string.menu_view);
386 
387                 // Only show if there's a single recipient
388                 if (recipients.size() == 1) {
389                     // do we have this recipient in contacts?
390                     if (recipients.get(0).existsInDatabase()) {
391                         menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact);
392                     } else {
393                         menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts);
394                     }
395                 }
396                 menu.add(0, MENU_DELETE, 0, R.string.menu_delete);
397             }
398         }
399     };
400 
401     @Override
onContextItemSelected(MenuItem item)402     public boolean onContextItemSelected(MenuItem item) {
403         Cursor cursor = mListAdapter.getCursor();
404         if (cursor.getPosition() >= 0) {
405             Conversation conv = Conversation.from(ConversationList.this, cursor);
406             long threadId = conv.getThreadId();
407             switch (item.getItemId()) {
408             case MENU_DELETE: {
409                 confirmDeleteThread(threadId, mQueryHandler);
410                 break;
411             }
412             case MENU_VIEW: {
413                 openThread(threadId);
414                 break;
415             }
416             case MENU_VIEW_CONTACT: {
417                 Contact contact = conv.getRecipients().get(0);
418                 Intent intent = new Intent(Intent.ACTION_VIEW, contact.getUri());
419                 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
420                 startActivity(intent);
421                 break;
422             }
423             case MENU_ADD_TO_CONTACTS: {
424                 String address = conv.getRecipients().get(0).getNumber();
425                 startActivity(createAddContactIntent(address));
426                 break;
427             }
428             default:
429                 break;
430             }
431         }
432         return super.onContextItemSelected(item);
433     }
434 
435     @Override
onConfigurationChanged(Configuration newConfig)436     public void onConfigurationChanged(Configuration newConfig) {
437         // We override this method to avoid restarting the entire
438         // activity when the keyboard is opened (declared in
439         // AndroidManifest.xml).  Because the only translatable text
440         // in this activity is "New Message", which has the full width
441         // of phone to work with, localization shouldn't be a problem:
442         // no abbreviated alternate words should be needed even in
443         // 'wide' languages like German or Russian.
444 
445         super.onConfigurationChanged(newConfig);
446         if (DEBUG) Log.v(TAG, "onConfigurationChanged: " + newConfig);
447     }
448 
449     /**
450      * Start the process of putting up a dialog to confirm deleting a thread,
451      * but first start a background query to see if any of the threads or thread
452      * contain locked messages so we'll know how detailed of a UI to display.
453      * @param threadId id of the thread to delete or -1 for all threads
454      * @param handler query handler to do the background locked query
455      */
confirmDeleteThread(long threadId, AsyncQueryHandler handler)456     public static void confirmDeleteThread(long threadId, AsyncQueryHandler handler) {
457         Conversation.startQueryHaveLockedMessages(handler, threadId,
458                 HAVE_LOCKED_MESSAGES_TOKEN);
459     }
460 
461     /**
462      * Build and show the proper delete thread dialog. The UI is slightly different
463      * depending on whether there are locked messages in the thread(s) and whether we're
464      * deleting a single thread or all threads.
465      * @param listener gets called when the delete button is pressed
466      * @param deleteAll whether to show a single thread or all threads UI
467      * @param hasLockedMessages whether the thread(s) contain locked messages
468      * @param context used to load the various UI elements
469      */
confirmDeleteThreadDialog(final DeleteThreadListener listener, boolean deleteAll, boolean hasLockedMessages, Context context)470     public static void confirmDeleteThreadDialog(final DeleteThreadListener listener,
471             boolean deleteAll,
472             boolean hasLockedMessages,
473             Context context) {
474         View contents = View.inflate(context, R.layout.delete_thread_dialog_view, null);
475         TextView msg = (TextView)contents.findViewById(R.id.message);
476         msg.setText(deleteAll
477                 ? R.string.confirm_delete_all_conversations
478                         : R.string.confirm_delete_conversation);
479         final CheckBox checkbox = (CheckBox)contents.findViewById(R.id.delete_locked);
480         if (!hasLockedMessages) {
481             checkbox.setVisibility(View.GONE);
482         } else {
483             listener.setDeleteLockedMessage(checkbox.isChecked());
484             checkbox.setOnClickListener(new View.OnClickListener() {
485                 public void onClick(View v) {
486                     listener.setDeleteLockedMessage(checkbox.isChecked());
487                 }
488             });
489         }
490 
491         AlertDialog.Builder builder = new AlertDialog.Builder(context);
492         builder.setTitle(R.string.confirm_dialog_title)
493             .setIcon(android.R.drawable.ic_dialog_alert)
494         .setCancelable(true)
495         .setPositiveButton(R.string.delete, listener)
496         .setNegativeButton(R.string.no, null)
497         .setView(contents)
498         .show();
499     }
500 
501     private final OnKeyListener mThreadListKeyListener = new OnKeyListener() {
502         public boolean onKey(View v, int keyCode, KeyEvent event) {
503             if (event.getAction() == KeyEvent.ACTION_DOWN) {
504                 switch (keyCode) {
505                     case KeyEvent.KEYCODE_DEL: {
506                         long id = getListView().getSelectedItemId();
507                         if (id > 0) {
508                             confirmDeleteThread(id, mQueryHandler);
509                         }
510                         return true;
511                     }
512                 }
513             }
514             return false;
515         }
516     };
517 
518     public static class DeleteThreadListener implements OnClickListener {
519         private final long mThreadId;
520         private final AsyncQueryHandler mHandler;
521         private final Context mContext;
522         private boolean mDeleteLockedMessages;
523 
DeleteThreadListener(long threadId, AsyncQueryHandler handler, Context context)524         public DeleteThreadListener(long threadId, AsyncQueryHandler handler, Context context) {
525             mThreadId = threadId;
526             mHandler = handler;
527             mContext = context;
528         }
529 
setDeleteLockedMessage(boolean deleteLockedMessages)530         public void setDeleteLockedMessage(boolean deleteLockedMessages) {
531             mDeleteLockedMessages = deleteLockedMessages;
532         }
533 
onClick(DialogInterface dialog, final int whichButton)534         public void onClick(DialogInterface dialog, final int whichButton) {
535             MessageUtils.handleReadReport(mContext, mThreadId,
536                     PduHeaders.READ_STATUS__DELETED_WITHOUT_BEING_READ, new Runnable() {
537                 public void run() {
538                     int token = DELETE_CONVERSATION_TOKEN;
539                     if (mThreadId == -1) {
540                         Conversation.startDeleteAll(mHandler, token, mDeleteLockedMessages);
541                         DraftCache.getInstance().refresh();
542                     } else {
543                         Conversation.startDelete(mHandler, token, mDeleteLockedMessages,
544                                 mThreadId);
545                         DraftCache.getInstance().setDraftState(mThreadId, false);
546                     }
547                 }
548             });
549         }
550     }
551 
552     private final class ThreadListQueryHandler extends AsyncQueryHandler {
ThreadListQueryHandler(ContentResolver contentResolver)553         public ThreadListQueryHandler(ContentResolver contentResolver) {
554             super(contentResolver);
555         }
556 
557         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)558         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
559             switch (token) {
560             case THREAD_LIST_QUERY_TOKEN:
561                 mListAdapter.changeCursor(cursor);
562                 setTitle(mTitle);
563                 setProgressBarIndeterminateVisibility(false);
564                 break;
565 
566             case HAVE_LOCKED_MESSAGES_TOKEN:
567                 long threadId = (Long)cookie;
568                 confirmDeleteThreadDialog(new DeleteThreadListener(threadId, mQueryHandler,
569                         ConversationList.this), threadId == -1,
570                         cursor != null && cursor.getCount() > 0,
571                         ConversationList.this);
572                 break;
573 
574             default:
575                 Log.e(TAG, "onQueryComplete called with unknown token " + token);
576             }
577         }
578 
579         @Override
onDeleteComplete(int token, Object cookie, int result)580         protected void onDeleteComplete(int token, Object cookie, int result) {
581             switch (token) {
582             case DELETE_CONVERSATION_TOKEN:
583                 // Make sure the conversation cache reflects the threads in the DB.
584                 Conversation.init(ConversationList.this);
585 
586                 // Update the notification for new messages since they
587                 // may be deleted.
588                 MessagingNotification.updateNewMessageIndicator(ConversationList.this);
589                 // Update the notification for failed messages since they
590                 // may be deleted.
591                 MessagingNotification.updateSendFailedNotification(ConversationList.this);
592 
593                 // Make sure the list reflects the delete
594                 startAsyncQuery();
595 
596                 onContentChanged();
597                 break;
598             }
599         }
600     }
601 
log(String format, Object... args)602     private void log(String format, Object... args) {
603         String s = String.format(format, args);
604         Log.d(TAG, "[" + Thread.currentThread().getId() + "] " + s);
605     }
606 }
607