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 = LogTag.TAG;
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 
MessageItem(Context context, String type, final Cursor cursor, final ColumnsMap columnsMap, Pattern highlight)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 
interpretFrom(EncodedStringValue from, Uri messageUri)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 
isMms()213     public boolean isMms() {
214         return mType.equals("mms");
215     }
216 
isSms()217     public boolean isSms() {
218         return mType.equals("sms");
219     }
220 
isDownloaded()221     public boolean isDownloaded() {
222         return (mMessageType != PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND);
223     }
224 
isMe()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 
isOutgoingMessage()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 
isSending()247     public boolean isSending() {
248         return !isFailedMessage() && isOutgoingMessage();
249     }
250 
isFailedMessage()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.
setCachedFormattedMessage(CharSequence formattedMessage)264     public void setCachedFormattedMessage(CharSequence formattedMessage) {
265         mCachedFormattedMessage = formattedMessage;
266     }
267 
getCachedFormattedMessage()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 
getBoxId()278     public int getBoxId() {
279         return mBoxId;
280     }
281 
getMessageId()282     public long getMessageId() {
283         return mMsgId;
284     }
285 
getMmsDownloadStatus()286     public int getMmsDownloadStatus() {
287         return mMmsStatus & ~DownloadManager.DEFERRED_MASK;
288     }
289 
290     @Override
toString()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 {
onItemLoaded(Object result, Throwable exception)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 
setOnPduLoaded(PduLoadedCallback pduLoadedCallback)402     public void setOnPduLoaded(PduLoadedCallback pduLoadedCallback) {
403         mPduLoadedCallback = pduLoadedCallback;
404     }
405 
cancelPduLoading()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          */
onPduLoaded(MessageItem messageItem)422         void onPduLoaded(MessageItem messageItem);
423     }
424 
getSlideshow()425     public SlideshowModel getSlideshow() {
426         return mSlideshow;
427     }
428 }
429