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