ConversationList.java revision 07ce1878a36d2df1707dd4bbd9cd7235679bdc94
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 18package com.android.mms.ui; 19 20import com.android.mms.R; 21import com.android.mms.data.Contact; 22import com.android.mms.data.ContactList; 23import com.android.mms.data.Conversation; 24import com.android.mms.transaction.MessagingNotification; 25import com.android.mms.util.DraftCache; 26 27import com.google.android.mms.pdu.PduHeaders; 28import com.google.android.mms.util.SqliteWrapper; 29 30import android.app.AlertDialog; 31import android.app.ListActivity; 32import android.content.AsyncQueryHandler; 33import android.content.ContentResolver; 34import android.content.DialogInterface; 35import android.content.Intent; 36import android.content.DialogInterface.OnClickListener; 37import android.content.res.Configuration; 38import android.database.Cursor; 39import android.database.sqlite.SQLiteException; 40import android.os.Bundle; 41import android.provider.Contacts; 42import android.provider.Contacts.Intents.Insert; 43import android.provider.Telephony.Mms; 44import android.util.Log; 45import android.view.ContextMenu; 46import android.view.KeyEvent; 47import android.view.LayoutInflater; 48import android.view.Menu; 49import android.view.MenuItem; 50import android.view.View; 51import android.view.Window; 52import android.view.ContextMenu.ContextMenuInfo; 53import android.view.View.OnCreateContextMenuListener; 54import android.view.View.OnKeyListener; 55import android.widget.AdapterView; 56import android.widget.ListView; 57 58/** 59 * This activity provides a list view of existing conversations. 60 */ 61public class ConversationList extends ListActivity 62 implements DraftCache.OnDraftChangedListener { 63 private static final String TAG = "ConversationList"; 64 private static final boolean DEBUG = false; 65 private static final boolean LOCAL_LOGV = DEBUG; 66 67 private static final int THREAD_LIST_QUERY_TOKEN = 1701; 68 69 private static final int DELETE_CONVERSATION_TOKEN = 1801; 70 71 // IDs of the main menu items. 72 public static final int MENU_COMPOSE_NEW = 0; 73 public static final int MENU_SEARCH = 1; 74 public static final int MENU_DELETE_ALL = 3; 75 public static final int MENU_PREFERENCES = 4; 76 77 // IDs of the context menu items for the list of conversations. 78 public static final int MENU_DELETE = 0; 79 public static final int MENU_VIEW = 1; 80 public static final int MENU_VIEW_CONTACT = 2; 81 public static final int MENU_ADD_TO_CONTACTS = 3; 82 83 private ThreadListQueryHandler mQueryHandler; 84 private ConversationListAdapter mListAdapter; 85 private CharSequence mTitle; 86 87 @Override 88 protected void onCreate(Bundle savedInstanceState) { 89 super.onCreate(savedInstanceState); 90 91 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 92 setContentView(R.layout.conversation_list_screen); 93 94 mQueryHandler = new ThreadListQueryHandler(getContentResolver()); 95 96 ListView listView = getListView(); 97 LayoutInflater inflater = LayoutInflater.from(this); 98 ConversationHeaderView headerView = (ConversationHeaderView) 99 inflater.inflate(R.layout.conversation_header, listView, false); 100 headerView.bind(getString(R.string.new_message), 101 getString(R.string.create_new_message)); 102 listView.addHeaderView(headerView, null, true); 103 104 listView.setOnCreateContextMenuListener(mConvListOnCreateContextMenuListener); 105 listView.setOnKeyListener(mThreadListKeyListener); 106 107 initListAdapter(); 108 109 handleCreationIntent(getIntent()); 110 } 111 112 private void initListAdapter() { 113 mListAdapter = new ConversationListAdapter(this, null); 114 setListAdapter(mListAdapter); 115 } 116 117 @Override 118 protected void onNewIntent(Intent intent) { 119 // Handle intents that occur after the activity has already been created. 120 handleCreationIntent(intent); 121 } 122 123 protected void handleCreationIntent(Intent intent) { 124 // Handle intents that occur upon creation of the activity. 125 initNormalQueryArgs(); 126 } 127 128 @Override 129 protected void onResume() { 130 super.onResume(); 131 132 DraftCache.getInstance().addOnDraftChangedListener(this); 133 134 Conversation.cleanup(this); 135 136 // Make sure the draft cache is up to date. 137 DraftCache.getInstance().refresh(); 138 139 startAsyncQuery(); 140 141 Contact.invalidateCache(); 142 } 143 144 @Override 145 protected void onPause() { 146 super.onPause(); 147 148 DraftCache.getInstance().removeOnDraftChangedListener(this); 149 } 150 151 @Override 152 protected void onStop() { 153 super.onStop(); 154 155 mListAdapter.changeCursor(null); 156 } 157 158 public void onDraftChanged(long threadId, boolean hasDraft) { 159 // Run notifyDataSetChanged() on the main thread. 160 mQueryHandler.post(new Runnable() { 161 public void run() { 162 mListAdapter.notifyDataSetChanged(); 163 } 164 }); 165 } 166 167 private void initNormalQueryArgs() { 168 mTitle = getString(R.string.app_label); 169 } 170 171 private void startAsyncQuery() { 172 try { 173 setTitle(getString(R.string.refreshing)); 174 setProgressBarIndeterminateVisibility(true); 175 176 Conversation.startQueryForAll(mQueryHandler, THREAD_LIST_QUERY_TOKEN); 177 } catch (SQLiteException e) { 178 SqliteWrapper.checkSQLiteException(this, e); 179 } 180 } 181 182 @Override 183 public boolean onPrepareOptionsMenu(Menu menu) { 184 menu.clear(); 185 186 menu.add(0, MENU_COMPOSE_NEW, 0, R.string.menu_compose_new).setIcon( 187 com.android.internal.R.drawable.ic_menu_compose); 188 189 if (mListAdapter.getCount() > 0) { 190 menu.add(0, MENU_DELETE_ALL, 0, R.string.menu_delete_all).setIcon( 191 android.R.drawable.ic_menu_delete); 192 } 193 194 menu.add(0, MENU_PREFERENCES, 0, R.string.menu_preferences).setIcon( 195 android.R.drawable.ic_menu_preferences); 196 197 menu.add(0, MENU_SEARCH, 0, android.R.string.search_go). 198 setIcon(android.R.drawable.ic_menu_search). 199 setAlphabeticShortcut(android.app.SearchManager.MENU_KEY); 200 201 return true; 202 } 203 204 @Override 205 public boolean onSearchRequested() { 206 startSearch(null, false, null /*appData*/, false); 207 return true; 208 } 209 210 @Override 211 public boolean onOptionsItemSelected(MenuItem item) { 212 switch(item.getItemId()) { 213 case MENU_COMPOSE_NEW: 214 createNewMessage(); 215 break; 216 case MENU_SEARCH: 217 onSearchRequested(); 218 break; 219 case MENU_DELETE_ALL: 220 confirmDeleteDialog(new DeleteThreadListener(-1L), true); 221 break; 222 case MENU_PREFERENCES: { 223 Intent intent = new Intent(this, MessagingPreferenceActivity.class); 224 startActivityIfNeeded(intent, -1); 225 break; 226 } 227 default: 228 return true; 229 } 230 return false; 231 } 232 233 @Override 234 protected void onListItemClick(ListView l, View v, int position, long id) { 235 if (LOCAL_LOGV) { 236 Log.v(TAG, "onListItemClick: position=" + position + ", id=" + id); 237 } 238 239 if (position == 0) { 240 createNewMessage(); 241 } else if (v instanceof ConversationHeaderView) { 242 ConversationHeaderView headerView = (ConversationHeaderView) v; 243 ConversationHeader ch = headerView.getConversationHeader(); 244 openThread(ch.getThreadId()); 245 } 246 } 247 248 private void createNewMessage() { 249 Intent intent = new Intent(this, ComposeMessageActivity.class); 250 startActivity(intent); 251 } 252 253 private void openThread(long threadId) { 254 Intent intent = new Intent(this, ComposeMessageActivity.class); 255 intent.putExtra("thread_id", threadId); 256 startActivity(intent); 257 } 258 259 public static Intent createAddContactIntent(String address) { 260 // address must be a single recipient 261 Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 262 intent.setType(Contacts.People.CONTENT_ITEM_TYPE); 263 if (Mms.isEmailAddress(address)) { 264 intent.putExtra(Insert.EMAIL, address); 265 } else { 266 intent.putExtra(Insert.PHONE, address); 267 } 268 269 return intent; 270 } 271 272 private final OnCreateContextMenuListener mConvListOnCreateContextMenuListener = 273 new OnCreateContextMenuListener() { 274 public void onCreateContextMenu(ContextMenu menu, View v, 275 ContextMenuInfo menuInfo) { 276 Cursor cursor = mListAdapter.getCursor(); 277 Conversation conv = Conversation.from(ConversationList.this, cursor); 278 ContactList recipients = conv.getRecipients(); 279 menu.setHeaderTitle(recipients.formatNames(",")); 280 281 AdapterView.AdapterContextMenuInfo info = 282 (AdapterView.AdapterContextMenuInfo) menuInfo; 283 if (info.position > 0) { 284 menu.add(0, MENU_VIEW, 0, R.string.menu_view); 285 286 // Only show if there's a single recipient 287 if (recipients.size() == 1) { 288 // do we have this recipient in contacts? 289 if (recipients.get(0).existsInDatabase()) { 290 menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact); 291 } else { 292 menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts); 293 } 294 } 295 menu.add(0, MENU_DELETE, 0, R.string.menu_delete); 296 } 297 } 298 }; 299 300 @Override 301 public boolean onContextItemSelected(MenuItem item) { 302 Cursor cursor = mListAdapter.getCursor(); 303 Conversation conv = Conversation.from(ConversationList.this, cursor); 304 long threadId = conv.getThreadId(); 305 switch (item.getItemId()) { 306 case MENU_DELETE: { 307 DeleteThreadListener l = new DeleteThreadListener(threadId); 308 confirmDeleteDialog(l, false); 309 break; 310 } 311 case MENU_VIEW: { 312 openThread(threadId); 313 break; 314 } 315 case MENU_VIEW_CONTACT: { 316 Contact contact = conv.getRecipients().get(0); 317 Intent intent = new Intent(Intent.ACTION_VIEW, contact.getUri()); 318 startActivity(intent); 319 break; 320 } 321 case MENU_ADD_TO_CONTACTS: { 322 String address = conv.getRecipients().get(0).getNumber(); 323 startActivity(createAddContactIntent(address)); 324 break; 325 } 326 default: 327 break; 328 } 329 330 return super.onContextItemSelected(item); 331 } 332 333 public void onConfigurationChanged(Configuration newConfig) { 334 // We override this method to avoid restarting the entire 335 // activity when the keyboard is opened (declared in 336 // AndroidManifest.xml). Because the only translatable text 337 // in this activity is "New Message", which has the full width 338 // of phone to work with, localization shouldn't be a problem: 339 // no abbreviated alternate words should be needed even in 340 // 'wide' languages like German or Russian. 341 342 super.onConfigurationChanged(newConfig); 343 if (DEBUG) Log.v(TAG, "onConfigurationChanged: " + newConfig); 344 } 345 346 private void confirmDeleteDialog(OnClickListener listener, boolean deleteAll) { 347 AlertDialog.Builder builder = new AlertDialog.Builder(this); 348 builder.setTitle(R.string.confirm_dialog_title); 349 builder.setIcon(android.R.drawable.ic_dialog_alert); 350 builder.setCancelable(true); 351 builder.setPositiveButton(R.string.yes, listener); 352 builder.setNegativeButton(R.string.no, null); 353 builder.setMessage(deleteAll 354 ? R.string.confirm_delete_all_conversations 355 : R.string.confirm_delete_conversation); 356 357 builder.show(); 358 } 359 360 private final OnKeyListener mThreadListKeyListener = new OnKeyListener() { 361 public boolean onKey(View v, int keyCode, KeyEvent event) { 362 if (event.getAction() == KeyEvent.ACTION_DOWN) { 363 switch (keyCode) { 364 case KeyEvent.KEYCODE_DEL: { 365 long id = getListView().getSelectedItemId(); 366 if (id > 0) { 367 DeleteThreadListener l = new DeleteThreadListener( 368 id); 369 confirmDeleteDialog(l, false); 370 } 371 return true; 372 } 373 } 374 } 375 return false; 376 } 377 }; 378 379 private class DeleteThreadListener implements OnClickListener { 380 private final long mThreadId; 381 382 public DeleteThreadListener(long threadId) { 383 mThreadId = threadId; 384 } 385 386 public void onClick(DialogInterface dialog, int whichButton) { 387 MessageUtils.handleReadReport(ConversationList.this, mThreadId, 388 PduHeaders.READ_STATUS__DELETED_WITHOUT_BEING_READ, new Runnable() { 389 public void run() { 390 int token = DELETE_CONVERSATION_TOKEN; 391 if (mThreadId == -1) { 392 Conversation.startDeleteAll(mQueryHandler, token); 393 } else { 394 Conversation.startDelete(mQueryHandler, token, mThreadId); 395 } 396 } 397 }); 398 } 399 } 400 401 private final class ThreadListQueryHandler extends AsyncQueryHandler { 402 public ThreadListQueryHandler(ContentResolver contentResolver) { 403 super(contentResolver); 404 } 405 406 @Override 407 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 408 switch (token) { 409 case THREAD_LIST_QUERY_TOKEN: 410 mListAdapter.changeCursor(cursor); 411 setTitle(mTitle); 412 setProgressBarIndeterminateVisibility(false); 413 break; 414 default: 415 Log.e(TAG, "onQueryComplete called with unknown token " + token); 416 } 417 } 418 419 @Override 420 protected void onDeleteComplete(int token, Object cookie, int result) { 421 switch (token) { 422 case DELETE_CONVERSATION_TOKEN: 423 // Make sure the conversation cache reflects the threads in the DB. 424 Conversation.init(ConversationList.this); 425 426 // Update the notification for new messages since they 427 // may be deleted. 428 MessagingNotification.updateNewMessageIndicator(ConversationList.this); 429 // Update the notification for failed messages since they 430 // may be deleted. 431 MessagingNotification.updateSendFailedNotification(ConversationList.this); 432 433 // Make sure the list reflects the delete 434 startAsyncQuery(); 435 436 onContentChanged(); 437 break; 438 } 439 } 440 } 441} 442