• 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 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 
MessageListAdapter( Context context, Cursor c, ListView listView, boolean useDefaultColumnsMap, Pattern highlight)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
bindView(View view, Context context, Cursor cursor)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 {
onDataSetChanged(MessageListAdapter adapter)172         void onDataSetChanged(MessageListAdapter adapter);
onContentChanged(MessageListAdapter adapter)173         void onContentChanged(MessageListAdapter adapter);
174     }
175 
setOnDataSetChangedListener(OnDataSetChangedListener l)176     public void setOnDataSetChangedListener(OnDataSetChangedListener l) {
177         mOnDataSetChangedListener = l;
178     }
179 
setMsgListItemHandler(Handler handler)180     public void setMsgListItemHandler(Handler handler) {
181         mMsgListItemHandler = handler;
182     }
183 
setIsGroupConversation(boolean isGroup)184     public void setIsGroupConversation(boolean isGroup) {
185         mIsGroupConversation = isGroup;
186     }
187 
cancelBackgroundLoading()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
notifyDataSetChanged()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
onContentChanged()209     protected void onContentChanged() {
210         if (getCursor() != null && !getCursor().isClosed()) {
211             if (mOnDataSetChangedListener != null) {
212                 mOnDataSetChangedListener.onContentChanged(this);
213             }
214         }
215     }
216 
217     @Override
newView(Context context, Cursor cursor, ViewGroup parent)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 
getCachedMessageItem(String type, long msgId, Cursor c)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 
isCursorValid(Cursor cursor)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 
getKey(String type, long id)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
areAllItemsEnabled()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
getViewTypeCount()275     public int getViewTypeCount() {
276         return 4;   // Incoming and outgoing messages, both sms and mms
277     }
278 
279     @Override
getItemViewType(int position)280     public int getItemViewType(int position) {
281         Cursor cursor = (Cursor)getItem(position);
282         return getItemViewType(cursor);
283     }
284 
getItemViewType(Cursor cursor)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 
getCursorForItem(MessageItem item)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 
ColumnsMap()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 
ColumnsMap(Cursor cursor)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> {
MessageItemCache(int maxSize)493         public MessageItemCache(int maxSize) {
494             super(maxSize);
495         }
496 
497         @Override
entryRemoved(boolean evicted, Long key, MessageItem oldValue, MessageItem newValue)498         protected void entryRemoved(boolean evicted, Long key,
499                 MessageItem oldValue, MessageItem newValue) {
500             oldValue.cancelPduLoading();
501         }
502     }
503 }
504