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                mTimestamp = 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.getTotalMessageSize();
232            }
233
234            if (!isOutgoingMessage()) {
235                if (PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND == mMessageType) {
236                    mTimestamp = context.getString(R.string.expire_on,
237                            MessageUtils.formatTimeStampString(context, timestamp));
238                } else {
239                    mTimestamp =  MessageUtils.formatTimeStampString(context, timestamp);
240                }
241            }
242        } else {
243            throw new MmsException("Unknown type of the message: " + type);
244        }
245    }
246
247    private void interpretFrom(EncodedStringValue from, Uri messageUri) {
248        if (from != null) {
249            mAddress = from.getString();
250        } else {
251            // In the rare case when getting the "from" address from the pdu fails,
252            // (e.g. from == null) fall back to a slower, yet more reliable method of
253            // getting the address from the "addr" table. This is what the Messaging
254            // notification system uses.
255            mAddress = AddressUtils.getFrom(mContext, messageUri);
256        }
257        mContact = TextUtils.isEmpty(mAddress) ? "" : Contact.get(mAddress, false).getName();
258    }
259
260    public boolean isMms() {
261        return mType.equals("mms");
262    }
263
264    public boolean isSms() {
265        return mType.equals("sms");
266    }
267
268    public boolean isDownloaded() {
269        return (mMessageType != PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND);
270    }
271
272    public boolean isOutgoingMessage() {
273        boolean isOutgoingMms = isMms() && (mBoxId == Mms.MESSAGE_BOX_OUTBOX);
274        boolean isOutgoingSms = isSms()
275                                    && ((mBoxId == Sms.MESSAGE_TYPE_FAILED)
276                                            || (mBoxId == Sms.MESSAGE_TYPE_OUTBOX)
277                                            || (mBoxId == Sms.MESSAGE_TYPE_QUEUED));
278        return isOutgoingMms || isOutgoingSms;
279    }
280
281    public boolean isSending() {
282        return !isFailedMessage() && isOutgoingMessage();
283    }
284
285    public boolean isFailedMessage() {
286        boolean isFailedMms = isMms()
287                            && (mErrorType >= MmsSms.ERR_TYPE_GENERIC_PERMANENT);
288        boolean isFailedSms = isSms()
289                            && (mBoxId == Sms.MESSAGE_TYPE_FAILED);
290        return isFailedMms || isFailedSms;
291    }
292
293    // Note: This is the only mutable field in this class.  Think of
294    // mCachedFormattedMessage as a C++ 'mutable' field on a const
295    // object, with this being a lazy accessor whose logic to set it
296    // is outside the class for model/view separation reasons.  In any
297    // case, please keep this class conceptually immutable.
298    public void setCachedFormattedMessage(CharSequence formattedMessage) {
299        mCachedFormattedMessage = formattedMessage;
300    }
301
302    public CharSequence getCachedFormattedMessage() {
303        boolean isSending = isSending();
304        if (isSending != mLastSendingState) {
305            mLastSendingState = isSending;
306            mCachedFormattedMessage = null;         // clear cache so we'll rebuild the message
307                                                    // to show "Sending..." or the sent date.
308        }
309        return mCachedFormattedMessage;
310    }
311
312    public int getBoxId() {
313        return mBoxId;
314    }
315
316    @Override
317    public String toString() {
318        return "type: " + mType +
319            " box: " + mBoxId +
320            " uri: " + mMessageUri +
321            " address: " + mAddress +
322            " contact: " + mContact +
323            " read: " + mReadReport +
324            " delivery status: " + mDeliveryStatus;
325    }
326}
327