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