• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 com.android.mms.R;
23 import com.android.mms.data.Contact;
24 import com.android.mms.model.SlideModel;
25 import com.android.mms.model.SlideshowModel;
26 import com.android.mms.model.TextModel;
27 import com.android.mms.ui.MessageListAdapter.ColumnsMap;
28 import com.android.mms.util.AddressUtils;
29 import com.google.android.mms.MmsException;
30 import com.google.android.mms.pdu.EncodedStringValue;
31 import com.google.android.mms.pdu.MultimediaMessagePdu;
32 import com.google.android.mms.pdu.NotificationInd;
33 import com.google.android.mms.pdu.PduHeaders;
34 import com.google.android.mms.pdu.PduPersister;
35 import com.google.android.mms.pdu.RetrieveConf;
36 import com.google.android.mms.pdu.SendReq;
37 
38 import android.content.ContentUris;
39 import android.content.Context;
40 import android.database.Cursor;
41 import android.net.Uri;
42 import android.provider.Telephony.Mms;
43 import android.provider.Telephony.MmsSms;
44 import android.provider.Telephony.Sms;
45 import android.text.TextUtils;
46 import 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  */
54 public 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 
MessageItem(Context context, String type, Cursor cursor, ColumnsMap columnsMap, Pattern highlight)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 
interpretFrom(EncodedStringValue from, Uri messageUri)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 
isMms()260     public boolean isMms() {
261         return mType.equals("mms");
262     }
263 
isSms()264     public boolean isSms() {
265         return mType.equals("sms");
266     }
267 
isDownloaded()268     public boolean isDownloaded() {
269         return (mMessageType != PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND);
270     }
271 
isOutgoingMessage()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 
isSending()281     public boolean isSending() {
282         return !isFailedMessage() && isOutgoingMessage();
283     }
284 
isFailedMessage()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.
setCachedFormattedMessage(CharSequence formattedMessage)298     public void setCachedFormattedMessage(CharSequence formattedMessage) {
299         mCachedFormattedMessage = formattedMessage;
300     }
301 
getCachedFormattedMessage()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 
getBoxId()312     public int getBoxId() {
313         return mBoxId;
314     }
315 
316     @Override
toString()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