MessageListAdapter.java revision f114492537753bc68640d4a0d403861387296bcb
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 com.android.mms.R;
21import com.android.mms.mms.MmsException;
22
23import android.content.AsyncQueryHandler;
24import android.content.ContentResolver;
25import android.content.ContentUris;
26import android.content.Context;
27import android.database.Cursor;
28import android.graphics.Bitmap;
29import android.graphics.BitmapFactory;
30import android.graphics.drawable.BitmapDrawable;
31import android.graphics.drawable.Drawable;
32import android.net.Uri;
33import android.os.Handler;
34import android.provider.BaseColumns;
35import android.provider.ContactsContract.Contacts;
36import android.provider.ContactsContract.Data;
37import android.provider.ContactsContract.PhoneLookup;
38import android.provider.ContactsContract.RawContacts;
39import android.provider.ContactsContract.StatusUpdates;
40import android.provider.ContactsContract.CommonDataKinds.Email;
41import android.provider.ContactsContract.CommonDataKinds.Photo;
42import com.android.mms.telephony.TelephonyProvider.Mms;
43import com.android.mms.telephony.TelephonyProvider.MmsSms;
44import com.android.mms.telephony.TelephonyProvider.Sms;
45import com.android.mms.telephony.TelephonyProvider.MmsSms.PendingMessages;
46import com.android.mms.telephony.TelephonyProvider.Sms.Conversations;
47import android.text.TextUtils;
48import android.text.format.DateUtils;
49import android.util.Config;
50import android.util.Log;
51import android.view.LayoutInflater;
52import android.view.View;
53import android.view.ViewGroup;
54import android.widget.CursorAdapter;
55import android.widget.ListView;
56
57import java.util.HashMap;
58import java.util.HashSet;
59import java.util.LinkedHashMap;
60import java.util.Map;
61import java.util.regex.Pattern;
62
63/**
64 * The back-end data adapter of a message list.
65 */
66public class MessageListAdapter extends CursorAdapter {
67    private static final String TAG = "MessageListAdapter";
68    private static final boolean DEBUG = false;
69    private static final boolean LOCAL_LOGV = Config.LOGV && DEBUG;
70
71    static final String[] PROJECTION = new String[] {
72        // TODO: should move this symbol into com.android.mms.telephony.Telephony.
73        MmsSms.TYPE_DISCRIMINATOR_COLUMN,
74        BaseColumns._ID,
75        Conversations.THREAD_ID,
76        // For SMS
77        Sms.ADDRESS,
78        Sms.BODY,
79        Sms.DATE,
80        Sms.READ,
81        Sms.TYPE,
82        Sms.STATUS,
83        Sms.LOCKED,
84        Sms.ERROR_CODE,
85        // For MMS
86        Mms.SUBJECT,
87        Mms.SUBJECT_CHARSET,
88        Mms.DATE,
89        Mms.READ,
90        Mms.MESSAGE_TYPE,
91        Mms.MESSAGE_BOX,
92        Mms.DELIVERY_REPORT,
93        Mms.READ_REPORT,
94        PendingMessages.ERROR_TYPE,
95        Mms.LOCKED
96    };
97
98    // The indexes of the default columns which must be consistent
99    // with above PROJECTION.
100    static final int COLUMN_MSG_TYPE            = 0;
101    static final int COLUMN_ID                  = 1;
102    static final int COLUMN_THREAD_ID           = 2;
103    static final int COLUMN_SMS_ADDRESS         = 3;
104    static final int COLUMN_SMS_BODY            = 4;
105    static final int COLUMN_SMS_DATE            = 5;
106    static final int COLUMN_SMS_READ            = 6;
107    static final int COLUMN_SMS_TYPE            = 7;
108    static final int COLUMN_SMS_STATUS          = 8;
109    static final int COLUMN_SMS_LOCKED          = 9;
110    static final int COLUMN_SMS_ERROR_CODE      = 10;
111    static final int COLUMN_MMS_SUBJECT         = 11;
112    static final int COLUMN_MMS_SUBJECT_CHARSET = 12;
113    static final int COLUMN_MMS_DATE            = 13;
114    static final int COLUMN_MMS_READ            = 14;
115    static final int COLUMN_MMS_MESSAGE_TYPE    = 15;
116    static final int COLUMN_MMS_MESSAGE_BOX     = 16;
117    static final int COLUMN_MMS_DELIVERY_REPORT = 17;
118    static final int COLUMN_MMS_READ_REPORT     = 18;
119    static final int COLUMN_MMS_ERROR_TYPE      = 19;
120    static final int COLUMN_MMS_LOCKED          = 20;
121
122    private static final int CACHE_SIZE         = 50;
123
124    protected LayoutInflater mInflater;
125    private final ListView mListView;
126    private final LinkedHashMap<Long, MessageItem> mMessageItemCache;
127    private final ColumnsMap mColumnsMap;
128    private OnDataSetChangedListener mOnDataSetChangedListener;
129    private Handler mMsgListItemHandler;
130    private Pattern mHighlight;
131    private Context mContext;
132
133    public MessageListAdapter(
134            Context context, Cursor c, ListView listView,
135            boolean useDefaultColumnsMap, Pattern highlight) {
136        super(context, c, false /* auto-requery */);
137        mContext = context;
138        mHighlight = highlight;
139
140        mInflater = (LayoutInflater) context.getSystemService(
141                Context.LAYOUT_INFLATER_SERVICE);
142        mListView = listView;
143        mMessageItemCache = new LinkedHashMap<Long, MessageItem>(
144                    10, 1.0f, true) {
145            @Override
146            protected boolean removeEldestEntry(Map.Entry eldest) {
147                return size() > CACHE_SIZE;
148            }
149        };
150
151        if (useDefaultColumnsMap) {
152            mColumnsMap = new ColumnsMap();
153        } else {
154            mColumnsMap = new ColumnsMap(c);
155        }
156
157        mAvatarCache = new AvatarCache();
158    }
159
160    @Override
161    public void bindView(View view, Context context, Cursor cursor) {
162        if (view instanceof MessageListItem) {
163            String type = cursor.getString(mColumnsMap.mColumnMsgType);
164            long msgId = cursor.getLong(mColumnsMap.mColumnMsgId);
165
166            MessageItem msgItem = getCachedMessageItem(type, msgId, cursor);
167            if (msgItem != null) {
168                ((MessageListItem) view).bind(mAvatarCache, msgItem);
169                ((MessageListItem) view).setMsgListItemHandler(mMsgListItemHandler);
170            }
171        }
172    }
173
174    public interface OnDataSetChangedListener {
175        void onDataSetChanged(MessageListAdapter adapter);
176        void onContentChanged(MessageListAdapter adapter);
177    }
178
179    public void setOnDataSetChangedListener(OnDataSetChangedListener l) {
180        mOnDataSetChangedListener = l;
181    }
182
183    public void setMsgListItemHandler(Handler handler) {
184        mMsgListItemHandler = handler;
185    }
186
187    @Override
188    public void notifyDataSetChanged() {
189        super.notifyDataSetChanged();
190        if (LOCAL_LOGV) {
191            Log.v(TAG, "MessageListAdapter.notifyDataSetChanged().");
192        }
193
194        mListView.setSelection(mListView.getCount());
195        mMessageItemCache.clear();
196
197        if (mOnDataSetChangedListener != null) {
198            mOnDataSetChangedListener.onDataSetChanged(this);
199        }
200    }
201
202    @Override
203    protected void onContentChanged() {
204        if (getCursor() != null && !getCursor().isClosed()) {
205            if (mOnDataSetChangedListener != null) {
206                mOnDataSetChangedListener.onContentChanged(this);
207            }
208        }
209    }
210
211    @Override
212    public View newView(Context context, Cursor cursor, ViewGroup parent) {
213        return mInflater.inflate(R.layout.message_list_item, parent, false);
214    }
215
216    public MessageItem getCachedMessageItem(String type, long msgId, Cursor c) {
217        MessageItem item = mMessageItemCache.get(getKey(type, msgId));
218        if (item == null) {
219            try {
220                item = new MessageItem(mContext, type, c, mColumnsMap, mHighlight);
221                mMessageItemCache.put(getKey(item.mType, item.mMsgId), item);
222            } catch (MmsException e) {
223                Log.e(TAG, e.getMessage());
224            }
225        }
226        return item;
227    }
228
229    private static long getKey(String type, long id) {
230        if (type.equals("mms")) {
231            return -id;
232        } else {
233            return id;
234        }
235    }
236
237    public static class ColumnsMap {
238        public int mColumnMsgType;
239        public int mColumnMsgId;
240        public int mColumnSmsAddress;
241        public int mColumnSmsBody;
242        public int mColumnSmsDate;
243        public int mColumnSmsRead;
244        public int mColumnSmsType;
245        public int mColumnSmsStatus;
246        public int mColumnSmsLocked;
247        public int mColumnSmsErrorCode;
248        public int mColumnMmsSubject;
249        public int mColumnMmsSubjectCharset;
250        public int mColumnMmsDate;
251        public int mColumnMmsRead;
252        public int mColumnMmsMessageType;
253        public int mColumnMmsMessageBox;
254        public int mColumnMmsDeliveryReport;
255        public int mColumnMmsReadReport;
256        public int mColumnMmsErrorType;
257        public int mColumnMmsLocked;
258
259        public ColumnsMap() {
260            mColumnMsgType            = COLUMN_MSG_TYPE;
261            mColumnMsgId              = COLUMN_ID;
262            mColumnSmsAddress         = COLUMN_SMS_ADDRESS;
263            mColumnSmsBody            = COLUMN_SMS_BODY;
264            mColumnSmsDate            = COLUMN_SMS_DATE;
265            mColumnSmsType            = COLUMN_SMS_TYPE;
266            mColumnSmsStatus          = COLUMN_SMS_STATUS;
267            mColumnSmsLocked          = COLUMN_SMS_LOCKED;
268            mColumnSmsErrorCode       = COLUMN_SMS_ERROR_CODE;
269            mColumnMmsSubject         = COLUMN_MMS_SUBJECT;
270            mColumnMmsSubjectCharset  = COLUMN_MMS_SUBJECT_CHARSET;
271            mColumnMmsMessageType     = COLUMN_MMS_MESSAGE_TYPE;
272            mColumnMmsMessageBox      = COLUMN_MMS_MESSAGE_BOX;
273            mColumnMmsDeliveryReport  = COLUMN_MMS_DELIVERY_REPORT;
274            mColumnMmsReadReport      = COLUMN_MMS_READ_REPORT;
275            mColumnMmsErrorType       = COLUMN_MMS_ERROR_TYPE;
276            mColumnMmsLocked          = COLUMN_MMS_LOCKED;
277        }
278
279        public ColumnsMap(Cursor cursor) {
280            // Ignore all 'not found' exceptions since the custom columns
281            // may be just a subset of the default columns.
282            try {
283                mColumnMsgType = cursor.getColumnIndexOrThrow(
284                        MmsSms.TYPE_DISCRIMINATOR_COLUMN);
285            } catch (IllegalArgumentException e) {
286                Log.w("colsMap", e.getMessage());
287            }
288
289            try {
290                mColumnMsgId = cursor.getColumnIndexOrThrow(BaseColumns._ID);
291            } catch (IllegalArgumentException e) {
292                Log.w("colsMap", e.getMessage());
293            }
294
295            try {
296                mColumnSmsAddress = cursor.getColumnIndexOrThrow(Sms.ADDRESS);
297            } catch (IllegalArgumentException e) {
298                Log.w("colsMap", e.getMessage());
299            }
300
301            try {
302                mColumnSmsBody = cursor.getColumnIndexOrThrow(Sms.BODY);
303            } catch (IllegalArgumentException e) {
304                Log.w("colsMap", e.getMessage());
305            }
306
307            try {
308                mColumnSmsDate = cursor.getColumnIndexOrThrow(Sms.DATE);
309            } catch (IllegalArgumentException e) {
310                Log.w("colsMap", e.getMessage());
311            }
312
313            try {
314                mColumnSmsType = cursor.getColumnIndexOrThrow(Sms.TYPE);
315            } catch (IllegalArgumentException e) {
316                Log.w("colsMap", e.getMessage());
317            }
318
319            try {
320                mColumnSmsStatus = cursor.getColumnIndexOrThrow(Sms.STATUS);
321            } catch (IllegalArgumentException e) {
322                Log.w("colsMap", e.getMessage());
323            }
324
325            try {
326                mColumnSmsLocked = cursor.getColumnIndexOrThrow(Sms.LOCKED);
327            } catch (IllegalArgumentException e) {
328                Log.w("colsMap", e.getMessage());
329            }
330
331            try {
332                mColumnSmsErrorCode = cursor.getColumnIndexOrThrow(Sms.ERROR_CODE);
333            } catch (IllegalArgumentException e) {
334                Log.w("colsMap", e.getMessage());
335            }
336
337            try {
338                mColumnMmsSubject = cursor.getColumnIndexOrThrow(Mms.SUBJECT);
339            } catch (IllegalArgumentException e) {
340                Log.w("colsMap", e.getMessage());
341            }
342
343            try {
344                mColumnMmsSubjectCharset = cursor.getColumnIndexOrThrow(Mms.SUBJECT_CHARSET);
345            } catch (IllegalArgumentException e) {
346                Log.w("colsMap", e.getMessage());
347            }
348
349            try {
350                mColumnMmsMessageType = cursor.getColumnIndexOrThrow(Mms.MESSAGE_TYPE);
351            } catch (IllegalArgumentException e) {
352                Log.w("colsMap", e.getMessage());
353            }
354
355            try {
356                mColumnMmsMessageBox = cursor.getColumnIndexOrThrow(Mms.MESSAGE_BOX);
357            } catch (IllegalArgumentException e) {
358                Log.w("colsMap", e.getMessage());
359            }
360
361            try {
362                mColumnMmsDeliveryReport = cursor.getColumnIndexOrThrow(Mms.DELIVERY_REPORT);
363            } catch (IllegalArgumentException e) {
364                Log.w("colsMap", e.getMessage());
365            }
366
367            try {
368                mColumnMmsReadReport = cursor.getColumnIndexOrThrow(Mms.READ_REPORT);
369            } catch (IllegalArgumentException e) {
370                Log.w("colsMap", e.getMessage());
371            }
372
373            try {
374                mColumnMmsErrorType = cursor.getColumnIndexOrThrow(PendingMessages.ERROR_TYPE);
375            } catch (IllegalArgumentException e) {
376                Log.w("colsMap", e.getMessage());
377            }
378
379            try {
380                mColumnMmsLocked = cursor.getColumnIndexOrThrow(Mms.LOCKED);
381            } catch (IllegalArgumentException e) {
382                Log.w("colsMap", e.getMessage());
383            }
384        }
385    }
386
387    private AvatarCache mAvatarCache;
388
389    /*
390     * Track avatars for each of the members of in the group chat.
391     */
392    class AvatarCache {
393        private static final int TOKEN_PHONE_LOOKUP = 101;
394        private static final int TOKEN_EMAIL_LOOKUP = 102;
395        private static final int TOKEN_CONTACT_INFO = 201;
396        private static final int TOKEN_PHOTO_DATA = 301;
397
398        //Projection used for the summary info in the header.
399        private final String[] COLUMNS = new String[] {
400                  Contacts._ID,
401                  Contacts.PHOTO_ID,
402                  // Other fields which we might want/need in the future (for example)
403//                Contacts.LOOKUP_KEY,
404//                Contacts.DISPLAY_NAME,
405//                Contacts.STARRED,
406//                Contacts.CONTACT_PRESENCE,
407//                Contacts.CONTACT_STATUS,
408//                Contacts.CONTACT_STATUS_TIMESTAMP,
409//                Contacts.CONTACT_STATUS_RES_PACKAGE,
410//                Contacts.CONTACT_STATUS_LABEL,
411        };
412        private final int PHOTO_ID = 1;
413
414        private final String[] PHONE_LOOKUP_PROJECTION = new String[] {
415            PhoneLookup._ID,
416            PhoneLookup.LOOKUP_KEY,
417        };
418        private static final int PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0;
419        private static final int PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1;
420
421        private final String[] EMAIL_LOOKUP_PROJECTION = new String[] {
422            RawContacts.CONTACT_ID,
423            Contacts.LOOKUP_KEY,
424        };
425        private static final int EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0;
426        private static final int EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1;
427
428
429        /*
430         * Map from mAddress to a blob of data which contains the contact id
431         * and the avatar.
432         */
433        HashMap<String, ContactData> mImageCache = new HashMap<String, ContactData>();
434
435        public class ContactData {
436            private String mAddress;
437            private long mContactId;
438            private Uri mContactUri;
439            private Drawable mPhoto;
440
441            ContactData(String address) {
442                mAddress = address;
443            }
444
445            public Drawable getAvatar() {
446                return mPhoto;
447            }
448
449            public Uri getContactUri() {
450                return mContactUri;
451            }
452
453            private boolean startInitialQuery() {
454                if (Mms.isPhoneNumber(mAddress)) {
455                    mQueryHandler.startQuery(
456                            TOKEN_PHONE_LOOKUP,
457                            this,
458                            Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(mAddress)),
459                            PHONE_LOOKUP_PROJECTION,
460                            null,
461                            null,
462                            null);
463                    return true;
464                } else if (Mms.isEmailAddress(mAddress)) {
465                    mQueryHandler.startQuery(
466                            TOKEN_EMAIL_LOOKUP,
467                            this,
468                            Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mAddress)),
469                            EMAIL_LOOKUP_PROJECTION,
470                            null,
471                            null,
472                            null);
473                    return true;
474                } else {
475                    return false;
476                }
477            }
478            /*
479             * Once we have the photo data load it into a drawable.
480             */
481            private boolean onPhotoDataLoaded(Cursor c) {
482                if (c == null || !c.moveToFirst()) return false;
483
484                try {
485                    byte[] photoData = c.getBlob(0);
486                    Bitmap b = BitmapFactory.decodeByteArray(photoData, 0, photoData.length, null);
487                    mPhoto = new BitmapDrawable(mContext.getResources(), b);
488                    return true;
489                } catch (Exception ex) {
490                    return false;
491                }
492            }
493
494            /*
495             * Once we have the contact info loaded take the photo id and query
496             * for the photo data.
497             */
498            private boolean onContactInfoLoaded(Cursor c) {
499                if (c == null || !c.moveToFirst()) return false;
500
501                mContactId = c.getLong(PHOTO_ID);
502                mContactUri = ContentUris.withAppendedId(Data.CONTENT_URI, mContactId);
503                mQueryHandler.startQuery(
504                        TOKEN_PHOTO_DATA,
505                        this,
506                        mContactUri,
507                        new String[] { Photo.PHOTO },
508                        null,
509                        null,
510                        null);
511
512                return true;
513            }
514
515            /*
516             * Once we have the contact id loaded start the query for the
517             * contact information (which will give us the photo id).
518             */
519            private boolean onContactIdLoaded(Cursor c, int contactIdColumn, int lookupKeyColumn) {
520                if (c == null || !c.moveToFirst()) return false;
521
522                mContactId = c.getLong(contactIdColumn);
523                String lookupKey = c.getString(lookupKeyColumn);
524                Uri contactUri = Contacts.getLookupUri(mContactId, lookupKey);
525                mQueryHandler.startQuery(
526                        TOKEN_CONTACT_INFO,
527                        this,
528                        contactUri,
529                        COLUMNS,
530                        null,
531                        null,
532                        null);
533                return true;
534            }
535
536            /*
537             * If for whatever reason we can't get the photo load teh
538             * default avatar.  NOTE that fasttrack tries to get fancy
539             * with various random images (upside down, etc.) we're not
540             * doing that here.
541             */
542            private void loadDefaultAvatar() {
543                if (mDefaultAvatarDrawable == null) {
544                    Bitmap b = BitmapFactory.decodeResource(mContext.getResources(),
545                            R.drawable.ic_contact_picture);
546                    mDefaultAvatarDrawable = new BitmapDrawable(mContext.getResources(), b);
547                }
548                mPhoto = mDefaultAvatarDrawable;
549            }
550
551        };
552
553        Drawable mDefaultAvatarDrawable = null;
554        AsyncQueryHandler mQueryHandler = new AsyncQueryHandler(mContext.getContentResolver()) {
555            @Override
556            protected void onQueryComplete(int token, Object cookieObject, Cursor cursor) {
557                super.onQueryComplete(token, cookieObject, cursor);
558
559                ContactData cookie = (ContactData) cookieObject;
560                switch (token) {
561                    case TOKEN_PHONE_LOOKUP: {
562                        if (!cookie.onContactIdLoaded(
563                                cursor,
564                                PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX,
565                                PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX)) {
566                            cookie.loadDefaultAvatar();
567                        }
568                        break;
569                    }
570                    case TOKEN_EMAIL_LOOKUP: {
571                        if (!cookie.onContactIdLoaded(
572                                cursor,
573                                EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX,
574                                EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX)) {
575                            cookie.loadDefaultAvatar();
576                        }
577                        break;
578                    }
579                    case TOKEN_CONTACT_INFO: {
580                        if (!cookie.onContactInfoLoaded(cursor)) {
581                            cookie.loadDefaultAvatar();
582                        }
583                        break;
584                    }
585                    case TOKEN_PHOTO_DATA: {
586                        if (!cookie.onPhotoDataLoaded(cursor)) {
587                            cookie.loadDefaultAvatar();
588                        } else {
589                            MessageListAdapter.this.notifyDataSetChanged();
590                        }
591                        break;
592                    }
593                    default:
594                        break;
595                }
596            }
597        };
598
599        public ContactData get(final String address) {
600            if (mImageCache.containsKey(address)) {
601                return mImageCache.get(address);
602            } else {
603                // Create the ContactData object and put it into the hashtable
604                // so that any subsequent requests for this same avatar do not kick
605                // off another query.
606                ContactData cookie = new ContactData(address);
607                mImageCache.put(address, cookie);
608                if (!cookie.startInitialQuery()) {
609                    cookie.loadDefaultAvatar();
610                }
611                return cookie;
612            }
613        }
614
615        public AvatarCache() {
616        }
617    };
618
619
620}
621