• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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