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