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 com.android.mms.R;
     23 import com.android.mms.data.Contact;
     24 import com.android.mms.model.SlideModel;
     25 import com.android.mms.model.SlideshowModel;
     26 import com.android.mms.model.TextModel;
     27 import com.android.mms.ui.MessageListAdapter.ColumnsMap;
     28 import com.android.mms.util.AddressUtils;
     29 import com.google.android.mms.MmsException;
     30 import com.google.android.mms.pdu.EncodedStringValue;
     31 import com.google.android.mms.pdu.MultimediaMessagePdu;
     32 import com.google.android.mms.pdu.NotificationInd;
     33 import com.google.android.mms.pdu.PduHeaders;
     34 import com.google.android.mms.pdu.PduPersister;
     35 import com.google.android.mms.pdu.RetrieveConf;
     36 import com.google.android.mms.pdu.SendReq;
     37 
     38 import android.content.ContentUris;
     39 import android.content.Context;
     40 import android.database.Cursor;
     41 import android.net.Uri;
     42 import android.provider.Telephony.Mms;
     43 import android.provider.Telephony.MmsSms;
     44 import android.provider.Telephony.Sms;
     45 import android.text.TextUtils;
     46 import android.util.Log;
     47 
     48 /**
     49  * Mostly immutable model for an SMS/MMS message.
     50  *
     51  * <p>The only mutable field is the cached formatted message member,
     52  * the formatting of which is done outside this model in MessageListItem.
     53  */
     54 public class MessageItem {
     55     private static String TAG = "MessageItem";
     56 
     57     public enum DeliveryStatus  { NONE, INFO, FAILED, PENDING, RECEIVED }
     58 
     59     final Context mContext;
     60     final String mType;
     61     final long mMsgId;
     62     final int mBoxId;
     63 
     64     DeliveryStatus mDeliveryStatus;
     65     boolean mReadReport;
     66     boolean mLocked;            // locked to prevent auto-deletion
     67 
     68     String mTimestamp;
     69     String mAddress;
     70     String mContact;
     71     String mBody; // Body of SMS, first text of MMS.
     72     String mTextContentType; // ContentType of text of MMS.
     73     Pattern mHighlight; // portion of message to highlight (from search)
     74 
     75     // The only non-immutable field.  Not synchronized, as access will
     76     // only be from the main GUI thread.  Worst case if accessed from
     77     // another thread is it'll return null and be set again from that
     78     // thread.
     79     CharSequence mCachedFormattedMessage;
     80 
     81     // The last message is cached above in mCachedFormattedMessage. In the latest design, we
     82     // show "Sending..." in place of the timestamp when a message is being sent. mLastSendingState
     83     // is used to keep track of the last sending state so that if the current sending state is
     84     // different, we can clear the message cache so it will get rebuilt and recached.
     85     boolean mLastSendingState;
     86 
     87     // Fields for MMS only.
     88     Uri mMessageUri;
     89     int mMessageType;
     90     int mAttachmentType;
     91     String mSubject;
     92     SlideshowModel mSlideshow;
     93     int mMessageSize;
     94     int mErrorType;
     95     int mErrorCode;
     96 
     97     MessageItem(Context context, String type, Cursor cursor,
     98             ColumnsMap columnsMap, Pattern highlight) throws MmsException {
     99         mContext = context;
    100         mMsgId = cursor.getLong(columnsMap.mColumnMsgId);
    101         mHighlight = highlight;
    102         mType = type;
    103 
    104         if ("sms".equals(type)) {
    105             mReadReport = false; // No read reports in sms
    106 
    107             long status = cursor.getLong(columnsMap.mColumnSmsStatus);
    108             if (status == Sms.STATUS_NONE) {
    109                 // No delivery report requested
    110                 mDeliveryStatus = DeliveryStatus.NONE;
    111             } else if (status >= Sms.STATUS_FAILED) {
    112                 // Failure
    113                 mDeliveryStatus = DeliveryStatus.FAILED;
    114             } else if (status >= Sms.STATUS_PENDING) {
    115                 // Pending
    116                 mDeliveryStatus = DeliveryStatus.PENDING;
    117             } else {
    118                 // Success
    119                 mDeliveryStatus = DeliveryStatus.RECEIVED;
    120             }
    121 
    122             mMessageUri = ContentUris.withAppendedId(Sms.CONTENT_URI, mMsgId);
    123             // Set contact and message body
    124             mBoxId = cursor.getInt(columnsMap.mColumnSmsType);
    125             mAddress = cursor.getString(columnsMap.mColumnSmsAddress);
    126             if (Sms.isOutgoingFolder(mBoxId)) {
    127                 String meString = context.getString(
    128                         R.string.messagelist_sender_self);
    129 
    130                 mContact = meString;
    131             } else {
    132                 // For incoming messages, the ADDRESS field contains the sender.
    133                 mContact = Contact.get(mAddress, false).getName();
    134             }
    135             mBody = cursor.getString(columnsMap.mColumnSmsBody);
    136 
    137             if (!isOutgoingMessage()) {
    138                 // Set "sent" time stamp
    139                 long date = cursor.getLong(columnsMap.mColumnSmsDate);
    140                 mTimestamp = String.format(context.getString(R.string.sent_on),
    141                         MessageUtils.formatTimeStampString(context, date));
    142             }
    143 
    144             mLocked = cursor.getInt(columnsMap.mColumnSmsLocked) != 0;
    145             mErrorCode = cursor.getInt(columnsMap.mColumnSmsErrorCode);
    146         } else if ("mms".equals(type)) {
    147             mMessageUri = ContentUris.withAppendedId(Mms.CONTENT_URI, mMsgId);
    148             mBoxId = cursor.getInt(columnsMap.mColumnMmsMessageBox);
    149             mMessageType = cursor.getInt(columnsMap.mColumnMmsMessageType);
    150             mErrorType = cursor.getInt(columnsMap.mColumnMmsErrorType);
    151             String subject = cursor.getString(columnsMap.mColumnMmsSubject);
    152             if (!TextUtils.isEmpty(subject)) {
    153                 EncodedStringValue v = new EncodedStringValue(
    154                         cursor.getInt(columnsMap.mColumnMmsSubjectCharset),
    155                         PduPersister.getBytes(subject));
    156                 mSubject = v.getString();
    157             }
    158             mLocked = cursor.getInt(columnsMap.mColumnMmsLocked) != 0;
    159 
    160             long timestamp = 0L;
    161             PduPersister p = PduPersister.getPduPersister(mContext);
    162             if (PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND == mMessageType) {
    163                 mDeliveryStatus = DeliveryStatus.NONE;
    164                 NotificationInd notifInd = (NotificationInd) p.load(mMessageUri);
    165                 interpretFrom(notifInd.getFrom(), mMessageUri);
    166                 // Borrow the mBody to hold the URL of the message.
    167                 mBody = new String(notifInd.getContentLocation());
    168                 mMessageSize = (int) notifInd.getMessageSize();
    169                 timestamp = notifInd.getExpiry() * 1000L;
    170             } else {
    171                 MultimediaMessagePdu msg = (MultimediaMessagePdu) p.load(mMessageUri);
    172                 mSlideshow = SlideshowModel.createFromPduBody(context, msg.getBody());
    173                 mAttachmentType = MessageUtils.getAttachmentType(mSlideshow);
    174 
    175                 if (mMessageType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF) {
    176                     RetrieveConf retrieveConf = (RetrieveConf) msg;
    177                     interpretFrom(retrieveConf.getFrom(), mMessageUri);
    178                     timestamp = retrieveConf.getDate() * 1000L;
    179                 } else {
    180                     // Use constant string for outgoing messages
    181                     mContact = mAddress = context.getString(R.string.messagelist_sender_self);
    182                     timestamp = ((SendReq) msg).getDate() * 1000L;
    183                 }
    184 
    185 
    186                 String report = cursor.getString(columnsMap.mColumnMmsDeliveryReport);
    187                 if ((report == null) || !mAddress.equals(context.getString(
    188                         R.string.messagelist_sender_self))) {
    189                     mDeliveryStatus = DeliveryStatus.NONE;
    190                 } else {
    191                     int reportInt;
    192                     try {
    193                         reportInt = Integer.parseInt(report);
    194                         if (reportInt == PduHeaders.VALUE_YES) {
    195                             mDeliveryStatus = DeliveryStatus.RECEIVED;
    196                         } else {
    197                             mDeliveryStatus = DeliveryStatus.NONE;
    198                         }
    199                     } catch (NumberFormatException nfe) {
    200                         Log.e(TAG, "Value for delivery report was invalid.");
    201                         mDeliveryStatus = DeliveryStatus.NONE;
    202                     }
    203                 }
    204 
    205                 report = cursor.getString(columnsMap.mColumnMmsReadReport);
    206                 if ((report == null) || !mAddress.equals(context.getString(
    207                         R.string.messagelist_sender_self))) {
    208                     mReadReport = false;
    209                 } else {
    210                     int reportInt;
    211                     try {
    212                         reportInt = Integer.parseInt(report);
    213                         mReadReport = (reportInt == PduHeaders.VALUE_YES);
    214                     } catch (NumberFormatException nfe) {
    215                         Log.e(TAG, "Value for read report was invalid.");
    216                         mReadReport = false;
    217                     }
    218                 }
    219 
    220                 SlideModel slide = mSlideshow.get(0);
    221                 if ((slide != null) && slide.hasText()) {
    222                     TextModel tm = slide.getText();
    223                     if (tm.isDrmProtected()) {
    224                         mBody = mContext.getString(R.string.drm_protected_text);
    225                     } else {
    226                         mBody = tm.getText();
    227                     }
    228                     mTextContentType = tm.getContentType();
    229                 }
    230 
    231                 mMessageSize = mSlideshow.getCurrentMessageSize();
    232             }
    233 
    234             if (!isOutgoingMessage()) {
    235                 mTimestamp = context.getString(getTimestampStrId(),
    236                         MessageUtils.formatTimeStampString(context, timestamp));
    237             }
    238         } else {
    239             throw new MmsException("Unknown type of the message: " + type);
    240         }
    241     }
    242 
    243     private void interpretFrom(EncodedStringValue from, Uri messageUri) {
    244         if (from != null) {
    245             mAddress = from.getString();
    246         } else {
    247             // In the rare case when getting the "from" address from the pdu fails,
    248             // (e.g. from == null) fall back to a slower, yet more reliable method of
    249             // getting the address from the "addr" table. This is what the Messaging
    250             // notification system uses.
    251             mAddress = AddressUtils.getFrom(mContext, messageUri);
    252         }
    253         mContact = TextUtils.isEmpty(mAddress) ? "" : Contact.get(mAddress, false).getName();
    254     }
    255 
    256     private int getTimestampStrId() {
    257         if (PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND == mMessageType) {
    258             return R.string.expire_on;
    259         } else {
    260             return R.string.sent_on;
    261         }
    262     }
    263 
    264     public boolean isMms() {
    265         return mType.equals("mms");
    266     }
    267 
    268     public boolean isSms() {
    269         return mType.equals("sms");
    270     }
    271 
    272     public boolean isDownloaded() {
    273         return (mMessageType != PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND);
    274     }
    275 
    276     public boolean isOutgoingMessage() {
    277         boolean isOutgoingMms = isMms() && (mBoxId == Mms.MESSAGE_BOX_OUTBOX);
    278         boolean isOutgoingSms = isSms()
    279                                     && ((mBoxId == Sms.MESSAGE_TYPE_FAILED)
    280                                             || (mBoxId == Sms.MESSAGE_TYPE_OUTBOX)
    281                                             || (mBoxId == Sms.MESSAGE_TYPE_QUEUED));
    282         return isOutgoingMms || isOutgoingSms;
    283     }
    284 
    285     public boolean isSending() {
    286         return !isFailedMessage() && isOutgoingMessage();
    287     }
    288 
    289     public boolean isFailedMessage() {
    290         boolean isFailedMms = isMms()
    291                             && (mErrorType >= MmsSms.ERR_TYPE_GENERIC_PERMANENT);
    292         boolean isFailedSms = isSms()
    293                             && (mBoxId == Sms.MESSAGE_TYPE_FAILED);
    294         return isFailedMms || isFailedSms;
    295     }
    296 
    297     // Note: This is the only mutable field in this class.  Think of
    298     // mCachedFormattedMessage as a C++ 'mutable' field on a const
    299     // object, with this being a lazy accessor whose logic to set it
    300     // is outside the class for model/view separation reasons.  In any
    301     // case, please keep this class conceptually immutable.
    302     public void setCachedFormattedMessage(CharSequence formattedMessage) {
    303         mCachedFormattedMessage = formattedMessage;
    304     }
    305 
    306     public CharSequence getCachedFormattedMessage() {
    307         boolean isSending = isSending();
    308         if (isSending != mLastSendingState) {
    309             mLastSendingState = isSending;
    310             mCachedFormattedMessage = null;         // clear cache so we'll rebuild the message
    311                                                     // to show "Sending..." or the sent date.
    312         }
    313         return mCachedFormattedMessage;
    314     }
    315 
    316     public int getBoxId() {
    317         return mBoxId;
    318     }
    319 
    320     @Override
    321     public String toString() {
    322         return "type: " + mType +
    323             " box: " + mBoxId +
    324             " uri: " + mMessageUri +
    325             " address: " + mAddress +
    326             " contact: " + mContact +
    327             " read: " + mReadReport +
    328             " delivery status: " + mDeliveryStatus;
    329     }
    330 }
    331