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            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