MessageListAdapter.java revision 9b9f5da86e17db206e0581704f41383ee0805049
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.google.android.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 android.provider.Telephony.Mms;
43import android.provider.Telephony.MmsSms;
44import android.provider.Telephony.Sms;
45import android.provider.Telephony.MmsSms.PendingMessages;
46import android.provider.Telephony.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    private HashMap<String, HashSet<MessageListItem>> mAddressToMessageListItems
134        = new HashMap<String, HashSet<MessageListItem>>();
135
136    public MessageListAdapter(
137            Context context, Cursor c, ListView listView,
138            boolean useDefaultColumnsMap, Pattern highlight) {
139        super(context, c, false /* auto-requery */);
140        mContext = context;
141        mHighlight = highlight;
142
143        mInflater = (LayoutInflater) context.getSystemService(
144                Context.LAYOUT_INFLATER_SERVICE);
145        mListView = listView;
146        mMessageItemCache = new LinkedHashMap<Long, MessageItem>(
147                    10, 1.0f, true) {
148            @Override
149            protected boolean removeEldestEntry(Map.Entry eldest) {
150                return size() > CACHE_SIZE;
151            }
152        };
153
154        if (useDefaultColumnsMap) {
155            mColumnsMap = new ColumnsMap();
156        } else {
157            mColumnsMap = new ColumnsMap(c);
158        }
159
160        mAvatarCache = new AvatarCache();
161    }
162
163    @Override
164    public void bindView(View view, Context context, Cursor cursor) {
165        if (view instanceof MessageListItem) {
166            String type = cursor.getString(mColumnsMap.mColumnMsgType);
167            long msgId = cursor.getLong(mColumnsMap.mColumnMsgId);
168
169            MessageItem msgItem = getCachedMessageItem(type, msgId, cursor);
170            if (msgItem != null) {
171                MessageListItem mli = (MessageListItem) view;
172
173                // Remove previous item from mapping
174                MessageItem oldMessageItem = mli.getMessageItem();
175                if (oldMessageItem != null) {
176                    String oldAddress = oldMessageItem.mAddress;
177                    if (oldAddress != null) {
178                        HashSet<MessageListItem> set = mAddressToMessageListItems.get(oldAddress);
179                        if (set != null) {
180                            set.remove(mli);
181                        }
182                    }
183                }
184
185                mli.bind(mAvatarCache, msgItem);
186                mli.setMsgListItemHandler(mMsgListItemHandler);
187
188                // Add current item to mapping
189
190                String addr;
191                if (!Sms.isOutgoingFolder(msgItem.mBoxId)) {
192                    addr = msgItem.mAddress;
193                } else {
194                    addr = MessageUtils.getLocalNumber();
195                }
196
197                HashSet<MessageListItem> set = mAddressToMessageListItems.get(addr);
198                if (set == null) {
199                    set = new HashSet<MessageListItem>();
200                    mAddressToMessageListItems.put(addr, set);
201                }
202                set.add(mli);
203            }
204        }
205    }
206
207    public interface OnDataSetChangedListener {
208        void onDataSetChanged(MessageListAdapter adapter);
209        void onContentChanged(MessageListAdapter adapter);
210    }
211
212    public void setOnDataSetChangedListener(OnDataSetChangedListener l) {
213        mOnDataSetChangedListener = l;
214    }
215
216    public void setMsgListItemHandler(Handler handler) {
217        mMsgListItemHandler = handler;
218    }
219
220    public void notifyImageLoaded(String address) {
221        HashSet<MessageListItem> set = mAddressToMessageListItems.get(address);
222        if (set != null) {
223            for (MessageListItem mli : set) {
224                mli.bind(mAvatarCache, mli.getMessageItem());
225            }
226        }
227    }
228
229    @Override
230    public void notifyDataSetChanged() {
231        super.notifyDataSetChanged();
232        if (LOCAL_LOGV) {
233            Log.v(TAG, "MessageListAdapter.notifyDataSetChanged().");
234        }
235
236        mListView.setSelection(mListView.getCount());
237        mMessageItemCache.clear();
238
239        if (mOnDataSetChangedListener != null) {
240            mOnDataSetChangedListener.onDataSetChanged(this);
241        }
242    }
243
244    @Override
245    protected void onContentChanged() {
246        if (getCursor() != null && !getCursor().isClosed()) {
247            if (mOnDataSetChangedListener != null) {
248                mOnDataSetChangedListener.onContentChanged(this);
249            }
250        }
251    }
252
253    @Override
254    public View newView(Context context, Cursor cursor, ViewGroup parent) {
255        return mInflater.inflate(R.layout.message_list_item, parent, false);
256    }
257
258    public MessageItem getCachedMessageItem(String type, long msgId, Cursor c) {
259        MessageItem item = mMessageItemCache.get(getKey(type, msgId));
260        if (item == null && c != null && isCursorValid(c)) {
261            try {
262                item = new MessageItem(mContext, type, c, mColumnsMap, mHighlight);
263                mMessageItemCache.put(getKey(item.mType, item.mMsgId), item);
264            } catch (MmsException e) {
265                Log.e(TAG, "getCachedMessageItem: ", e);
266            }
267        }
268        return item;
269    }
270
271    private boolean isCursorValid(Cursor cursor) {
272        // Check whether the cursor is valid or not.
273        if (cursor.isClosed() || cursor.isBeforeFirst() || cursor.isAfterLast()) {
274            return false;
275        }
276        return true;
277    }
278
279    private static long getKey(String type, long id) {
280        if (type.equals("mms")) {
281            return -id;
282        } else {
283            return id;
284        }
285    }
286
287    public static class ColumnsMap {
288        public int mColumnMsgType;
289        public int mColumnMsgId;
290        public int mColumnSmsAddress;
291        public int mColumnSmsBody;
292        public int mColumnSmsDate;
293        public int mColumnSmsRead;
294        public int mColumnSmsType;
295        public int mColumnSmsStatus;
296        public int mColumnSmsLocked;
297        public int mColumnSmsErrorCode;
298        public int mColumnMmsSubject;
299        public int mColumnMmsSubjectCharset;
300        public int mColumnMmsDate;
301        public int mColumnMmsRead;
302        public int mColumnMmsMessageType;
303        public int mColumnMmsMessageBox;
304        public int mColumnMmsDeliveryReport;
305        public int mColumnMmsReadReport;
306        public int mColumnMmsErrorType;
307        public int mColumnMmsLocked;
308
309        public ColumnsMap() {
310            mColumnMsgType            = COLUMN_MSG_TYPE;
311            mColumnMsgId              = COLUMN_ID;
312            mColumnSmsAddress         = COLUMN_SMS_ADDRESS;
313            mColumnSmsBody            = COLUMN_SMS_BODY;
314            mColumnSmsDate            = COLUMN_SMS_DATE;
315            mColumnSmsType            = COLUMN_SMS_TYPE;
316            mColumnSmsStatus          = COLUMN_SMS_STATUS;
317            mColumnSmsLocked          = COLUMN_SMS_LOCKED;
318            mColumnSmsErrorCode       = COLUMN_SMS_ERROR_CODE;
319            mColumnMmsSubject         = COLUMN_MMS_SUBJECT;
320            mColumnMmsSubjectCharset  = COLUMN_MMS_SUBJECT_CHARSET;
321            mColumnMmsMessageType     = COLUMN_MMS_MESSAGE_TYPE;
322            mColumnMmsMessageBox      = COLUMN_MMS_MESSAGE_BOX;
323            mColumnMmsDeliveryReport  = COLUMN_MMS_DELIVERY_REPORT;
324            mColumnMmsReadReport      = COLUMN_MMS_READ_REPORT;
325            mColumnMmsErrorType       = COLUMN_MMS_ERROR_TYPE;
326            mColumnMmsLocked          = COLUMN_MMS_LOCKED;
327        }
328
329        public ColumnsMap(Cursor cursor) {
330            // Ignore all 'not found' exceptions since the custom columns
331            // may be just a subset of the default columns.
332            try {
333                mColumnMsgType = cursor.getColumnIndexOrThrow(
334                        MmsSms.TYPE_DISCRIMINATOR_COLUMN);
335            } catch (IllegalArgumentException e) {
336                Log.w("colsMap", e.getMessage());
337            }
338
339            try {
340                mColumnMsgId = cursor.getColumnIndexOrThrow(BaseColumns._ID);
341            } catch (IllegalArgumentException e) {
342                Log.w("colsMap", e.getMessage());
343            }
344
345            try {
346                mColumnSmsAddress = cursor.getColumnIndexOrThrow(Sms.ADDRESS);
347            } catch (IllegalArgumentException e) {
348                Log.w("colsMap", e.getMessage());
349            }
350
351            try {
352                mColumnSmsBody = cursor.getColumnIndexOrThrow(Sms.BODY);
353            } catch (IllegalArgumentException e) {
354                Log.w("colsMap", e.getMessage());
355            }
356
357            try {
358                mColumnSmsDate = cursor.getColumnIndexOrThrow(Sms.DATE);
359            } catch (IllegalArgumentException e) {
360                Log.w("colsMap", e.getMessage());
361            }
362
363            try {
364                mColumnSmsType = cursor.getColumnIndexOrThrow(Sms.TYPE);
365            } catch (IllegalArgumentException e) {
366                Log.w("colsMap", e.getMessage());
367            }
368
369            try {
370                mColumnSmsStatus = cursor.getColumnIndexOrThrow(Sms.STATUS);
371            } catch (IllegalArgumentException e) {
372                Log.w("colsMap", e.getMessage());
373            }
374
375            try {
376                mColumnSmsLocked = cursor.getColumnIndexOrThrow(Sms.LOCKED);
377            } catch (IllegalArgumentException e) {
378                Log.w("colsMap", e.getMessage());
379            }
380
381            try {
382                mColumnSmsErrorCode = cursor.getColumnIndexOrThrow(Sms.ERROR_CODE);
383            } catch (IllegalArgumentException e) {
384                Log.w("colsMap", e.getMessage());
385            }
386
387            try {
388                mColumnMmsSubject = cursor.getColumnIndexOrThrow(Mms.SUBJECT);
389            } catch (IllegalArgumentException e) {
390                Log.w("colsMap", e.getMessage());
391            }
392
393            try {
394                mColumnMmsSubjectCharset = cursor.getColumnIndexOrThrow(Mms.SUBJECT_CHARSET);
395            } catch (IllegalArgumentException e) {
396                Log.w("colsMap", e.getMessage());
397            }
398
399            try {
400                mColumnMmsMessageType = cursor.getColumnIndexOrThrow(Mms.MESSAGE_TYPE);
401            } catch (IllegalArgumentException e) {
402                Log.w("colsMap", e.getMessage());
403            }
404
405            try {
406                mColumnMmsMessageBox = cursor.getColumnIndexOrThrow(Mms.MESSAGE_BOX);
407            } catch (IllegalArgumentException e) {
408                Log.w("colsMap", e.getMessage());
409            }
410
411            try {
412                mColumnMmsDeliveryReport = cursor.getColumnIndexOrThrow(Mms.DELIVERY_REPORT);
413            } catch (IllegalArgumentException e) {
414                Log.w("colsMap", e.getMessage());
415            }
416
417            try {
418                mColumnMmsReadReport = cursor.getColumnIndexOrThrow(Mms.READ_REPORT);
419            } catch (IllegalArgumentException e) {
420                Log.w("colsMap", e.getMessage());
421            }
422
423            try {
424                mColumnMmsErrorType = cursor.getColumnIndexOrThrow(PendingMessages.ERROR_TYPE);
425            } catch (IllegalArgumentException e) {
426                Log.w("colsMap", e.getMessage());
427            }
428
429            try {
430                mColumnMmsLocked = cursor.getColumnIndexOrThrow(Mms.LOCKED);
431            } catch (IllegalArgumentException e) {
432                Log.w("colsMap", e.getMessage());
433            }
434        }
435    }
436
437    private AvatarCache mAvatarCache;
438
439    /*
440     * Track avatars for each of the members of in the group chat.
441     */
442    class AvatarCache {
443        private static final int TOKEN_PHONE_LOOKUP = 101;
444        private static final int TOKEN_EMAIL_LOOKUP = 102;
445        private static final int TOKEN_CONTACT_INFO = 201;
446        private static final int TOKEN_PHOTO_DATA = 301;
447
448        //Projection used for the summary info in the header.
449        private final String[] COLUMNS = new String[] {
450                  Contacts._ID,
451                  Contacts.PHOTO_ID,
452                  // Other fields which we might want/need in the future (for example)
453//                Contacts.LOOKUP_KEY,
454//                Contacts.DISPLAY_NAME,
455//                Contacts.STARRED,
456//                Contacts.CONTACT_PRESENCE,
457//                Contacts.CONTACT_STATUS,
458//                Contacts.CONTACT_STATUS_TIMESTAMP,
459//                Contacts.CONTACT_STATUS_RES_PACKAGE,
460//                Contacts.CONTACT_STATUS_LABEL,
461        };
462        private final int PHOTO_ID = 1;
463
464        private final String[] PHONE_LOOKUP_PROJECTION = new String[] {
465            PhoneLookup._ID,
466            PhoneLookup.LOOKUP_KEY,
467        };
468        private static final int PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0;
469        private static final int PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1;
470
471        private final String[] EMAIL_LOOKUP_PROJECTION = new String[] {
472            RawContacts.CONTACT_ID,
473            Contacts.LOOKUP_KEY,
474        };
475        private static final int EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0;
476        private static final int EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1;
477
478
479        /*
480         * Map from mAddress to a blob of data which contains the contact id
481         * and the avatar.
482         */
483        HashMap<String, ContactData> mImageCache = new HashMap<String, ContactData>();
484
485        public class ContactData {
486            private String mAddress;
487            private long mContactId;
488            private Uri mContactUri;
489            private Drawable mPhoto;
490
491            ContactData(String address) {
492                mAddress = address;
493            }
494
495            public Drawable getAvatar() {
496                return mPhoto;
497            }
498
499            public Uri getContactUri() {
500                return mContactUri;
501            }
502
503            private boolean startInitialQuery() {
504                if (Mms.isPhoneNumber(mAddress)) {
505                    mQueryHandler.startQuery(
506                            TOKEN_PHONE_LOOKUP,
507                            this,
508                            Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(mAddress)),
509                            PHONE_LOOKUP_PROJECTION,
510                            null,
511                            null,
512                            null);
513                    return true;
514                } else if (Mms.isEmailAddress(mAddress)) {
515                    mQueryHandler.startQuery(
516                            TOKEN_EMAIL_LOOKUP,
517                            this,
518                            Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mAddress)),
519                            EMAIL_LOOKUP_PROJECTION,
520                            null,
521                            null,
522                            null);
523                    return true;
524                } else {
525                    return false;
526                }
527            }
528            /*
529             * Once we have the photo data load it into a drawable.
530             */
531            private boolean onPhotoDataLoaded(Cursor c) {
532                if (c == null || !c.moveToFirst()) return false;
533
534                try {
535                    byte[] photoData = c.getBlob(0);
536                    Bitmap b = BitmapFactory.decodeByteArray(photoData, 0, photoData.length, null);
537                    mPhoto = new BitmapDrawable(mContext.getResources(), b);
538                    return true;
539                } catch (Exception ex) {
540                    return false;
541                }
542            }
543
544            /*
545             * Once we have the contact info loaded take the photo id and query
546             * for the photo data.
547             */
548            private boolean onContactInfoLoaded(Cursor c) {
549                if (c == null || !c.moveToFirst()) return false;
550
551                long photoId = c.getLong(PHOTO_ID);
552                Uri contactUri  = ContentUris.withAppendedId(Data.CONTENT_URI, photoId);
553                mQueryHandler.startQuery(
554                        TOKEN_PHOTO_DATA,
555                        this,
556                        contactUri,
557                        new String[] { Photo.PHOTO },
558                        null,
559                        null,
560                        null);
561
562                return true;
563            }
564
565            /*
566             * Once we have the contact id loaded start the query for the
567             * contact information (which will give us the photo id).
568             */
569            private boolean onContactIdLoaded(Cursor c, int contactIdColumn, int lookupKeyColumn) {
570                if (c == null || !c.moveToFirst()) return false;
571
572                mContactId = c.getLong(contactIdColumn);
573                String lookupKey = c.getString(lookupKeyColumn);
574                mContactUri = Contacts.getLookupUri(mContactId, lookupKey);
575                mQueryHandler.startQuery(
576                        TOKEN_CONTACT_INFO,
577                        this,
578                        mContactUri,
579                        COLUMNS,
580                        null,
581                        null,
582                        null);
583                return true;
584            }
585
586            /*
587             * If for whatever reason we can't get the photo load teh
588             * default avatar.  NOTE that fasttrack tries to get fancy
589             * with various random images (upside down, etc.) we're not
590             * doing that here.
591             */
592            private void loadDefaultAvatar() {
593                try {
594                    if (mDefaultAvatarDrawable == null) {
595                        Bitmap b = BitmapFactory.decodeResource(mContext.getResources(),
596                                R.drawable.ic_contact_picture);
597                        mDefaultAvatarDrawable = new BitmapDrawable(mContext.getResources(), b);
598                    }
599                    mPhoto = mDefaultAvatarDrawable;
600                } catch (java.lang.OutOfMemoryError e) {
601                    Log.e(TAG, "loadDefaultAvatar: out of memory: ", e);
602                }
603            }
604
605        };
606
607        Drawable mDefaultAvatarDrawable = null;
608        AsyncQueryHandler mQueryHandler = new AsyncQueryHandler(mContext.getContentResolver()) {
609            @Override
610            protected void onQueryComplete(int token, Object cookieObject, Cursor cursor) {
611                super.onQueryComplete(token, cookieObject, cursor);
612
613                ContactData cookie = (ContactData) cookieObject;
614                switch (token) {
615                    case TOKEN_PHONE_LOOKUP: {
616                        if (!cookie.onContactIdLoaded(
617                                cursor,
618                                PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX,
619                                PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX)) {
620                            cookie.loadDefaultAvatar();
621                        }
622                        break;
623                    }
624                    case TOKEN_EMAIL_LOOKUP: {
625                        if (!cookie.onContactIdLoaded(
626                                cursor,
627                                EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX,
628                                EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX)) {
629                            cookie.loadDefaultAvatar();
630                        }
631                        break;
632                    }
633                    case TOKEN_CONTACT_INFO: {
634                        if (!cookie.onContactInfoLoaded(cursor)) {
635                            cookie.loadDefaultAvatar();
636                        }
637                        break;
638                    }
639                    case TOKEN_PHOTO_DATA: {
640                        if (!cookie.onPhotoDataLoaded(cursor)) {
641                            cookie.loadDefaultAvatar();
642                        } else {
643                            MessageListAdapter.this.notifyImageLoaded(cookie.mAddress);
644                        }
645                        break;
646                    }
647                    default:
648                        break;
649                }
650            }
651        };
652
653        public ContactData get(final String address) {
654            if (mImageCache.containsKey(address)) {
655                return mImageCache.get(address);
656            } else {
657                // Create the ContactData object and put it into the hashtable
658                // so that any subsequent requests for this same avatar do not kick
659                // off another query.
660                ContactData cookie = new ContactData(address);
661                mImageCache.put(address, cookie);
662                cookie.startInitialQuery();
663                cookie.loadDefaultAvatar();
664                return cookie;
665            }
666        }
667
668        public AvatarCache() {
669        }
670    };
671
672
673}
674