Home | History | Annotate | Download | only in ui
      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 
     18 package com.android.mms.ui;
     19 
     20 import java.util.regex.Pattern;
     21 
     22 import android.content.Context;
     23 import android.database.Cursor;
     24 import android.os.Handler;
     25 import android.provider.BaseColumns;
     26 import android.provider.Telephony.Mms;
     27 import android.provider.Telephony.MmsSms;
     28 import android.provider.Telephony.MmsSms.PendingMessages;
     29 import android.provider.Telephony.Sms;
     30 import android.provider.Telephony.Sms.Conversations;
     31 import android.provider.Telephony.TextBasedSmsColumns;
     32 import android.util.Log;
     33 import android.util.LruCache;
     34 import android.view.LayoutInflater;
     35 import android.view.View;
     36 import android.view.ViewGroup;
     37 import android.widget.AbsListView;
     38 import android.widget.CursorAdapter;
     39 import android.widget.ListView;
     40 
     41 import com.android.mms.R;
     42 import com.google.android.mms.MmsException;
     43 
     44 /**
     45  * The back-end data adapter of a message list.
     46  */
     47 public class MessageListAdapter extends CursorAdapter {
     48     private static final String TAG = "MessageListAdapter";
     49     private static final boolean LOCAL_LOGV = false;
     50 
     51     static final String[] PROJECTION = new String[] {
     52         // TODO: should move this symbol into com.android.mms.telephony.Telephony.
     53         MmsSms.TYPE_DISCRIMINATOR_COLUMN,
     54         BaseColumns._ID,
     55         Conversations.THREAD_ID,
     56         // For SMS
     57         Sms.ADDRESS,
     58         Sms.BODY,
     59         Sms.DATE,
     60         Sms.DATE_SENT,
     61         Sms.READ,
     62         Sms.TYPE,
     63         Sms.STATUS,
     64         Sms.LOCKED,
     65         Sms.ERROR_CODE,
     66         // For MMS
     67         Mms.SUBJECT,
     68         Mms.SUBJECT_CHARSET,
     69         Mms.DATE,
     70         Mms.DATE_SENT,
     71         Mms.READ,
     72         Mms.MESSAGE_TYPE,
     73         Mms.MESSAGE_BOX,
     74         Mms.DELIVERY_REPORT,
     75         Mms.READ_REPORT,
     76         PendingMessages.ERROR_TYPE,
     77         Mms.LOCKED,
     78         Mms.STATUS,
     79         Mms.TEXT_ONLY
     80     };
     81 
     82     // The indexes of the default columns which must be consistent
     83     // with above PROJECTION.
     84     static final int COLUMN_MSG_TYPE            = 0;
     85     static final int COLUMN_ID                  = 1;
     86     static final int COLUMN_THREAD_ID           = 2;
     87     static final int COLUMN_SMS_ADDRESS         = 3;
     88     static final int COLUMN_SMS_BODY            = 4;
     89     static final int COLUMN_SMS_DATE            = 5;
     90     static final int COLUMN_SMS_DATE_SENT       = 6;
     91     static final int COLUMN_SMS_READ            = 7;
     92     static final int COLUMN_SMS_TYPE            = 8;
     93     static final int COLUMN_SMS_STATUS          = 9;
     94     static final int COLUMN_SMS_LOCKED          = 10;
     95     static final int COLUMN_SMS_ERROR_CODE      = 11;
     96     static final int COLUMN_MMS_SUBJECT         = 12;
     97     static final int COLUMN_MMS_SUBJECT_CHARSET = 13;
     98     static final int COLUMN_MMS_DATE            = 14;
     99     static final int COLUMN_MMS_DATE_SENT       = 15;
    100     static final int COLUMN_MMS_READ            = 16;
    101     static final int COLUMN_MMS_MESSAGE_TYPE    = 17;
    102     static final int COLUMN_MMS_MESSAGE_BOX     = 18;
    103     static final int COLUMN_MMS_DELIVERY_REPORT = 19;
    104     static final int COLUMN_MMS_READ_REPORT     = 20;
    105     static final int COLUMN_MMS_ERROR_TYPE      = 21;
    106     static final int COLUMN_MMS_LOCKED          = 22;
    107     static final int COLUMN_MMS_STATUS          = 23;
    108     static final int COLUMN_MMS_TEXT_ONLY       = 24;
    109 
    110     private static final int CACHE_SIZE         = 50;
    111 
    112     public static final int INCOMING_ITEM_TYPE_SMS = 0;
    113     public static final int OUTGOING_ITEM_TYPE_SMS = 1;
    114     public static final int INCOMING_ITEM_TYPE_MMS = 2;
    115     public static final int OUTGOING_ITEM_TYPE_MMS = 3;
    116 
    117     protected LayoutInflater mInflater;
    118     private final MessageItemCache mMessageItemCache;
    119     private final ColumnsMap mColumnsMap;
    120     private OnDataSetChangedListener mOnDataSetChangedListener;
    121     private Handler mMsgListItemHandler;
    122     private Pattern mHighlight;
    123     private Context mContext;
    124     private boolean mIsGroupConversation;
    125 
    126     public MessageListAdapter(
    127             Context context, Cursor c, ListView listView,
    128             boolean useDefaultColumnsMap, Pattern highlight) {
    129         super(context, c, FLAG_REGISTER_CONTENT_OBSERVER);
    130         mContext = context;
    131         mHighlight = highlight;
    132 
    133         mInflater = (LayoutInflater) context.getSystemService(
    134                 Context.LAYOUT_INFLATER_SERVICE);
    135         mMessageItemCache = new MessageItemCache(CACHE_SIZE);
    136 
    137         if (useDefaultColumnsMap) {
    138             mColumnsMap = new ColumnsMap();
    139         } else {
    140             mColumnsMap = new ColumnsMap(c);
    141         }
    142 
    143         listView.setRecyclerListener(new AbsListView.RecyclerListener() {
    144             @Override
    145             public void onMovedToScrapHeap(View view) {
    146                 if (view instanceof MessageListItem) {
    147                     MessageListItem mli = (MessageListItem) view;
    148                     // Clear references to resources
    149                     mli.unbind();
    150                 }
    151             }
    152         });
    153     }
    154 
    155     @Override
    156     public void bindView(View view, Context context, Cursor cursor) {
    157         if (view instanceof MessageListItem) {
    158             String type = cursor.getString(mColumnsMap.mColumnMsgType);
    159             long msgId = cursor.getLong(mColumnsMap.mColumnMsgId);
    160 
    161             MessageItem msgItem = getCachedMessageItem(type, msgId, cursor);
    162             if (msgItem != null) {
    163                 MessageListItem mli = (MessageListItem) view;
    164                 int position = cursor.getPosition();
    165                 mli.bind(msgItem, mIsGroupConversation, position);
    166                 mli.setMsgListItemHandler(mMsgListItemHandler);
    167             }
    168         }
    169     }
    170 
    171     public interface OnDataSetChangedListener {
    172         void onDataSetChanged(MessageListAdapter adapter);
    173         void onContentChanged(MessageListAdapter adapter);
    174     }
    175 
    176     public void setOnDataSetChangedListener(OnDataSetChangedListener l) {
    177         mOnDataSetChangedListener = l;
    178     }
    179 
    180     public void setMsgListItemHandler(Handler handler) {
    181         mMsgListItemHandler = handler;
    182     }
    183 
    184     public void setIsGroupConversation(boolean isGroup) {
    185         mIsGroupConversation = isGroup;
    186     }
    187 
    188     public void cancelBackgroundLoading() {
    189         mMessageItemCache.evictAll();   // causes entryRemoved to be called for each MessageItem
    190                                         // in the cache which causes us to cancel loading of
    191                                         // background pdu's and images.
    192     }
    193 
    194     @Override
    195     public void notifyDataSetChanged() {
    196         super.notifyDataSetChanged();
    197         if (LOCAL_LOGV) {
    198             Log.v(TAG, "MessageListAdapter.notifyDataSetChanged().");
    199         }
    200 
    201         mMessageItemCache.evictAll();
    202 
    203         if (mOnDataSetChangedListener != null) {
    204             mOnDataSetChangedListener.onDataSetChanged(this);
    205         }
    206     }
    207 
    208     @Override
    209     protected void onContentChanged() {
    210         if (getCursor() != null && !getCursor().isClosed()) {
    211             if (mOnDataSetChangedListener != null) {
    212                 mOnDataSetChangedListener.onContentChanged(this);
    213             }
    214         }
    215     }
    216 
    217     @Override
    218     public View newView(Context context, Cursor cursor, ViewGroup parent) {
    219         int boxType = getItemViewType(cursor);
    220         View view = mInflater.inflate((boxType == INCOMING_ITEM_TYPE_SMS ||
    221                 boxType == INCOMING_ITEM_TYPE_MMS) ?
    222                         R.layout.message_list_item_recv : R.layout.message_list_item_send,
    223                         parent, false);
    224         if (boxType == INCOMING_ITEM_TYPE_MMS || boxType == OUTGOING_ITEM_TYPE_MMS) {
    225             // We've got an mms item, pre-inflate the mms portion of the view
    226             view.findViewById(R.id.mms_layout_view_stub).setVisibility(View.VISIBLE);
    227         }
    228         return view;
    229     }
    230 
    231     public MessageItem getCachedMessageItem(String type, long msgId, Cursor c) {
    232         MessageItem item = mMessageItemCache.get(getKey(type, msgId));
    233         if (item == null && c != null && isCursorValid(c)) {
    234             try {
    235                 item = new MessageItem(mContext, type, c, mColumnsMap, mHighlight);
    236                 mMessageItemCache.put(getKey(item.mType, item.mMsgId), item);
    237             } catch (MmsException e) {
    238                 Log.e(TAG, "getCachedMessageItem: ", e);
    239             }
    240         }
    241         return item;
    242     }
    243 
    244     private boolean isCursorValid(Cursor cursor) {
    245         // Check whether the cursor is valid or not.
    246         if (cursor == null || cursor.isClosed() || cursor.isBeforeFirst() || cursor.isAfterLast()) {
    247             return false;
    248         }
    249         return true;
    250     }
    251 
    252     private static long getKey(String type, long id) {
    253         if (type.equals("mms")) {
    254             return -id;
    255         } else {
    256             return id;
    257         }
    258     }
    259 
    260     @Override
    261     public boolean areAllItemsEnabled() {
    262         return true;
    263     }
    264 
    265     /* MessageListAdapter says that it contains four types of views. Really, it just contains
    266      * a single type, a MessageListItem. Depending upon whether the message is an incoming or
    267      * outgoing message, the avatar and text and other items are laid out either left or right
    268      * justified. That works fine for everything but the message text. When views are recycled,
    269      * there's a greater than zero chance that the right-justified text on outgoing messages
    270      * will remain left-justified. The best solution at this point is to tell the adapter we've
    271      * got two different types of views. That way we won't recycle views between the two types.
    272      * @see android.widget.BaseAdapter#getViewTypeCount()
    273      */
    274     @Override
    275     public int getViewTypeCount() {
    276         return 4;   // Incoming and outgoing messages, both sms and mms
    277     }
    278 
    279     @Override
    280     public int getItemViewType(int position) {
    281         Cursor cursor = (Cursor)getItem(position);
    282         return getItemViewType(cursor);
    283     }
    284 
    285     private int getItemViewType(Cursor cursor) {
    286         String type = cursor.getString(mColumnsMap.mColumnMsgType);
    287         int boxId;
    288         if ("sms".equals(type)) {
    289             boxId = cursor.getInt(mColumnsMap.mColumnSmsType);
    290             // Note that messages from the SIM card all have a boxId of zero.
    291             return (boxId == TextBasedSmsColumns.MESSAGE_TYPE_INBOX ||
    292                     boxId == TextBasedSmsColumns.MESSAGE_TYPE_ALL) ?
    293                     INCOMING_ITEM_TYPE_SMS : OUTGOING_ITEM_TYPE_SMS;
    294         } else {
    295             boxId = cursor.getInt(mColumnsMap.mColumnMmsMessageBox);
    296             // Note that messages from the SIM card all have a boxId of zero: Mms.MESSAGE_BOX_ALL
    297             return (boxId == Mms.MESSAGE_BOX_INBOX || boxId == Mms.MESSAGE_BOX_ALL) ?
    298                     INCOMING_ITEM_TYPE_MMS : OUTGOING_ITEM_TYPE_MMS;
    299         }
    300     }
    301 
    302     public Cursor getCursorForItem(MessageItem item) {
    303         Cursor cursor = getCursor();
    304         if (isCursorValid(cursor)) {
    305             if (cursor.moveToFirst()) {
    306                 do {
    307                     long id = cursor.getLong(mRowIDColumn);
    308                     if (id == item.mMsgId) {
    309                         return cursor;
    310                     }
    311                 } while (cursor.moveToNext());
    312             }
    313         }
    314         return null;
    315     }
    316 
    317     public static class ColumnsMap {
    318         public int mColumnMsgType;
    319         public int mColumnMsgId;
    320         public int mColumnSmsAddress;
    321         public int mColumnSmsBody;
    322         public int mColumnSmsDate;
    323         public int mColumnSmsDateSent;
    324         public int mColumnSmsRead;
    325         public int mColumnSmsType;
    326         public int mColumnSmsStatus;
    327         public int mColumnSmsLocked;
    328         public int mColumnSmsErrorCode;
    329         public int mColumnMmsSubject;
    330         public int mColumnMmsSubjectCharset;
    331         public int mColumnMmsDate;
    332         public int mColumnMmsDateSent;
    333         public int mColumnMmsRead;
    334         public int mColumnMmsMessageType;
    335         public int mColumnMmsMessageBox;
    336         public int mColumnMmsDeliveryReport;
    337         public int mColumnMmsReadReport;
    338         public int mColumnMmsErrorType;
    339         public int mColumnMmsLocked;
    340         public int mColumnMmsStatus;
    341         public int mColumnMmsTextOnly;
    342 
    343         public ColumnsMap() {
    344             mColumnMsgType            = COLUMN_MSG_TYPE;
    345             mColumnMsgId              = COLUMN_ID;
    346             mColumnSmsAddress         = COLUMN_SMS_ADDRESS;
    347             mColumnSmsBody            = COLUMN_SMS_BODY;
    348             mColumnSmsDate            = COLUMN_SMS_DATE;
    349             mColumnSmsDateSent        = COLUMN_SMS_DATE_SENT;
    350             mColumnSmsType            = COLUMN_SMS_TYPE;
    351             mColumnSmsStatus          = COLUMN_SMS_STATUS;
    352             mColumnSmsLocked          = COLUMN_SMS_LOCKED;
    353             mColumnSmsErrorCode       = COLUMN_SMS_ERROR_CODE;
    354             mColumnMmsSubject         = COLUMN_MMS_SUBJECT;
    355             mColumnMmsSubjectCharset  = COLUMN_MMS_SUBJECT_CHARSET;
    356             mColumnMmsMessageType     = COLUMN_MMS_MESSAGE_TYPE;
    357             mColumnMmsMessageBox      = COLUMN_MMS_MESSAGE_BOX;
    358             mColumnMmsDeliveryReport  = COLUMN_MMS_DELIVERY_REPORT;
    359             mColumnMmsReadReport      = COLUMN_MMS_READ_REPORT;
    360             mColumnMmsErrorType       = COLUMN_MMS_ERROR_TYPE;
    361             mColumnMmsLocked          = COLUMN_MMS_LOCKED;
    362             mColumnMmsStatus          = COLUMN_MMS_STATUS;
    363             mColumnMmsTextOnly        = COLUMN_MMS_TEXT_ONLY;
    364         }
    365 
    366         public ColumnsMap(Cursor cursor) {
    367             // Ignore all 'not found' exceptions since the custom columns
    368             // may be just a subset of the default columns.
    369             try {
    370                 mColumnMsgType = cursor.getColumnIndexOrThrow(
    371                         MmsSms.TYPE_DISCRIMINATOR_COLUMN);
    372             } catch (IllegalArgumentException e) {
    373                 Log.w("colsMap", e.getMessage());
    374             }
    375 
    376             try {
    377                 mColumnMsgId = cursor.getColumnIndexOrThrow(BaseColumns._ID);
    378             } catch (IllegalArgumentException e) {
    379                 Log.w("colsMap", e.getMessage());
    380             }
    381 
    382             try {
    383                 mColumnSmsAddress = cursor.getColumnIndexOrThrow(Sms.ADDRESS);
    384             } catch (IllegalArgumentException e) {
    385                 Log.w("colsMap", e.getMessage());
    386             }
    387 
    388             try {
    389                 mColumnSmsBody = cursor.getColumnIndexOrThrow(Sms.BODY);
    390             } catch (IllegalArgumentException e) {
    391                 Log.w("colsMap", e.getMessage());
    392             }
    393 
    394             try {
    395                 mColumnSmsDate = cursor.getColumnIndexOrThrow(Sms.DATE);
    396             } catch (IllegalArgumentException e) {
    397                 Log.w("colsMap", e.getMessage());
    398             }
    399 
    400             try {
    401                 mColumnSmsDateSent = cursor.getColumnIndexOrThrow(Sms.DATE_SENT);
    402             } catch (IllegalArgumentException e) {
    403                 Log.w("colsMap", e.getMessage());
    404             }
    405 
    406             try {
    407                 mColumnSmsType = cursor.getColumnIndexOrThrow(Sms.TYPE);
    408             } catch (IllegalArgumentException e) {
    409                 Log.w("colsMap", e.getMessage());
    410             }
    411 
    412             try {
    413                 mColumnSmsStatus = cursor.getColumnIndexOrThrow(Sms.STATUS);
    414             } catch (IllegalArgumentException e) {
    415                 Log.w("colsMap", e.getMessage());
    416             }
    417 
    418             try {
    419                 mColumnSmsLocked = cursor.getColumnIndexOrThrow(Sms.LOCKED);
    420             } catch (IllegalArgumentException e) {
    421                 Log.w("colsMap", e.getMessage());
    422             }
    423 
    424             try {
    425                 mColumnSmsErrorCode = cursor.getColumnIndexOrThrow(Sms.ERROR_CODE);
    426             } catch (IllegalArgumentException e) {
    427                 Log.w("colsMap", e.getMessage());
    428             }
    429 
    430             try {
    431                 mColumnMmsSubject = cursor.getColumnIndexOrThrow(Mms.SUBJECT);
    432             } catch (IllegalArgumentException e) {
    433                 Log.w("colsMap", e.getMessage());
    434             }
    435 
    436             try {
    437                 mColumnMmsSubjectCharset = cursor.getColumnIndexOrThrow(Mms.SUBJECT_CHARSET);
    438             } catch (IllegalArgumentException e) {
    439                 Log.w("colsMap", e.getMessage());
    440             }
    441 
    442             try {
    443                 mColumnMmsMessageType = cursor.getColumnIndexOrThrow(Mms.MESSAGE_TYPE);
    444             } catch (IllegalArgumentException e) {
    445                 Log.w("colsMap", e.getMessage());
    446             }
    447 
    448             try {
    449                 mColumnMmsMessageBox = cursor.getColumnIndexOrThrow(Mms.MESSAGE_BOX);
    450             } catch (IllegalArgumentException e) {
    451                 Log.w("colsMap", e.getMessage());
    452             }
    453 
    454             try {
    455                 mColumnMmsDeliveryReport = cursor.getColumnIndexOrThrow(Mms.DELIVERY_REPORT);
    456             } catch (IllegalArgumentException e) {
    457                 Log.w("colsMap", e.getMessage());
    458             }
    459 
    460             try {
    461                 mColumnMmsReadReport = cursor.getColumnIndexOrThrow(Mms.READ_REPORT);
    462             } catch (IllegalArgumentException e) {
    463                 Log.w("colsMap", e.getMessage());
    464             }
    465 
    466             try {
    467                 mColumnMmsErrorType = cursor.getColumnIndexOrThrow(PendingMessages.ERROR_TYPE);
    468             } catch (IllegalArgumentException e) {
    469                 Log.w("colsMap", e.getMessage());
    470             }
    471 
    472             try {
    473                 mColumnMmsLocked = cursor.getColumnIndexOrThrow(Mms.LOCKED);
    474             } catch (IllegalArgumentException e) {
    475                 Log.w("colsMap", e.getMessage());
    476             }
    477 
    478             try {
    479                 mColumnMmsStatus = cursor.getColumnIndexOrThrow(Mms.STATUS);
    480             } catch (IllegalArgumentException e) {
    481                 Log.w("colsMap", e.getMessage());
    482             }
    483 
    484             try {
    485                 mColumnMmsTextOnly = cursor.getColumnIndexOrThrow(Mms.TEXT_ONLY);
    486             } catch (IllegalArgumentException e) {
    487                 Log.w("colsMap", e.getMessage());
    488             }
    489         }
    490     }
    491 
    492     private static class MessageItemCache extends LruCache<Long, MessageItem> {
    493         public MessageItemCache(int maxSize) {
    494             super(maxSize);
    495         }
    496 
    497         @Override
    498         protected void entryRemoved(boolean evicted, Long key,
    499                 MessageItem oldValue, MessageItem newValue) {
    500             oldValue.cancelPduLoading();
    501         }
    502     }
    503 }
    504