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