public static interface

MessageListAdapter.OnDataSetChangedListener

/*
 * Copyright (C) 2008 Esmertec AG.
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.mms.ui;

import com.android.mms.R;
import com.google.android.mms.MmsException;

import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler;
import android.provider.BaseColumns;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.PhoneLookup;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.StatusUpdates;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.Telephony.Mms;
import android.provider.Telephony.MmsSms;
import android.provider.Telephony.Sms;
import android.provider.Telephony.MmsSms.PendingMessages;
import android.provider.Telephony.Sms.Conversations;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Config;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.ListView;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * The back-end data adapter of a message list.
 */
public class MessageListAdapter extends CursorAdapter {
    private static final String TAG = "MessageListAdapter";
    private static final boolean DEBUG = false;
    private static final boolean LOCAL_LOGV = Config.LOGV && DEBUG;

    static final String[] PROJECTION = new String[] {
        // TODO: should move this symbol into com.android.mms.telephony.Telephony.
        MmsSms.TYPE_DISCRIMINATOR_COLUMN,
        BaseColumns._ID,
        Conversations.THREAD_ID,
        // For SMS
        Sms.ADDRESS,
        Sms.BODY,
        Sms.DATE,
        Sms.READ,
        Sms.TYPE,
        Sms.STATUS,
        Sms.LOCKED,
        Sms.ERROR_CODE,
        // For MMS
        Mms.SUBJECT,
        Mms.SUBJECT_CHARSET,
        Mms.DATE,
        Mms.READ,
        Mms.MESSAGE_TYPE,
        Mms.MESSAGE_BOX,
        Mms.DELIVERY_REPORT,
        Mms.READ_REPORT,
        PendingMessages.ERROR_TYPE,
        Mms.LOCKED
    };

    // The indexes of the default columns which must be consistent
    // with above PROJECTION.
    static final int COLUMN_MSG_TYPE            = 0;
    static final int COLUMN_ID                  = 1;
    static final int COLUMN_THREAD_ID           = 2;
    static final int COLUMN_SMS_ADDRESS         = 3;
    static final int COLUMN_SMS_BODY            = 4;
    static final int COLUMN_SMS_DATE            = 5;
    static final int COLUMN_SMS_READ            = 6;
    static final int COLUMN_SMS_TYPE            = 7;
    static final int COLUMN_SMS_STATUS          = 8;
    static final int COLUMN_SMS_LOCKED          = 9;
    static final int COLUMN_SMS_ERROR_CODE      = 10;
    static final int COLUMN_MMS_SUBJECT         = 11;
    static final int COLUMN_MMS_SUBJECT_CHARSET = 12;
    static final int COLUMN_MMS_DATE            = 13;
    static final int COLUMN_MMS_READ            = 14;
    static final int COLUMN_MMS_MESSAGE_TYPE    = 15;
    static final int COLUMN_MMS_MESSAGE_BOX     = 16;
    static final int COLUMN_MMS_DELIVERY_REPORT = 17;
    static final int COLUMN_MMS_READ_REPORT     = 18;
    static final int COLUMN_MMS_ERROR_TYPE      = 19;
    static final int COLUMN_MMS_LOCKED          = 20;

    private static final int CACHE_SIZE         = 50;

    protected LayoutInflater mInflater;
    private final ListView mListView;
    private final LinkedHashMap<Long, MessageItem> mMessageItemCache;
    private final ColumnsMap mColumnsMap;
    private OnDataSetChangedListener mOnDataSetChangedListener;
    private Handler mMsgListItemHandler;
    private Pattern mHighlight;
    private Context mContext;

    private HashMap<String, HashSet<MessageListItem>> mAddressToMessageListItems
        = new HashMap<String, HashSet<MessageListItem>>();

    public MessageListAdapter(
            Context context, Cursor c, ListView listView,
            boolean useDefaultColumnsMap, Pattern highlight) {
        super(context, c, false /* auto-requery */);
        mContext = context;
        mHighlight = highlight;

        mInflater = (LayoutInflater) context.getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);
        mListView = listView;
        mMessageItemCache = new LinkedHashMap<Long, MessageItem>(
                    10, 1.0f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size() > CACHE_SIZE;
            }
        };

        if (useDefaultColumnsMap) {
            mColumnsMap = new ColumnsMap();
        } else {
            mColumnsMap = new ColumnsMap(c);
        }

        mAvatarCache = new AvatarCache();
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        if (view instanceof MessageListItem) {
            String type = cursor.getString(mColumnsMap.mColumnMsgType);
            long msgId = cursor.getLong(mColumnsMap.mColumnMsgId);

            MessageItem msgItem = getCachedMessageItem(type, msgId, cursor);
            if (msgItem != null) {
                MessageListItem mli = (MessageListItem) view;

                // Remove previous item from mapping
                MessageItem oldMessageItem = mli.getMessageItem();
                if (oldMessageItem != null) {
                    String oldAddress = oldMessageItem.mAddress;
                    if (oldAddress != null) {
                        HashSet<MessageListItem> set = mAddressToMessageListItems.get(oldAddress);
                        if (set != null) {
                            set.remove(mli);
                        }
                    }
                }

                mli.bind(mAvatarCache, msgItem);
                mli.setMsgListItemHandler(mMsgListItemHandler);

                // Add current item to mapping

                String addr;
                if (!Sms.isOutgoingFolder(msgItem.mBoxId)) {
                    addr = msgItem.mAddress;
                } else {
                    addr = MessageUtils.getLocalNumber();
                }

                HashSet<MessageListItem> set = mAddressToMessageListItems.get(addr);
                if (set == null) {
                    set = new HashSet<MessageListItem>();
                    mAddressToMessageListItems.put(addr, set);
                }
                set.add(mli);
            }
        }
    }

    public interface OnDataSetChangedListener {
        void onDataSetChanged(MessageListAdapter adapter);
        void onContentChanged(MessageListAdapter adapter);
    }

    public void setOnDataSetChangedListener(OnDataSetChangedListener l) {
        mOnDataSetChangedListener = l;
    }

    public void setMsgListItemHandler(Handler handler) {
        mMsgListItemHandler = handler;
    }

    public void notifyImageLoaded(String address) {
        HashSet<MessageListItem> set = mAddressToMessageListItems.get(address);
        if (set != null) {
            for (MessageListItem mli : set) {
                mli.bind(mAvatarCache, mli.getMessageItem());
            }
        }
    }

    @Override
    public void notifyDataSetChanged() {
        super.notifyDataSetChanged();
        if (LOCAL_LOGV) {
            Log.v(TAG, "MessageListAdapter.notifyDataSetChanged().");
        }

        mListView.setSelection(mListView.getCount());
        mMessageItemCache.clear();

        if (mOnDataSetChangedListener != null) {
            mOnDataSetChangedListener.onDataSetChanged(this);
        }
    }

    @Override
    protected void onContentChanged() {
        if (getCursor() != null && !getCursor().isClosed()) {
            if (mOnDataSetChangedListener != null) {
                mOnDataSetChangedListener.onContentChanged(this);
            }
        }
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        return mInflater.inflate(R.layout.message_list_item, parent, false);
    }

    public MessageItem getCachedMessageItem(String type, long msgId, Cursor c) {
        MessageItem item = mMessageItemCache.get(getKey(type, msgId));
        if (item == null && c != null && isCursorValid(c)) {
            try {
                item = new MessageItem(mContext, type, c, mColumnsMap, mHighlight);
                mMessageItemCache.put(getKey(item.mType, item.mMsgId), item);
            } catch (MmsException e) {
                Log.e(TAG, "getCachedMessageItem: ", e);
            }
        }
        return item;
    }

    private boolean isCursorValid(Cursor cursor) {
        // Check whether the cursor is valid or not.
        if (cursor.isClosed() || cursor.isBeforeFirst() || cursor.isAfterLast()) {
            return false;
        }
        return true;
    }

    private static long getKey(String type, long id) {
        if (type.equals("mms")) {
            return -id;
        } else {
            return id;
        }
    }

    public static class ColumnsMap {
        public int mColumnMsgType;
        public int mColumnMsgId;
        public int mColumnSmsAddress;
        public int mColumnSmsBody;
        public int mColumnSmsDate;
        public int mColumnSmsRead;
        public int mColumnSmsType;
        public int mColumnSmsStatus;
        public int mColumnSmsLocked;
        public int mColumnSmsErrorCode;
        public int mColumnMmsSubject;
        public int mColumnMmsSubjectCharset;
        public int mColumnMmsDate;
        public int mColumnMmsRead;
        public int mColumnMmsMessageType;
        public int mColumnMmsMessageBox;
        public int mColumnMmsDeliveryReport;
        public int mColumnMmsReadReport;
        public int mColumnMmsErrorType;
        public int mColumnMmsLocked;

        public ColumnsMap() {
            mColumnMsgType            = COLUMN_MSG_TYPE;
            mColumnMsgId              = COLUMN_ID;
            mColumnSmsAddress         = COLUMN_SMS_ADDRESS;
            mColumnSmsBody            = COLUMN_SMS_BODY;
            mColumnSmsDate            = COLUMN_SMS_DATE;
            mColumnSmsType            = COLUMN_SMS_TYPE;
            mColumnSmsStatus          = COLUMN_SMS_STATUS;
            mColumnSmsLocked          = COLUMN_SMS_LOCKED;
            mColumnSmsErrorCode       = COLUMN_SMS_ERROR_CODE;
            mColumnMmsSubject         = COLUMN_MMS_SUBJECT;
            mColumnMmsSubjectCharset  = COLUMN_MMS_SUBJECT_CHARSET;
            mColumnMmsMessageType     = COLUMN_MMS_MESSAGE_TYPE;
            mColumnMmsMessageBox      = COLUMN_MMS_MESSAGE_BOX;
            mColumnMmsDeliveryReport  = COLUMN_MMS_DELIVERY_REPORT;
            mColumnMmsReadReport      = COLUMN_MMS_READ_REPORT;
            mColumnMmsErrorType       = COLUMN_MMS_ERROR_TYPE;
            mColumnMmsLocked          = COLUMN_MMS_LOCKED;
        }

        public ColumnsMap(Cursor cursor) {
            // Ignore all 'not found' exceptions since the custom columns
            // may be just a subset of the default columns.
            try {
                mColumnMsgType = cursor.getColumnIndexOrThrow(
                        MmsSms.TYPE_DISCRIMINATOR_COLUMN);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnMsgId = cursor.getColumnIndexOrThrow(BaseColumns._ID);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnSmsAddress = cursor.getColumnIndexOrThrow(Sms.ADDRESS);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnSmsBody = cursor.getColumnIndexOrThrow(Sms.BODY);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnSmsDate = cursor.getColumnIndexOrThrow(Sms.DATE);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnSmsType = cursor.getColumnIndexOrThrow(Sms.TYPE);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnSmsStatus = cursor.getColumnIndexOrThrow(Sms.STATUS);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnSmsLocked = cursor.getColumnIndexOrThrow(Sms.LOCKED);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnSmsErrorCode = cursor.getColumnIndexOrThrow(Sms.ERROR_CODE);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnMmsSubject = cursor.getColumnIndexOrThrow(Mms.SUBJECT);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnMmsSubjectCharset = cursor.getColumnIndexOrThrow(Mms.SUBJECT_CHARSET);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnMmsMessageType = cursor.getColumnIndexOrThrow(Mms.MESSAGE_TYPE);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnMmsMessageBox = cursor.getColumnIndexOrThrow(Mms.MESSAGE_BOX);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnMmsDeliveryReport = cursor.getColumnIndexOrThrow(Mms.DELIVERY_REPORT);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnMmsReadReport = cursor.getColumnIndexOrThrow(Mms.READ_REPORT);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnMmsErrorType = cursor.getColumnIndexOrThrow(PendingMessages.ERROR_TYPE);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }

            try {
                mColumnMmsLocked = cursor.getColumnIndexOrThrow(Mms.LOCKED);
            } catch (IllegalArgumentException e) {
                Log.w("colsMap", e.getMessage());
            }
        }
    }

    private AvatarCache mAvatarCache;

    /*
     * Track avatars for each of the members of in the group chat.
     */
    class AvatarCache {
        private static final int TOKEN_PHONE_LOOKUP = 101;
        private static final int TOKEN_EMAIL_LOOKUP = 102;
        private static final int TOKEN_CONTACT_INFO = 201;
        private static final int TOKEN_PHOTO_DATA = 301;

        //Projection used for the summary info in the header.
        private final String[] COLUMNS = new String[] {
                  Contacts._ID,
                  Contacts.PHOTO_ID,
                  // Other fields which we might want/need in the future (for example)
//                Contacts.LOOKUP_KEY,
//                Contacts.DISPLAY_NAME,
//                Contacts.STARRED,
//                Contacts.CONTACT_PRESENCE,
//                Contacts.CONTACT_STATUS,
//                Contacts.CONTACT_STATUS_TIMESTAMP,
//                Contacts.CONTACT_STATUS_RES_PACKAGE,
//                Contacts.CONTACT_STATUS_LABEL,
        };
        private final int PHOTO_ID = 1;

        private final String[] PHONE_LOOKUP_PROJECTION = new String[] {
            PhoneLookup._ID,
            PhoneLookup.LOOKUP_KEY,
        };
        private static final int PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0;
        private static final int PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1;

        private final String[] EMAIL_LOOKUP_PROJECTION = new String[] {
            RawContacts.CONTACT_ID,
            Contacts.LOOKUP_KEY,
        };
        private static final int EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0;
        private static final int EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1;


        /*
         * Map from mAddress to a blob of data which contains the contact id
         * and the avatar.
         */
        HashMap<String, ContactData> mImageCache = new HashMap<String, ContactData>();

        public class ContactData {
            private String mAddress;
            private long mContactId;
            private Uri mContactUri;
            private Drawable mPhoto;

            ContactData(String address) {
                mAddress = address;
            }

            public Drawable getAvatar() {
                return mPhoto;
            }

            public Uri getContactUri() {
                return mContactUri;
            }

            private boolean startInitialQuery() {
                if (Mms.isPhoneNumber(mAddress)) {
                    mQueryHandler.startQuery(
                            TOKEN_PHONE_LOOKUP,
                            this,
                            Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(mAddress)),
                            PHONE_LOOKUP_PROJECTION,
                            null,
                            null,
                            null);
                    return true;
                } else if (Mms.isEmailAddress(mAddress)) {
                    mQueryHandler.startQuery(
                            TOKEN_EMAIL_LOOKUP,
                            this,
                            Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mAddress)),
                            EMAIL_LOOKUP_PROJECTION,
                            null,
                            null,
                            null);
                    return true;
                } else {
                    return false;
                }
            }
            /*
             * Once we have the photo data load it into a drawable.
             */
            private boolean onPhotoDataLoaded(Cursor c) {
                if (c == null || !c.moveToFirst()) return false;

                try {
                    byte[] photoData = c.getBlob(0);
                    Bitmap b = BitmapFactory.decodeByteArray(photoData, 0, photoData.length, null);
                    mPhoto = new BitmapDrawable(mContext.getResources(), b);
                    return true;
                } catch (Exception ex) {
                    return false;
                }
            }

            /*
             * Once we have the contact info loaded take the photo id and query
             * for the photo data.
             */
            private boolean onContactInfoLoaded(Cursor c) {
                if (c == null || !c.moveToFirst()) return false;

                long photoId = c.getLong(PHOTO_ID);
                Uri contactUri  = ContentUris.withAppendedId(Data.CONTENT_URI, photoId);
                mQueryHandler.startQuery(
                        TOKEN_PHOTO_DATA,
                        this,
                        contactUri,
                        new String[] { Photo.PHOTO },
                        null,
                        null,
                        null);

                return true;
            }

            /*
             * Once we have the contact id loaded start the query for the
             * contact information (which will give us the photo id).
             */
            private boolean onContactIdLoaded(Cursor c, int contactIdColumn, int lookupKeyColumn) {
                if (c == null || !c.moveToFirst()) return false;

                mContactId = c.getLong(contactIdColumn);
                String lookupKey = c.getString(lookupKeyColumn);
                mContactUri = Contacts.getLookupUri(mContactId, lookupKey);
                mQueryHandler.startQuery(
                        TOKEN_CONTACT_INFO,
                        this,
                        mContactUri,
                        COLUMNS,
                        null,
                        null,
                        null);
                return true;
            }

            /*
             * If for whatever reason we can't get the photo load teh
             * default avatar.  NOTE that fasttrack tries to get fancy
             * with various random images (upside down, etc.) we're not
             * doing that here.
             */
            private void loadDefaultAvatar() {
                try {
                    if (mDefaultAvatarDrawable == null) {
                        Bitmap b = BitmapFactory.decodeResource(mContext.getResources(),
                                R.drawable.ic_contact_picture);
                        mDefaultAvatarDrawable = new BitmapDrawable(mContext.getResources(), b);
                    }
                    mPhoto = mDefaultAvatarDrawable;
                } catch (java.lang.OutOfMemoryError e) {
                    Log.e(TAG, "loadDefaultAvatar: out of memory: ", e);
                }
            }

        };

        Drawable mDefaultAvatarDrawable = null;
        AsyncQueryHandler mQueryHandler = new AsyncQueryHandler(mContext.getContentResolver()) {
            @Override
            protected void onQueryComplete(int token, Object cookieObject, Cursor cursor) {
                super.onQueryComplete(token, cookieObject, cursor);

                ContactData cookie = (ContactData) cookieObject;
                switch (token) {
                    case TOKEN_PHONE_LOOKUP: {
                        if (!cookie.onContactIdLoaded(
                                cursor,
                                PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX,
                                PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX)) {
                            cookie.loadDefaultAvatar();
                        }
                        break;
                    }
                    case TOKEN_EMAIL_LOOKUP: {
                        if (!cookie.onContactIdLoaded(
                                cursor,
                                EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX,
                                EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX)) {
                            cookie.loadDefaultAvatar();
                        }
                        break;
                    }
                    case TOKEN_CONTACT_INFO: {
                        if (!cookie.onContactInfoLoaded(cursor)) {
                            cookie.loadDefaultAvatar();
                        }
                        break;
                    }
                    case TOKEN_PHOTO_DATA: {
                        if (!cookie.onPhotoDataLoaded(cursor)) {
                            cookie.loadDefaultAvatar();
                        } else {
                            MessageListAdapter.this.notifyImageLoaded(cookie.mAddress);
                        }
                        break;
                    }
                    default:
                        break;
                }
            }
        };

        public ContactData get(final String address) {
            if (mImageCache.containsKey(address)) {
                return mImageCache.get(address);
            } else {
                // Create the ContactData object and put it into the hashtable
                // so that any subsequent requests for this same avatar do not kick
                // off another query.
                ContactData cookie = new ContactData(address);
                mImageCache.put(address, cookie);
                cookie.startInitialQuery();
                cookie.loadDefaultAvatar();
                return cookie;
            }
        }

        public AvatarCache() {
        }
    };


}