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