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