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