blob: 222df6372aefc14de7e35c9f2e1306b222610cd3 [file] [log] [blame]
/*
* 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, e.getMessage());
}
}
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() {
}
};
}