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