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