MessageListAdapter.java revision f114492537753bc68640d4a0d403861387296bcb
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.mms.MmsException; 22 23import android.content.AsyncQueryHandler; 24import android.content.ContentResolver; 25import android.content.ContentUris; 26import android.content.Context; 27import android.database.Cursor; 28import android.graphics.Bitmap; 29import android.graphics.BitmapFactory; 30import android.graphics.drawable.BitmapDrawable; 31import android.graphics.drawable.Drawable; 32import android.net.Uri; 33import android.os.Handler; 34import android.provider.BaseColumns; 35import android.provider.ContactsContract.Contacts; 36import android.provider.ContactsContract.Data; 37import android.provider.ContactsContract.PhoneLookup; 38import android.provider.ContactsContract.RawContacts; 39import android.provider.ContactsContract.StatusUpdates; 40import android.provider.ContactsContract.CommonDataKinds.Email; 41import android.provider.ContactsContract.CommonDataKinds.Photo; 42import com.android.mms.telephony.TelephonyProvider.Mms; 43import com.android.mms.telephony.TelephonyProvider.MmsSms; 44import com.android.mms.telephony.TelephonyProvider.Sms; 45import com.android.mms.telephony.TelephonyProvider.MmsSms.PendingMessages; 46import com.android.mms.telephony.TelephonyProvider.Sms.Conversations; 47import android.text.TextUtils; 48import android.text.format.DateUtils; 49import android.util.Config; 50import android.util.Log; 51import android.view.LayoutInflater; 52import android.view.View; 53import android.view.ViewGroup; 54import android.widget.CursorAdapter; 55import android.widget.ListView; 56 57import java.util.HashMap; 58import java.util.HashSet; 59import java.util.LinkedHashMap; 60import java.util.Map; 61import java.util.regex.Pattern; 62 63/** 64 * The back-end data adapter of a message list. 65 */ 66public class MessageListAdapter extends CursorAdapter { 67 private static final String TAG = "MessageListAdapter"; 68 private static final boolean DEBUG = false; 69 private static final boolean LOCAL_LOGV = Config.LOGV && DEBUG; 70 71 static final String[] PROJECTION = new String[] { 72 // TODO: should move this symbol into com.android.mms.telephony.Telephony. 73 MmsSms.TYPE_DISCRIMINATOR_COLUMN, 74 BaseColumns._ID, 75 Conversations.THREAD_ID, 76 // For SMS 77 Sms.ADDRESS, 78 Sms.BODY, 79 Sms.DATE, 80 Sms.READ, 81 Sms.TYPE, 82 Sms.STATUS, 83 Sms.LOCKED, 84 Sms.ERROR_CODE, 85 // For MMS 86 Mms.SUBJECT, 87 Mms.SUBJECT_CHARSET, 88 Mms.DATE, 89 Mms.READ, 90 Mms.MESSAGE_TYPE, 91 Mms.MESSAGE_BOX, 92 Mms.DELIVERY_REPORT, 93 Mms.READ_REPORT, 94 PendingMessages.ERROR_TYPE, 95 Mms.LOCKED 96 }; 97 98 // The indexes of the default columns which must be consistent 99 // with above PROJECTION. 100 static final int COLUMN_MSG_TYPE = 0; 101 static final int COLUMN_ID = 1; 102 static final int COLUMN_THREAD_ID = 2; 103 static final int COLUMN_SMS_ADDRESS = 3; 104 static final int COLUMN_SMS_BODY = 4; 105 static final int COLUMN_SMS_DATE = 5; 106 static final int COLUMN_SMS_READ = 6; 107 static final int COLUMN_SMS_TYPE = 7; 108 static final int COLUMN_SMS_STATUS = 8; 109 static final int COLUMN_SMS_LOCKED = 9; 110 static final int COLUMN_SMS_ERROR_CODE = 10; 111 static final int COLUMN_MMS_SUBJECT = 11; 112 static final int COLUMN_MMS_SUBJECT_CHARSET = 12; 113 static final int COLUMN_MMS_DATE = 13; 114 static final int COLUMN_MMS_READ = 14; 115 static final int COLUMN_MMS_MESSAGE_TYPE = 15; 116 static final int COLUMN_MMS_MESSAGE_BOX = 16; 117 static final int COLUMN_MMS_DELIVERY_REPORT = 17; 118 static final int COLUMN_MMS_READ_REPORT = 18; 119 static final int COLUMN_MMS_ERROR_TYPE = 19; 120 static final int COLUMN_MMS_LOCKED = 20; 121 122 private static final int CACHE_SIZE = 50; 123 124 protected LayoutInflater mInflater; 125 private final ListView mListView; 126 private final LinkedHashMap<Long, MessageItem> mMessageItemCache; 127 private final ColumnsMap mColumnsMap; 128 private OnDataSetChangedListener mOnDataSetChangedListener; 129 private Handler mMsgListItemHandler; 130 private Pattern mHighlight; 131 private Context mContext; 132 133 public MessageListAdapter( 134 Context context, Cursor c, ListView listView, 135 boolean useDefaultColumnsMap, Pattern highlight) { 136 super(context, c, false /* auto-requery */); 137 mContext = context; 138 mHighlight = highlight; 139 140 mInflater = (LayoutInflater) context.getSystemService( 141 Context.LAYOUT_INFLATER_SERVICE); 142 mListView = listView; 143 mMessageItemCache = new LinkedHashMap<Long, MessageItem>( 144 10, 1.0f, true) { 145 @Override 146 protected boolean removeEldestEntry(Map.Entry eldest) { 147 return size() > CACHE_SIZE; 148 } 149 }; 150 151 if (useDefaultColumnsMap) { 152 mColumnsMap = new ColumnsMap(); 153 } else { 154 mColumnsMap = new ColumnsMap(c); 155 } 156 157 mAvatarCache = new AvatarCache(); 158 } 159 160 @Override 161 public void bindView(View view, Context context, Cursor cursor) { 162 if (view instanceof MessageListItem) { 163 String type = cursor.getString(mColumnsMap.mColumnMsgType); 164 long msgId = cursor.getLong(mColumnsMap.mColumnMsgId); 165 166 MessageItem msgItem = getCachedMessageItem(type, msgId, cursor); 167 if (msgItem != null) { 168 ((MessageListItem) view).bind(mAvatarCache, msgItem); 169 ((MessageListItem) view).setMsgListItemHandler(mMsgListItemHandler); 170 } 171 } 172 } 173 174 public interface OnDataSetChangedListener { 175 void onDataSetChanged(MessageListAdapter adapter); 176 void onContentChanged(MessageListAdapter adapter); 177 } 178 179 public void setOnDataSetChangedListener(OnDataSetChangedListener l) { 180 mOnDataSetChangedListener = l; 181 } 182 183 public void setMsgListItemHandler(Handler handler) { 184 mMsgListItemHandler = handler; 185 } 186 187 @Override 188 public void notifyDataSetChanged() { 189 super.notifyDataSetChanged(); 190 if (LOCAL_LOGV) { 191 Log.v(TAG, "MessageListAdapter.notifyDataSetChanged()."); 192 } 193 194 mListView.setSelection(mListView.getCount()); 195 mMessageItemCache.clear(); 196 197 if (mOnDataSetChangedListener != null) { 198 mOnDataSetChangedListener.onDataSetChanged(this); 199 } 200 } 201 202 @Override 203 protected void onContentChanged() { 204 if (getCursor() != null && !getCursor().isClosed()) { 205 if (mOnDataSetChangedListener != null) { 206 mOnDataSetChangedListener.onContentChanged(this); 207 } 208 } 209 } 210 211 @Override 212 public View newView(Context context, Cursor cursor, ViewGroup parent) { 213 return mInflater.inflate(R.layout.message_list_item, parent, false); 214 } 215 216 public MessageItem getCachedMessageItem(String type, long msgId, Cursor c) { 217 MessageItem item = mMessageItemCache.get(getKey(type, msgId)); 218 if (item == null) { 219 try { 220 item = new MessageItem(mContext, type, c, mColumnsMap, mHighlight); 221 mMessageItemCache.put(getKey(item.mType, item.mMsgId), item); 222 } catch (MmsException e) { 223 Log.e(TAG, e.getMessage()); 224 } 225 } 226 return item; 227 } 228 229 private static long getKey(String type, long id) { 230 if (type.equals("mms")) { 231 return -id; 232 } else { 233 return id; 234 } 235 } 236 237 public static class ColumnsMap { 238 public int mColumnMsgType; 239 public int mColumnMsgId; 240 public int mColumnSmsAddress; 241 public int mColumnSmsBody; 242 public int mColumnSmsDate; 243 public int mColumnSmsRead; 244 public int mColumnSmsType; 245 public int mColumnSmsStatus; 246 public int mColumnSmsLocked; 247 public int mColumnSmsErrorCode; 248 public int mColumnMmsSubject; 249 public int mColumnMmsSubjectCharset; 250 public int mColumnMmsDate; 251 public int mColumnMmsRead; 252 public int mColumnMmsMessageType; 253 public int mColumnMmsMessageBox; 254 public int mColumnMmsDeliveryReport; 255 public int mColumnMmsReadReport; 256 public int mColumnMmsErrorType; 257 public int mColumnMmsLocked; 258 259 public ColumnsMap() { 260 mColumnMsgType = COLUMN_MSG_TYPE; 261 mColumnMsgId = COLUMN_ID; 262 mColumnSmsAddress = COLUMN_SMS_ADDRESS; 263 mColumnSmsBody = COLUMN_SMS_BODY; 264 mColumnSmsDate = COLUMN_SMS_DATE; 265 mColumnSmsType = COLUMN_SMS_TYPE; 266 mColumnSmsStatus = COLUMN_SMS_STATUS; 267 mColumnSmsLocked = COLUMN_SMS_LOCKED; 268 mColumnSmsErrorCode = COLUMN_SMS_ERROR_CODE; 269 mColumnMmsSubject = COLUMN_MMS_SUBJECT; 270 mColumnMmsSubjectCharset = COLUMN_MMS_SUBJECT_CHARSET; 271 mColumnMmsMessageType = COLUMN_MMS_MESSAGE_TYPE; 272 mColumnMmsMessageBox = COLUMN_MMS_MESSAGE_BOX; 273 mColumnMmsDeliveryReport = COLUMN_MMS_DELIVERY_REPORT; 274 mColumnMmsReadReport = COLUMN_MMS_READ_REPORT; 275 mColumnMmsErrorType = COLUMN_MMS_ERROR_TYPE; 276 mColumnMmsLocked = COLUMN_MMS_LOCKED; 277 } 278 279 public ColumnsMap(Cursor cursor) { 280 // Ignore all 'not found' exceptions since the custom columns 281 // may be just a subset of the default columns. 282 try { 283 mColumnMsgType = cursor.getColumnIndexOrThrow( 284 MmsSms.TYPE_DISCRIMINATOR_COLUMN); 285 } catch (IllegalArgumentException e) { 286 Log.w("colsMap", e.getMessage()); 287 } 288 289 try { 290 mColumnMsgId = cursor.getColumnIndexOrThrow(BaseColumns._ID); 291 } catch (IllegalArgumentException e) { 292 Log.w("colsMap", e.getMessage()); 293 } 294 295 try { 296 mColumnSmsAddress = cursor.getColumnIndexOrThrow(Sms.ADDRESS); 297 } catch (IllegalArgumentException e) { 298 Log.w("colsMap", e.getMessage()); 299 } 300 301 try { 302 mColumnSmsBody = cursor.getColumnIndexOrThrow(Sms.BODY); 303 } catch (IllegalArgumentException e) { 304 Log.w("colsMap", e.getMessage()); 305 } 306 307 try { 308 mColumnSmsDate = cursor.getColumnIndexOrThrow(Sms.DATE); 309 } catch (IllegalArgumentException e) { 310 Log.w("colsMap", e.getMessage()); 311 } 312 313 try { 314 mColumnSmsType = cursor.getColumnIndexOrThrow(Sms.TYPE); 315 } catch (IllegalArgumentException e) { 316 Log.w("colsMap", e.getMessage()); 317 } 318 319 try { 320 mColumnSmsStatus = cursor.getColumnIndexOrThrow(Sms.STATUS); 321 } catch (IllegalArgumentException e) { 322 Log.w("colsMap", e.getMessage()); 323 } 324 325 try { 326 mColumnSmsLocked = cursor.getColumnIndexOrThrow(Sms.LOCKED); 327 } catch (IllegalArgumentException e) { 328 Log.w("colsMap", e.getMessage()); 329 } 330 331 try { 332 mColumnSmsErrorCode = cursor.getColumnIndexOrThrow(Sms.ERROR_CODE); 333 } catch (IllegalArgumentException e) { 334 Log.w("colsMap", e.getMessage()); 335 } 336 337 try { 338 mColumnMmsSubject = cursor.getColumnIndexOrThrow(Mms.SUBJECT); 339 } catch (IllegalArgumentException e) { 340 Log.w("colsMap", e.getMessage()); 341 } 342 343 try { 344 mColumnMmsSubjectCharset = cursor.getColumnIndexOrThrow(Mms.SUBJECT_CHARSET); 345 } catch (IllegalArgumentException e) { 346 Log.w("colsMap", e.getMessage()); 347 } 348 349 try { 350 mColumnMmsMessageType = cursor.getColumnIndexOrThrow(Mms.MESSAGE_TYPE); 351 } catch (IllegalArgumentException e) { 352 Log.w("colsMap", e.getMessage()); 353 } 354 355 try { 356 mColumnMmsMessageBox = cursor.getColumnIndexOrThrow(Mms.MESSAGE_BOX); 357 } catch (IllegalArgumentException e) { 358 Log.w("colsMap", e.getMessage()); 359 } 360 361 try { 362 mColumnMmsDeliveryReport = cursor.getColumnIndexOrThrow(Mms.DELIVERY_REPORT); 363 } catch (IllegalArgumentException e) { 364 Log.w("colsMap", e.getMessage()); 365 } 366 367 try { 368 mColumnMmsReadReport = cursor.getColumnIndexOrThrow(Mms.READ_REPORT); 369 } catch (IllegalArgumentException e) { 370 Log.w("colsMap", e.getMessage()); 371 } 372 373 try { 374 mColumnMmsErrorType = cursor.getColumnIndexOrThrow(PendingMessages.ERROR_TYPE); 375 } catch (IllegalArgumentException e) { 376 Log.w("colsMap", e.getMessage()); 377 } 378 379 try { 380 mColumnMmsLocked = cursor.getColumnIndexOrThrow(Mms.LOCKED); 381 } catch (IllegalArgumentException e) { 382 Log.w("colsMap", e.getMessage()); 383 } 384 } 385 } 386 387 private AvatarCache mAvatarCache; 388 389 /* 390 * Track avatars for each of the members of in the group chat. 391 */ 392 class AvatarCache { 393 private static final int TOKEN_PHONE_LOOKUP = 101; 394 private static final int TOKEN_EMAIL_LOOKUP = 102; 395 private static final int TOKEN_CONTACT_INFO = 201; 396 private static final int TOKEN_PHOTO_DATA = 301; 397 398 //Projection used for the summary info in the header. 399 private final String[] COLUMNS = new String[] { 400 Contacts._ID, 401 Contacts.PHOTO_ID, 402 // Other fields which we might want/need in the future (for example) 403// Contacts.LOOKUP_KEY, 404// Contacts.DISPLAY_NAME, 405// Contacts.STARRED, 406// Contacts.CONTACT_PRESENCE, 407// Contacts.CONTACT_STATUS, 408// Contacts.CONTACT_STATUS_TIMESTAMP, 409// Contacts.CONTACT_STATUS_RES_PACKAGE, 410// Contacts.CONTACT_STATUS_LABEL, 411 }; 412 private final int PHOTO_ID = 1; 413 414 private final String[] PHONE_LOOKUP_PROJECTION = new String[] { 415 PhoneLookup._ID, 416 PhoneLookup.LOOKUP_KEY, 417 }; 418 private static final int PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0; 419 private static final int PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1; 420 421 private final String[] EMAIL_LOOKUP_PROJECTION = new String[] { 422 RawContacts.CONTACT_ID, 423 Contacts.LOOKUP_KEY, 424 }; 425 private static final int EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0; 426 private static final int EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1; 427 428 429 /* 430 * Map from mAddress to a blob of data which contains the contact id 431 * and the avatar. 432 */ 433 HashMap<String, ContactData> mImageCache = new HashMap<String, ContactData>(); 434 435 public class ContactData { 436 private String mAddress; 437 private long mContactId; 438 private Uri mContactUri; 439 private Drawable mPhoto; 440 441 ContactData(String address) { 442 mAddress = address; 443 } 444 445 public Drawable getAvatar() { 446 return mPhoto; 447 } 448 449 public Uri getContactUri() { 450 return mContactUri; 451 } 452 453 private boolean startInitialQuery() { 454 if (Mms.isPhoneNumber(mAddress)) { 455 mQueryHandler.startQuery( 456 TOKEN_PHONE_LOOKUP, 457 this, 458 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(mAddress)), 459 PHONE_LOOKUP_PROJECTION, 460 null, 461 null, 462 null); 463 return true; 464 } else if (Mms.isEmailAddress(mAddress)) { 465 mQueryHandler.startQuery( 466 TOKEN_EMAIL_LOOKUP, 467 this, 468 Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mAddress)), 469 EMAIL_LOOKUP_PROJECTION, 470 null, 471 null, 472 null); 473 return true; 474 } else { 475 return false; 476 } 477 } 478 /* 479 * Once we have the photo data load it into a drawable. 480 */ 481 private boolean onPhotoDataLoaded(Cursor c) { 482 if (c == null || !c.moveToFirst()) return false; 483 484 try { 485 byte[] photoData = c.getBlob(0); 486 Bitmap b = BitmapFactory.decodeByteArray(photoData, 0, photoData.length, null); 487 mPhoto = new BitmapDrawable(mContext.getResources(), b); 488 return true; 489 } catch (Exception ex) { 490 return false; 491 } 492 } 493 494 /* 495 * Once we have the contact info loaded take the photo id and query 496 * for the photo data. 497 */ 498 private boolean onContactInfoLoaded(Cursor c) { 499 if (c == null || !c.moveToFirst()) return false; 500 501 mContactId = c.getLong(PHOTO_ID); 502 mContactUri = ContentUris.withAppendedId(Data.CONTENT_URI, mContactId); 503 mQueryHandler.startQuery( 504 TOKEN_PHOTO_DATA, 505 this, 506 mContactUri, 507 new String[] { Photo.PHOTO }, 508 null, 509 null, 510 null); 511 512 return true; 513 } 514 515 /* 516 * Once we have the contact id loaded start the query for the 517 * contact information (which will give us the photo id). 518 */ 519 private boolean onContactIdLoaded(Cursor c, int contactIdColumn, int lookupKeyColumn) { 520 if (c == null || !c.moveToFirst()) return false; 521 522 mContactId = c.getLong(contactIdColumn); 523 String lookupKey = c.getString(lookupKeyColumn); 524 Uri contactUri = Contacts.getLookupUri(mContactId, lookupKey); 525 mQueryHandler.startQuery( 526 TOKEN_CONTACT_INFO, 527 this, 528 contactUri, 529 COLUMNS, 530 null, 531 null, 532 null); 533 return true; 534 } 535 536 /* 537 * If for whatever reason we can't get the photo load teh 538 * default avatar. NOTE that fasttrack tries to get fancy 539 * with various random images (upside down, etc.) we're not 540 * doing that here. 541 */ 542 private void loadDefaultAvatar() { 543 if (mDefaultAvatarDrawable == null) { 544 Bitmap b = BitmapFactory.decodeResource(mContext.getResources(), 545 R.drawable.ic_contact_picture); 546 mDefaultAvatarDrawable = new BitmapDrawable(mContext.getResources(), b); 547 } 548 mPhoto = mDefaultAvatarDrawable; 549 } 550 551 }; 552 553 Drawable mDefaultAvatarDrawable = null; 554 AsyncQueryHandler mQueryHandler = new AsyncQueryHandler(mContext.getContentResolver()) { 555 @Override 556 protected void onQueryComplete(int token, Object cookieObject, Cursor cursor) { 557 super.onQueryComplete(token, cookieObject, cursor); 558 559 ContactData cookie = (ContactData) cookieObject; 560 switch (token) { 561 case TOKEN_PHONE_LOOKUP: { 562 if (!cookie.onContactIdLoaded( 563 cursor, 564 PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX, 565 PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX)) { 566 cookie.loadDefaultAvatar(); 567 } 568 break; 569 } 570 case TOKEN_EMAIL_LOOKUP: { 571 if (!cookie.onContactIdLoaded( 572 cursor, 573 EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX, 574 EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX)) { 575 cookie.loadDefaultAvatar(); 576 } 577 break; 578 } 579 case TOKEN_CONTACT_INFO: { 580 if (!cookie.onContactInfoLoaded(cursor)) { 581 cookie.loadDefaultAvatar(); 582 } 583 break; 584 } 585 case TOKEN_PHOTO_DATA: { 586 if (!cookie.onPhotoDataLoaded(cursor)) { 587 cookie.loadDefaultAvatar(); 588 } else { 589 MessageListAdapter.this.notifyDataSetChanged(); 590 } 591 break; 592 } 593 default: 594 break; 595 } 596 } 597 }; 598 599 public ContactData get(final String address) { 600 if (mImageCache.containsKey(address)) { 601 return mImageCache.get(address); 602 } else { 603 // Create the ContactData object and put it into the hashtable 604 // so that any subsequent requests for this same avatar do not kick 605 // off another query. 606 ContactData cookie = new ContactData(address); 607 mImageCache.put(address, cookie); 608 if (!cookie.startInitialQuery()) { 609 cookie.loadDefaultAvatar(); 610 } 611 return cookie; 612 } 613 } 614 615 public AvatarCache() { 616 } 617 }; 618 619 620} 621