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
18package com.android.mms.ui;
19
20import java.util.regex.Pattern;
21
22import android.content.Context;
23import android.database.Cursor;
24import android.os.Handler;
25import android.provider.BaseColumns;
26import android.provider.Telephony.Mms;
27import android.provider.Telephony.MmsSms;
28import android.provider.Telephony.MmsSms.PendingMessages;
29import android.provider.Telephony.Sms;
30import android.provider.Telephony.Sms.Conversations;
31import android.provider.Telephony.TextBasedSmsColumns;
32import android.util.Log;
33import android.util.LruCache;
34import android.view.LayoutInflater;
35import android.view.View;
36import android.view.ViewGroup;
37import android.widget.AbsListView;
38import android.widget.CursorAdapter;
39import android.widget.ListView;
40
41import com.android.mms.R;
42import com.google.android.mms.MmsException;
43
44/**
45 * The back-end data adapter of a message list.
46 */
47public 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
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
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 {
172        void onDataSetChanged(MessageListAdapter adapter);
173        void onContentChanged(MessageListAdapter adapter);
174    }
175
176    public void setOnDataSetChangedListener(OnDataSetChangedListener l) {
177        mOnDataSetChangedListener = l;
178    }
179
180    public void setMsgListItemHandler(Handler handler) {
181        mMsgListItemHandler = handler;
182    }
183
184    public void setIsGroupConversation(boolean isGroup) {
185        mIsGroupConversation = isGroup;
186    }
187
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
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
209    protected void onContentChanged() {
210        if (getCursor() != null && !getCursor().isClosed()) {
211            if (mOnDataSetChangedListener != null) {
212                mOnDataSetChangedListener.onContentChanged(this);
213            }
214        }
215    }
216
217    @Override
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
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
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
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
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
275    public int getViewTypeCount() {
276        return 4;   // Incoming and outgoing messages, both sms and mms
277    }
278
279    @Override
280    public int getItemViewType(int position) {
281        Cursor cursor = (Cursor)getItem(position);
282        return getItemViewType(cursor);
283    }
284
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
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
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
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> {
493        public MessageItemCache(int maxSize) {
494            super(maxSize);
495        }
496
497        @Override
498        protected void entryRemoved(boolean evicted, Long key,
499                MessageItem oldValue, MessageItem newValue) {
500            oldValue.cancelPduLoading();
501        }
502    }
503}
504