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 java.util.regex.Pattern; 21 22 import android.content.Context; 23 import android.database.Cursor; 24 import android.os.Handler; 25 import android.provider.BaseColumns; 26 import android.provider.Telephony.Mms; 27 import android.provider.Telephony.MmsSms; 28 import android.provider.Telephony.MmsSms.PendingMessages; 29 import android.provider.Telephony.Sms; 30 import android.provider.Telephony.Sms.Conversations; 31 import android.provider.Telephony.TextBasedSmsColumns; 32 import android.util.Log; 33 import android.util.LruCache; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.widget.AbsListView; 38 import android.widget.CursorAdapter; 39 import android.widget.ListView; 40 41 import com.android.mms.R; 42 import com.google.android.mms.MmsException; 43 44 /** 45 * The back-end data adapter of a message list. 46 */ 47 public class MessageListAdapter extends CursorAdapter { 48 private static final String TAG = "MessageListAdapter"; 49 private static final boolean LOCAL_LOGV = false; 50 51 static final String[] PROJECTION = new String[] { 52 // TODO: should move this symbol into com.android.mms.telephony.Telephony. 53 MmsSms.TYPE_DISCRIMINATOR_COLUMN, 54 BaseColumns._ID, 55 Conversations.THREAD_ID, 56 // For SMS 57 Sms.ADDRESS, 58 Sms.BODY, 59 Sms.DATE, 60 Sms.DATE_SENT, 61 Sms.READ, 62 Sms.TYPE, 63 Sms.STATUS, 64 Sms.LOCKED, 65 Sms.ERROR_CODE, 66 // For MMS 67 Mms.SUBJECT, 68 Mms.SUBJECT_CHARSET, 69 Mms.DATE, 70 Mms.DATE_SENT, 71 Mms.READ, 72 Mms.MESSAGE_TYPE, 73 Mms.MESSAGE_BOX, 74 Mms.DELIVERY_REPORT, 75 Mms.READ_REPORT, 76 PendingMessages.ERROR_TYPE, 77 Mms.LOCKED, 78 Mms.STATUS, 79 Mms.TEXT_ONLY 80 }; 81 82 // The indexes of the default columns which must be consistent 83 // with above PROJECTION. 84 static final int COLUMN_MSG_TYPE = 0; 85 static final int COLUMN_ID = 1; 86 static final int COLUMN_THREAD_ID = 2; 87 static final int COLUMN_SMS_ADDRESS = 3; 88 static final int COLUMN_SMS_BODY = 4; 89 static final int COLUMN_SMS_DATE = 5; 90 static final int COLUMN_SMS_DATE_SENT = 6; 91 static final int COLUMN_SMS_READ = 7; 92 static final int COLUMN_SMS_TYPE = 8; 93 static final int COLUMN_SMS_STATUS = 9; 94 static final int COLUMN_SMS_LOCKED = 10; 95 static final int COLUMN_SMS_ERROR_CODE = 11; 96 static final int COLUMN_MMS_SUBJECT = 12; 97 static final int COLUMN_MMS_SUBJECT_CHARSET = 13; 98 static final int COLUMN_MMS_DATE = 14; 99 static final int COLUMN_MMS_DATE_SENT = 15; 100 static final int COLUMN_MMS_READ = 16; 101 static final int COLUMN_MMS_MESSAGE_TYPE = 17; 102 static final int COLUMN_MMS_MESSAGE_BOX = 18; 103 static final int COLUMN_MMS_DELIVERY_REPORT = 19; 104 static final int COLUMN_MMS_READ_REPORT = 20; 105 static final int COLUMN_MMS_ERROR_TYPE = 21; 106 static final int COLUMN_MMS_LOCKED = 22; 107 static final int COLUMN_MMS_STATUS = 23; 108 static final int COLUMN_MMS_TEXT_ONLY = 24; 109 110 private static final int CACHE_SIZE = 50; 111 112 public static final int INCOMING_ITEM_TYPE_SMS = 0; 113 public static final int OUTGOING_ITEM_TYPE_SMS = 1; 114 public static final int INCOMING_ITEM_TYPE_MMS = 2; 115 public static final int OUTGOING_ITEM_TYPE_MMS = 3; 116 117 protected LayoutInflater mInflater; 118 private final MessageItemCache mMessageItemCache; 119 private final ColumnsMap mColumnsMap; 120 private OnDataSetChangedListener mOnDataSetChangedListener; 121 private Handler mMsgListItemHandler; 122 private Pattern mHighlight; 123 private Context mContext; 124 private boolean mIsGroupConversation; 125 126 public MessageListAdapter( 127 Context context, Cursor c, ListView listView, 128 boolean useDefaultColumnsMap, Pattern highlight) { 129 super(context, c, FLAG_REGISTER_CONTENT_OBSERVER); 130 mContext = context; 131 mHighlight = highlight; 132 133 mInflater = (LayoutInflater) context.getSystemService( 134 Context.LAYOUT_INFLATER_SERVICE); 135 mMessageItemCache = new MessageItemCache(CACHE_SIZE); 136 137 if (useDefaultColumnsMap) { 138 mColumnsMap = new ColumnsMap(); 139 } else { 140 mColumnsMap = new ColumnsMap(c); 141 } 142 143 listView.setRecyclerListener(new AbsListView.RecyclerListener() { 144 @Override 145 public void onMovedToScrapHeap(View view) { 146 if (view instanceof MessageListItem) { 147 MessageListItem mli = (MessageListItem) view; 148 // Clear references to resources 149 mli.unbind(); 150 } 151 } 152 }); 153 } 154 155 @Override 156 public void bindView(View view, Context context, Cursor cursor) { 157 if (view instanceof MessageListItem) { 158 String type = cursor.getString(mColumnsMap.mColumnMsgType); 159 long msgId = cursor.getLong(mColumnsMap.mColumnMsgId); 160 161 MessageItem msgItem = getCachedMessageItem(type, msgId, cursor); 162 if (msgItem != null) { 163 MessageListItem mli = (MessageListItem) view; 164 int position = cursor.getPosition(); 165 mli.bind(msgItem, mIsGroupConversation, position); 166 mli.setMsgListItemHandler(mMsgListItemHandler); 167 } 168 } 169 } 170 171 public interface OnDataSetChangedListener { 172 void onDataSetChanged(MessageListAdapter adapter); 173 void onContentChanged(MessageListAdapter adapter); 174 } 175 176 public void setOnDataSetChangedListener(OnDataSetChangedListener l) { 177 mOnDataSetChangedListener = l; 178 } 179 180 public void setMsgListItemHandler(Handler handler) { 181 mMsgListItemHandler = handler; 182 } 183 184 public void setIsGroupConversation(boolean isGroup) { 185 mIsGroupConversation = isGroup; 186 } 187 188 public void cancelBackgroundLoading() { 189 mMessageItemCache.evictAll(); // causes entryRemoved to be called for each MessageItem 190 // in the cache which causes us to cancel loading of 191 // background pdu's and images. 192 } 193 194 @Override 195 public void notifyDataSetChanged() { 196 super.notifyDataSetChanged(); 197 if (LOCAL_LOGV) { 198 Log.v(TAG, "MessageListAdapter.notifyDataSetChanged()."); 199 } 200 201 mMessageItemCache.evictAll(); 202 203 if (mOnDataSetChangedListener != null) { 204 mOnDataSetChangedListener.onDataSetChanged(this); 205 } 206 } 207 208 @Override 209 protected void onContentChanged() { 210 if (getCursor() != null && !getCursor().isClosed()) { 211 if (mOnDataSetChangedListener != null) { 212 mOnDataSetChangedListener.onContentChanged(this); 213 } 214 } 215 } 216 217 @Override 218 public View newView(Context context, Cursor cursor, ViewGroup parent) { 219 int boxType = getItemViewType(cursor); 220 View view = mInflater.inflate((boxType == INCOMING_ITEM_TYPE_SMS || 221 boxType == INCOMING_ITEM_TYPE_MMS) ? 222 R.layout.message_list_item_recv : R.layout.message_list_item_send, 223 parent, false); 224 if (boxType == INCOMING_ITEM_TYPE_MMS || boxType == OUTGOING_ITEM_TYPE_MMS) { 225 // We've got an mms item, pre-inflate the mms portion of the view 226 view.findViewById(R.id.mms_layout_view_stub).setVisibility(View.VISIBLE); 227 } 228 return view; 229 } 230 231 public MessageItem getCachedMessageItem(String type, long msgId, Cursor c) { 232 MessageItem item = mMessageItemCache.get(getKey(type, msgId)); 233 if (item == null && c != null && isCursorValid(c)) { 234 try { 235 item = new MessageItem(mContext, type, c, mColumnsMap, mHighlight); 236 mMessageItemCache.put(getKey(item.mType, item.mMsgId), item); 237 } catch (MmsException e) { 238 Log.e(TAG, "getCachedMessageItem: ", e); 239 } 240 } 241 return item; 242 } 243 244 private boolean isCursorValid(Cursor cursor) { 245 // Check whether the cursor is valid or not. 246 if (cursor == null || cursor.isClosed() || cursor.isBeforeFirst() || cursor.isAfterLast()) { 247 return false; 248 } 249 return true; 250 } 251 252 private static long getKey(String type, long id) { 253 if (type.equals("mms")) { 254 return -id; 255 } else { 256 return id; 257 } 258 } 259 260 @Override 261 public boolean areAllItemsEnabled() { 262 return true; 263 } 264 265 /* MessageListAdapter says that it contains four types of views. Really, it just contains 266 * a single type, a MessageListItem. Depending upon whether the message is an incoming or 267 * outgoing message, the avatar and text and other items are laid out either left or right 268 * justified. That works fine for everything but the message text. When views are recycled, 269 * there's a greater than zero chance that the right-justified text on outgoing messages 270 * will remain left-justified. The best solution at this point is to tell the adapter we've 271 * got two different types of views. That way we won't recycle views between the two types. 272 * @see android.widget.BaseAdapter#getViewTypeCount() 273 */ 274 @Override 275 public int getViewTypeCount() { 276 return 4; // Incoming and outgoing messages, both sms and mms 277 } 278 279 @Override 280 public int getItemViewType(int position) { 281 Cursor cursor = (Cursor)getItem(position); 282 return getItemViewType(cursor); 283 } 284 285 private int getItemViewType(Cursor cursor) { 286 String type = cursor.getString(mColumnsMap.mColumnMsgType); 287 int boxId; 288 if ("sms".equals(type)) { 289 boxId = cursor.getInt(mColumnsMap.mColumnSmsType); 290 // Note that messages from the SIM card all have a boxId of zero. 291 return (boxId == TextBasedSmsColumns.MESSAGE_TYPE_INBOX || 292 boxId == TextBasedSmsColumns.MESSAGE_TYPE_ALL) ? 293 INCOMING_ITEM_TYPE_SMS : OUTGOING_ITEM_TYPE_SMS; 294 } else { 295 boxId = cursor.getInt(mColumnsMap.mColumnMmsMessageBox); 296 // Note that messages from the SIM card all have a boxId of zero: Mms.MESSAGE_BOX_ALL 297 return (boxId == Mms.MESSAGE_BOX_INBOX || boxId == Mms.MESSAGE_BOX_ALL) ? 298 INCOMING_ITEM_TYPE_MMS : OUTGOING_ITEM_TYPE_MMS; 299 } 300 } 301 302 public Cursor getCursorForItem(MessageItem item) { 303 Cursor cursor = getCursor(); 304 if (isCursorValid(cursor)) { 305 if (cursor.moveToFirst()) { 306 do { 307 long id = cursor.getLong(mRowIDColumn); 308 if (id == item.mMsgId) { 309 return cursor; 310 } 311 } while (cursor.moveToNext()); 312 } 313 } 314 return null; 315 } 316 317 public static class ColumnsMap { 318 public int mColumnMsgType; 319 public int mColumnMsgId; 320 public int mColumnSmsAddress; 321 public int mColumnSmsBody; 322 public int mColumnSmsDate; 323 public int mColumnSmsDateSent; 324 public int mColumnSmsRead; 325 public int mColumnSmsType; 326 public int mColumnSmsStatus; 327 public int mColumnSmsLocked; 328 public int mColumnSmsErrorCode; 329 public int mColumnMmsSubject; 330 public int mColumnMmsSubjectCharset; 331 public int mColumnMmsDate; 332 public int mColumnMmsDateSent; 333 public int mColumnMmsRead; 334 public int mColumnMmsMessageType; 335 public int mColumnMmsMessageBox; 336 public int mColumnMmsDeliveryReport; 337 public int mColumnMmsReadReport; 338 public int mColumnMmsErrorType; 339 public int mColumnMmsLocked; 340 public int mColumnMmsStatus; 341 public int mColumnMmsTextOnly; 342 343 public ColumnsMap() { 344 mColumnMsgType = COLUMN_MSG_TYPE; 345 mColumnMsgId = COLUMN_ID; 346 mColumnSmsAddress = COLUMN_SMS_ADDRESS; 347 mColumnSmsBody = COLUMN_SMS_BODY; 348 mColumnSmsDate = COLUMN_SMS_DATE; 349 mColumnSmsDateSent = COLUMN_SMS_DATE_SENT; 350 mColumnSmsType = COLUMN_SMS_TYPE; 351 mColumnSmsStatus = COLUMN_SMS_STATUS; 352 mColumnSmsLocked = COLUMN_SMS_LOCKED; 353 mColumnSmsErrorCode = COLUMN_SMS_ERROR_CODE; 354 mColumnMmsSubject = COLUMN_MMS_SUBJECT; 355 mColumnMmsSubjectCharset = COLUMN_MMS_SUBJECT_CHARSET; 356 mColumnMmsMessageType = COLUMN_MMS_MESSAGE_TYPE; 357 mColumnMmsMessageBox = COLUMN_MMS_MESSAGE_BOX; 358 mColumnMmsDeliveryReport = COLUMN_MMS_DELIVERY_REPORT; 359 mColumnMmsReadReport = COLUMN_MMS_READ_REPORT; 360 mColumnMmsErrorType = COLUMN_MMS_ERROR_TYPE; 361 mColumnMmsLocked = COLUMN_MMS_LOCKED; 362 mColumnMmsStatus = COLUMN_MMS_STATUS; 363 mColumnMmsTextOnly = COLUMN_MMS_TEXT_ONLY; 364 } 365 366 public ColumnsMap(Cursor cursor) { 367 // Ignore all 'not found' exceptions since the custom columns 368 // may be just a subset of the default columns. 369 try { 370 mColumnMsgType = cursor.getColumnIndexOrThrow( 371 MmsSms.TYPE_DISCRIMINATOR_COLUMN); 372 } catch (IllegalArgumentException e) { 373 Log.w("colsMap", e.getMessage()); 374 } 375 376 try { 377 mColumnMsgId = cursor.getColumnIndexOrThrow(BaseColumns._ID); 378 } catch (IllegalArgumentException e) { 379 Log.w("colsMap", e.getMessage()); 380 } 381 382 try { 383 mColumnSmsAddress = cursor.getColumnIndexOrThrow(Sms.ADDRESS); 384 } catch (IllegalArgumentException e) { 385 Log.w("colsMap", e.getMessage()); 386 } 387 388 try { 389 mColumnSmsBody = cursor.getColumnIndexOrThrow(Sms.BODY); 390 } catch (IllegalArgumentException e) { 391 Log.w("colsMap", e.getMessage()); 392 } 393 394 try { 395 mColumnSmsDate = cursor.getColumnIndexOrThrow(Sms.DATE); 396 } catch (IllegalArgumentException e) { 397 Log.w("colsMap", e.getMessage()); 398 } 399 400 try { 401 mColumnSmsDateSent = cursor.getColumnIndexOrThrow(Sms.DATE_SENT); 402 } catch (IllegalArgumentException e) { 403 Log.w("colsMap", e.getMessage()); 404 } 405 406 try { 407 mColumnSmsType = cursor.getColumnIndexOrThrow(Sms.TYPE); 408 } catch (IllegalArgumentException e) { 409 Log.w("colsMap", e.getMessage()); 410 } 411 412 try { 413 mColumnSmsStatus = cursor.getColumnIndexOrThrow(Sms.STATUS); 414 } catch (IllegalArgumentException e) { 415 Log.w("colsMap", e.getMessage()); 416 } 417 418 try { 419 mColumnSmsLocked = cursor.getColumnIndexOrThrow(Sms.LOCKED); 420 } catch (IllegalArgumentException e) { 421 Log.w("colsMap", e.getMessage()); 422 } 423 424 try { 425 mColumnSmsErrorCode = cursor.getColumnIndexOrThrow(Sms.ERROR_CODE); 426 } catch (IllegalArgumentException e) { 427 Log.w("colsMap", e.getMessage()); 428 } 429 430 try { 431 mColumnMmsSubject = cursor.getColumnIndexOrThrow(Mms.SUBJECT); 432 } catch (IllegalArgumentException e) { 433 Log.w("colsMap", e.getMessage()); 434 } 435 436 try { 437 mColumnMmsSubjectCharset = cursor.getColumnIndexOrThrow(Mms.SUBJECT_CHARSET); 438 } catch (IllegalArgumentException e) { 439 Log.w("colsMap", e.getMessage()); 440 } 441 442 try { 443 mColumnMmsMessageType = cursor.getColumnIndexOrThrow(Mms.MESSAGE_TYPE); 444 } catch (IllegalArgumentException e) { 445 Log.w("colsMap", e.getMessage()); 446 } 447 448 try { 449 mColumnMmsMessageBox = cursor.getColumnIndexOrThrow(Mms.MESSAGE_BOX); 450 } catch (IllegalArgumentException e) { 451 Log.w("colsMap", e.getMessage()); 452 } 453 454 try { 455 mColumnMmsDeliveryReport = cursor.getColumnIndexOrThrow(Mms.DELIVERY_REPORT); 456 } catch (IllegalArgumentException e) { 457 Log.w("colsMap", e.getMessage()); 458 } 459 460 try { 461 mColumnMmsReadReport = cursor.getColumnIndexOrThrow(Mms.READ_REPORT); 462 } catch (IllegalArgumentException e) { 463 Log.w("colsMap", e.getMessage()); 464 } 465 466 try { 467 mColumnMmsErrorType = cursor.getColumnIndexOrThrow(PendingMessages.ERROR_TYPE); 468 } catch (IllegalArgumentException e) { 469 Log.w("colsMap", e.getMessage()); 470 } 471 472 try { 473 mColumnMmsLocked = cursor.getColumnIndexOrThrow(Mms.LOCKED); 474 } catch (IllegalArgumentException e) { 475 Log.w("colsMap", e.getMessage()); 476 } 477 478 try { 479 mColumnMmsStatus = cursor.getColumnIndexOrThrow(Mms.STATUS); 480 } catch (IllegalArgumentException e) { 481 Log.w("colsMap", e.getMessage()); 482 } 483 484 try { 485 mColumnMmsTextOnly = cursor.getColumnIndexOrThrow(Mms.TEXT_ONLY); 486 } catch (IllegalArgumentException e) { 487 Log.w("colsMap", e.getMessage()); 488 } 489 } 490 } 491 492 private static class MessageItemCache extends LruCache<Long, MessageItem> { 493 public MessageItemCache(int maxSize) { 494 super(maxSize); 495 } 496 497 @Override 498 protected void entryRemoved(boolean evicted, Long key, 499 MessageItem oldValue, MessageItem newValue) { 500 oldValue.cancelPduLoading(); 501 } 502 } 503 } 504