MessageItem.java revision 0e6ae5265245a13e1bef96c6aad1e3f2c1c7236e
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
18package com.android.mms.ui;
19
20import java.util.regex.Pattern;
21
22import com.android.mms.R;
23import com.android.mms.data.Contact;
24import com.android.mms.model.SlideModel;
25import com.android.mms.model.SlideshowModel;
26import com.android.mms.model.TextModel;
27import com.android.mms.ui.MessageListAdapter.ColumnsMap;
28import com.android.mms.util.AddressUtils;
29import com.google.android.mms.MmsException;
30import com.google.android.mms.pdu.EncodedStringValue;
31import com.google.android.mms.pdu.MultimediaMessagePdu;
32import com.google.android.mms.pdu.NotificationInd;
33import com.google.android.mms.pdu.PduHeaders;
34import com.google.android.mms.pdu.PduPersister;
35import com.google.android.mms.pdu.RetrieveConf;
36import com.google.android.mms.pdu.SendReq;
37
38import android.content.ContentUris;
39import android.content.Context;
40import android.database.Cursor;
41import android.net.Uri;
42import android.provider.Telephony.Mms;
43import android.provider.Telephony.MmsSms;
44import android.provider.Telephony.Sms;
45import android.text.TextUtils;
46import 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 */
54public 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            // Unless the message is currently in the progress of being sent, it gets a time stamp.
138            if (!isOutgoingMessage()) {
139                // Set "received" or "sent" time stamp
140                long date = cursor.getLong(columnsMap.mColumnSmsDate);
141                String label = context.getString(
142                    Sms.isOutgoingFolder(mBoxId) ? R.string.sent_on : R.string.received_on);
143                mTimestamp = String.format(label,
144                        MessageUtils.formatTimeStampString(context, date));
145            }
146
147            mLocked = cursor.getInt(columnsMap.mColumnSmsLocked) != 0;
148            mErrorCode = cursor.getInt(columnsMap.mColumnSmsErrorCode);
149        } else if ("mms".equals(type)) {
150            mMessageUri = ContentUris.withAppendedId(Mms.CONTENT_URI, mMsgId);
151            mBoxId = cursor.getInt(columnsMap.mColumnMmsMessageBox);
152            mMessageType = cursor.getInt(columnsMap.mColumnMmsMessageType);
153            mErrorType = cursor.getInt(columnsMap.mColumnMmsErrorType);
154            String subject = cursor.getString(columnsMap.mColumnMmsSubject);
155            if (!TextUtils.isEmpty(subject)) {
156                EncodedStringValue v = new EncodedStringValue(
157                        cursor.getInt(columnsMap.mColumnMmsSubjectCharset),
158                        PduPersister.getBytes(subject));
159                mSubject = v.getString();
160            }
161            mLocked = cursor.getInt(columnsMap.mColumnMmsLocked) != 0;
162
163            long timestamp = 0L;
164            PduPersister p = PduPersister.getPduPersister(mContext);
165            if (PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND == mMessageType) {
166                mDeliveryStatus = DeliveryStatus.NONE;
167                NotificationInd notifInd = (NotificationInd) p.load(mMessageUri);
168                interpretFrom(notifInd.getFrom(), mMessageUri);
169                // Borrow the mBody to hold the URL of the message.
170                mBody = new String(notifInd.getContentLocation());
171                mMessageSize = (int) notifInd.getMessageSize();
172                timestamp = notifInd.getExpiry() * 1000L;
173            } else {
174                MultimediaMessagePdu msg = (MultimediaMessagePdu) p.load(mMessageUri);
175                mSlideshow = SlideshowModel.createFromPduBody(context, msg.getBody());
176                mAttachmentType = MessageUtils.getAttachmentType(mSlideshow);
177
178                if (mMessageType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF) {
179                    RetrieveConf retrieveConf = (RetrieveConf) msg;
180                    interpretFrom(retrieveConf.getFrom(), mMessageUri);
181                    timestamp = retrieveConf.getDate() * 1000L;
182                } else {
183                    // Use constant string for outgoing messages
184                    mContact = mAddress = context.getString(R.string.messagelist_sender_self);
185                    timestamp = ((SendReq) msg).getDate() * 1000L;
186                }
187
188
189                String report = cursor.getString(columnsMap.mColumnMmsDeliveryReport);
190                if ((report == null) || !mAddress.equals(context.getString(
191                        R.string.messagelist_sender_self))) {
192                    mDeliveryStatus = DeliveryStatus.NONE;
193                } else {
194                    int reportInt;
195                    try {
196                        reportInt = Integer.parseInt(report);
197                        if (reportInt == PduHeaders.VALUE_YES) {
198                            mDeliveryStatus = DeliveryStatus.RECEIVED;
199                        } else {
200                            mDeliveryStatus = DeliveryStatus.NONE;
201                        }
202                    } catch (NumberFormatException nfe) {
203                        Log.e(TAG, "Value for delivery report was invalid.");
204                        mDeliveryStatus = DeliveryStatus.NONE;
205                    }
206                }
207
208                report = cursor.getString(columnsMap.mColumnMmsReadReport);
209                if ((report == null) || !mAddress.equals(context.getString(
210                        R.string.messagelist_sender_self))) {
211                    mReadReport = false;
212                } else {
213                    int reportInt;
214                    try {
215                        reportInt = Integer.parseInt(report);
216                        mReadReport = (reportInt == PduHeaders.VALUE_YES);
217                    } catch (NumberFormatException nfe) {
218                        Log.e(TAG, "Value for read report was invalid.");
219                        mReadReport = false;
220                    }
221                }
222
223                SlideModel slide = mSlideshow.get(0);
224                if ((slide != null) && slide.hasText()) {
225                    TextModel tm = slide.getText();
226                    if (tm.isDrmProtected()) {
227                        mBody = mContext.getString(R.string.drm_protected_text);
228                    } else {
229                        mBody = tm.getText();
230                    }
231                    mTextContentType = tm.getContentType();
232                }
233
234                mMessageSize = mSlideshow.getCurrentMessageSize();
235            }
236
237            if (!isOutgoingMessage()) {
238                mTimestamp = context.getString(getTimestampStrId(),
239                        MessageUtils.formatTimeStampString(context, timestamp));
240            }
241        } else {
242            throw new MmsException("Unknown type of the message: " + type);
243        }
244    }
245
246    private void interpretFrom(EncodedStringValue from, Uri messageUri) {
247        if (from != null) {
248            mAddress = from.getString();
249        } else {
250            // In the rare case when getting the "from" address from the pdu fails,
251            // (e.g. from == null) fall back to a slower, yet more reliable method of
252            // getting the address from the "addr" table. This is what the Messaging
253            // notification system uses.
254            mAddress = AddressUtils.getFrom(mContext, messageUri);
255        }
256        mContact = TextUtils.isEmpty(mAddress) ? "" : Contact.get(mAddress, false).getName();
257    }
258
259    private int getTimestampStrId() {
260        if (PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND == mMessageType) {
261            return R.string.expire_on;
262        } else if (PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF == mMessageType) {
263            return R.string.received_on;
264        } else {
265            return R.string.sent_on;
266        }
267    }
268
269    public boolean isMms() {
270        return mType.equals("mms");
271    }
272
273    public boolean isSms() {
274        return mType.equals("sms");
275    }
276
277    public boolean isDownloaded() {
278        return (mMessageType != PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND);
279    }
280
281    public boolean isOutgoingMessage() {
282        boolean isOutgoingMms = isMms() && (mBoxId == Mms.MESSAGE_BOX_OUTBOX);
283        boolean isOutgoingSms = isSms()
284                                    && ((mBoxId == Sms.MESSAGE_TYPE_FAILED)
285                                            || (mBoxId == Sms.MESSAGE_TYPE_OUTBOX)
286                                            || (mBoxId == Sms.MESSAGE_TYPE_QUEUED));
287        return isOutgoingMms || isOutgoingSms;
288    }
289
290    public boolean isSending() {
291        return !isFailedMessage() && isOutgoingMessage();
292    }
293
294    public boolean isFailedMessage() {
295        boolean isFailedMms = isMms()
296                            && (mErrorType >= MmsSms.ERR_TYPE_GENERIC_PERMANENT);
297        boolean isFailedSms = isSms()
298                            && (mBoxId == Sms.MESSAGE_TYPE_FAILED);
299        return isFailedMms || isFailedSms;
300    }
301
302    // Note: This is the only mutable field in this class.  Think of
303    // mCachedFormattedMessage as a C++ 'mutable' field on a const
304    // object, with this being a lazy accessor whose logic to set it
305    // is outside the class for model/view separation reasons.  In any
306    // case, please keep this class conceptually immutable.
307    public void setCachedFormattedMessage(CharSequence formattedMessage) {
308        mCachedFormattedMessage = formattedMessage;
309    }
310
311    public CharSequence getCachedFormattedMessage() {
312        boolean isSending = isSending();
313        if (isSending != mLastSendingState) {
314            mLastSendingState = isSending;
315            mCachedFormattedMessage = null;         // clear cache so we'll rebuild the message
316                                                    // to show "Sending..." or the sent date.
317        }
318        return mCachedFormattedMessage;
319    }
320
321    public int getBoxId() {
322        return mBoxId;
323    }
324
325    @Override
326    public String toString() {
327        return "type: " + mType +
328            " box: " + mBoxId +
329            " uri: " + mMessageUri +
330            " address: " + mAddress +
331            " contact: " + mContact +
332            " read: " + mReadReport +
333            " delivery status: " + mDeliveryStatus;
334    }
335}
336