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.model.SlideModel; 37 import com.android.mms.model.SlideshowModel; 38 import com.android.mms.model.TextModel; 39 import com.android.mms.ui.MessageListAdapter.ColumnsMap; 40 import com.android.mms.util.AddressUtils; 41 import com.android.mms.util.DownloadManager; 42 import com.android.mms.util.ItemLoadedCallback; 43 import com.android.mms.util.ItemLoadedFuture; 44 import com.android.mms.util.PduLoaderManager; 45 import com.google.android.mms.MmsException; 46 import com.google.android.mms.pdu.EncodedStringValue; 47 import com.google.android.mms.pdu.MultimediaMessagePdu; 48 import com.google.android.mms.pdu.NotificationInd; 49 import com.google.android.mms.pdu.PduHeaders; 50 import com.google.android.mms.pdu.PduPersister; 51 import com.google.android.mms.pdu.RetrieveConf; 52 import com.google.android.mms.pdu.SendReq; 53 54 /** 55 * Mostly immutable model for an SMS/MMS message. 56 * 57 * <p>The only mutable field is the cached formatted message member, 58 * the formatting of which is done outside this model in MessageListItem. 59 */ 60 public class MessageItem { 61 private static String TAG = "MessageItem"; 62 63 public enum DeliveryStatus { NONE, INFO, FAILED, PENDING, RECEIVED } 64 65 public static int ATTACHMENT_TYPE_NOT_LOADED = -1; 66 67 final Context mContext; 68 final String mType; 69 final long mMsgId; 70 final int mBoxId; 71 72 DeliveryStatus mDeliveryStatus; 73 boolean mReadReport; 74 boolean mLocked; // locked to prevent auto-deletion 75 76 String mTimestamp; 77 String mAddress; 78 String mContact; 79 String mBody; // Body of SMS, first text of MMS. 80 String mTextContentType; // ContentType of text of MMS. 81 Pattern mHighlight; // portion of message to highlight (from search) 82 83 // The only non-immutable field. Not synchronized, as access will 84 // only be from the main GUI thread. Worst case if accessed from 85 // another thread is it'll return null and be set again from that 86 // thread. 87 CharSequence mCachedFormattedMessage; 88 89 // The last message is cached above in mCachedFormattedMessage. In the latest design, we 90 // show "Sending..." in place of the timestamp when a message is being sent. mLastSendingState 91 // is used to keep track of the last sending state so that if the current sending state is 92 // different, we can clear the message cache so it will get rebuilt and recached. 93 boolean mLastSendingState; 94 95 // Fields for MMS only. 96 Uri mMessageUri; 97 int mMessageType; 98 int mAttachmentType; 99 String mSubject; 100 SlideshowModel mSlideshow; 101 int mMessageSize; 102 int mErrorType; 103 int mErrorCode; 104 int mMmsStatus; 105 Cursor mCursor; 106 ColumnsMap mColumnsMap; 107 private PduLoadedCallback mPduLoadedCallback; 108 private ItemLoadedFuture mItemLoadedFuture; 109 MessageItem(Context context, String type, final Cursor cursor, final ColumnsMap columnsMap, Pattern highlight)110 MessageItem(Context context, String type, final Cursor cursor, 111 final ColumnsMap columnsMap, Pattern highlight) throws MmsException { 112 mContext = context; 113 mMsgId = cursor.getLong(columnsMap.mColumnMsgId); 114 mHighlight = highlight; 115 mType = type; 116 mCursor = cursor; 117 mColumnsMap = columnsMap; 118 119 if ("sms".equals(type)) { 120 mReadReport = false; // No read reports in sms 121 122 long status = cursor.getLong(columnsMap.mColumnSmsStatus); 123 if (status == Sms.STATUS_NONE) { 124 // No delivery report requested 125 mDeliveryStatus = DeliveryStatus.NONE; 126 } else if (status >= Sms.STATUS_FAILED) { 127 // Failure 128 mDeliveryStatus = DeliveryStatus.FAILED; 129 } else if (status >= Sms.STATUS_PENDING) { 130 // Pending 131 mDeliveryStatus = DeliveryStatus.PENDING; 132 } else { 133 // Success 134 mDeliveryStatus = DeliveryStatus.RECEIVED; 135 } 136 137 mMessageUri = ContentUris.withAppendedId(Sms.CONTENT_URI, mMsgId); 138 // Set contact and message body 139 mBoxId = cursor.getInt(columnsMap.mColumnSmsType); 140 mAddress = cursor.getString(columnsMap.mColumnSmsAddress); 141 if (Sms.isOutgoingFolder(mBoxId)) { 142 String meString = context.getString( 143 R.string.messagelist_sender_self); 144 145 mContact = meString; 146 } else { 147 // For incoming messages, the ADDRESS field contains the sender. 148 mContact = Contact.get(mAddress, false).getName(); 149 } 150 mBody = cursor.getString(columnsMap.mColumnSmsBody); 151 152 // Unless the message is currently in the progress of being sent, it gets a time stamp. 153 if (!isOutgoingMessage()) { 154 // Set "received" or "sent" time stamp 155 long date = cursor.getLong(columnsMap.mColumnSmsDate); 156 mTimestamp = MessageUtils.formatTimeStampString(context, date); 157 } 158 159 mLocked = cursor.getInt(columnsMap.mColumnSmsLocked) != 0; 160 mErrorCode = cursor.getInt(columnsMap.mColumnSmsErrorCode); 161 } else if ("mms".equals(type)) { 162 mMessageUri = ContentUris.withAppendedId(Mms.CONTENT_URI, mMsgId); 163 mBoxId = cursor.getInt(columnsMap.mColumnMmsMessageBox); 164 mMessageType = cursor.getInt(columnsMap.mColumnMmsMessageType); 165 mErrorType = cursor.getInt(columnsMap.mColumnMmsErrorType); 166 String subject = cursor.getString(columnsMap.mColumnMmsSubject); 167 if (!TextUtils.isEmpty(subject)) { 168 EncodedStringValue v = new EncodedStringValue( 169 cursor.getInt(columnsMap.mColumnMmsSubjectCharset), 170 PduPersister.getBytes(subject)); 171 mSubject = v.getString(); 172 } 173 mLocked = cursor.getInt(columnsMap.mColumnMmsLocked) != 0; 174 mSlideshow = null; 175 mAttachmentType = ATTACHMENT_TYPE_NOT_LOADED; 176 mDeliveryStatus = DeliveryStatus.NONE; 177 mReadReport = false; 178 mBody = null; 179 mMessageSize = 0; 180 mTextContentType = null; 181 mTimestamp = null; 182 mMmsStatus = cursor.getInt(columnsMap.mColumnMmsStatus); 183 184 // Start an async load of the pdu. If the pdu is already loaded, the callback 185 // will get called immediately 186 boolean loadSlideshow = mMessageType != PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND; 187 188 mItemLoadedFuture = MmsApp.getApplication().getPduLoaderManager() 189 .getPdu(mMessageUri, loadSlideshow, 190 new PduLoadedMessageItemCallback()); 191 192 } else { 193 throw new MmsException("Unknown type of the message: " + type); 194 } 195 } 196 interpretFrom(EncodedStringValue from, Uri messageUri)197 private void interpretFrom(EncodedStringValue from, Uri messageUri) { 198 if (from != null) { 199 mAddress = from.getString(); 200 } else { 201 // In the rare case when getting the "from" address from the pdu fails, 202 // (e.g. from == null) fall back to a slower, yet more reliable method of 203 // getting the address from the "addr" table. This is what the Messaging 204 // notification system uses. 205 mAddress = AddressUtils.getFrom(mContext, messageUri); 206 } 207 mContact = TextUtils.isEmpty(mAddress) ? "" : Contact.get(mAddress, false).getName(); 208 } 209 isMms()210 public boolean isMms() { 211 return mType.equals("mms"); 212 } 213 isSms()214 public boolean isSms() { 215 return mType.equals("sms"); 216 } 217 isDownloaded()218 public boolean isDownloaded() { 219 return (mMessageType != PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND); 220 } 221 isOutgoingMessage()222 public boolean isOutgoingMessage() { 223 boolean isOutgoingMms = isMms() && (mBoxId == Mms.MESSAGE_BOX_OUTBOX); 224 boolean isOutgoingSms = isSms() 225 && ((mBoxId == Sms.MESSAGE_TYPE_FAILED) 226 || (mBoxId == Sms.MESSAGE_TYPE_OUTBOX) 227 || (mBoxId == Sms.MESSAGE_TYPE_QUEUED)); 228 return isOutgoingMms || isOutgoingSms; 229 } 230 isSending()231 public boolean isSending() { 232 return !isFailedMessage() && isOutgoingMessage(); 233 } 234 isFailedMessage()235 public boolean isFailedMessage() { 236 boolean isFailedMms = isMms() 237 && (mErrorType >= MmsSms.ERR_TYPE_GENERIC_PERMANENT); 238 boolean isFailedSms = isSms() 239 && (mBoxId == Sms.MESSAGE_TYPE_FAILED); 240 return isFailedMms || isFailedSms; 241 } 242 243 // Note: This is the only mutable field in this class. Think of 244 // mCachedFormattedMessage as a C++ 'mutable' field on a const 245 // object, with this being a lazy accessor whose logic to set it 246 // is outside the class for model/view separation reasons. In any 247 // case, please keep this class conceptually immutable. setCachedFormattedMessage(CharSequence formattedMessage)248 public void setCachedFormattedMessage(CharSequence formattedMessage) { 249 mCachedFormattedMessage = formattedMessage; 250 } 251 getCachedFormattedMessage()252 public CharSequence getCachedFormattedMessage() { 253 boolean isSending = isSending(); 254 if (isSending != mLastSendingState) { 255 mLastSendingState = isSending; 256 mCachedFormattedMessage = null; // clear cache so we'll rebuild the message 257 // to show "Sending..." or the sent date. 258 } 259 return mCachedFormattedMessage; 260 } 261 getBoxId()262 public int getBoxId() { 263 return mBoxId; 264 } 265 getMessageId()266 public long getMessageId() { 267 return mMsgId; 268 } 269 getMmsDownloadStatus()270 public int getMmsDownloadStatus() { 271 return mMmsStatus & ~DownloadManager.DEFERRED_MASK; 272 } 273 274 @Override toString()275 public String toString() { 276 return "type: " + mType + 277 " box: " + mBoxId + 278 " uri: " + mMessageUri + 279 " address: " + mAddress + 280 " contact: " + mContact + 281 " read: " + mReadReport + 282 " delivery status: " + mDeliveryStatus; 283 } 284 285 public class PduLoadedMessageItemCallback implements ItemLoadedCallback { onItemLoaded(Object result, Throwable exception)286 public void onItemLoaded(Object result, Throwable exception) { 287 if (exception != null) { 288 Log.e(TAG, "PduLoadedMessageItemCallback PDU couldn't be loaded: ", exception); 289 return; 290 } 291 PduLoaderManager.PduLoaded pduLoaded = (PduLoaderManager.PduLoaded)result; 292 long timestamp = 0L; 293 if (PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND == mMessageType) { 294 mDeliveryStatus = DeliveryStatus.NONE; 295 NotificationInd notifInd = (NotificationInd)pduLoaded.mPdu; 296 interpretFrom(notifInd.getFrom(), mMessageUri); 297 // Borrow the mBody to hold the URL of the message. 298 mBody = new String(notifInd.getContentLocation()); 299 mMessageSize = (int) notifInd.getMessageSize(); 300 timestamp = notifInd.getExpiry() * 1000L; 301 } else { 302 if (mCursor.isClosed()) { 303 return; 304 } 305 MultimediaMessagePdu msg = (MultimediaMessagePdu)pduLoaded.mPdu; 306 mSlideshow = pduLoaded.mSlideshow; 307 mAttachmentType = MessageUtils.getAttachmentType(mSlideshow); 308 309 if (mMessageType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF) { 310 if (msg == null) { 311 interpretFrom(null, mMessageUri); 312 } else { 313 RetrieveConf retrieveConf = (RetrieveConf) msg; 314 interpretFrom(retrieveConf.getFrom(), mMessageUri); 315 timestamp = retrieveConf.getDate() * 1000L; 316 } 317 } else { 318 // Use constant string for outgoing messages 319 mContact = mAddress = 320 mContext.getString(R.string.messagelist_sender_self); 321 timestamp = msg == null ? 0 : ((SendReq) msg).getDate() * 1000L; 322 } 323 324 SlideModel slide = mSlideshow == null ? null : mSlideshow.get(0); 325 if ((slide != null) && slide.hasText()) { 326 TextModel tm = slide.getText(); 327 mBody = tm.getText(); 328 mTextContentType = tm.getContentType(); 329 } 330 331 mMessageSize = mSlideshow == null ? 0 : mSlideshow.getTotalMessageSize(); 332 333 String report = mCursor.getString(mColumnsMap.mColumnMmsDeliveryReport); 334 if ((report == null) || !mAddress.equals(mContext.getString( 335 R.string.messagelist_sender_self))) { 336 mDeliveryStatus = DeliveryStatus.NONE; 337 } else { 338 int reportInt; 339 try { 340 reportInt = Integer.parseInt(report); 341 if (reportInt == PduHeaders.VALUE_YES) { 342 mDeliveryStatus = DeliveryStatus.RECEIVED; 343 } else { 344 mDeliveryStatus = DeliveryStatus.NONE; 345 } 346 } catch (NumberFormatException nfe) { 347 Log.e(TAG, "Value for delivery report was invalid."); 348 mDeliveryStatus = DeliveryStatus.NONE; 349 } 350 } 351 352 report = mCursor.getString(mColumnsMap.mColumnMmsReadReport); 353 if ((report == null) || !mAddress.equals(mContext.getString( 354 R.string.messagelist_sender_self))) { 355 mReadReport = false; 356 } else { 357 int reportInt; 358 try { 359 reportInt = Integer.parseInt(report); 360 mReadReport = (reportInt == PduHeaders.VALUE_YES); 361 } catch (NumberFormatException nfe) { 362 Log.e(TAG, "Value for read report was invalid."); 363 mReadReport = false; 364 } 365 } 366 } 367 if (!isOutgoingMessage()) { 368 if (PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND == mMessageType) { 369 mTimestamp = mContext.getString(R.string.expire_on, 370 MessageUtils.formatTimeStampString(mContext, timestamp)); 371 } else { 372 mTimestamp = MessageUtils.formatTimeStampString(mContext, timestamp); 373 } 374 } 375 if (mPduLoadedCallback != null) { 376 mPduLoadedCallback.onPduLoaded(MessageItem.this); 377 } 378 } 379 } 380 setOnPduLoaded(PduLoadedCallback pduLoadedCallback)381 public void setOnPduLoaded(PduLoadedCallback pduLoadedCallback) { 382 mPduLoadedCallback = pduLoadedCallback; 383 } 384 cancelPduLoading()385 public void cancelPduLoading() { 386 if (mItemLoadedFuture != null) { 387 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 388 Log.v(TAG, "cancelPduLoading for: " + this); 389 } 390 mItemLoadedFuture.cancel(); 391 mItemLoadedFuture = null; 392 } 393 } 394 395 public interface PduLoadedCallback { 396 /** 397 * Called when this item's pdu and slideshow are finished loading. 398 * 399 * @param messageItem the MessageItem that finished loading. 400 */ onPduLoaded(MessageItem messageItem)401 void onPduLoaded(MessageItem messageItem); 402 } 403 getSlideshow()404 public SlideshowModel getSlideshow() { 405 return mSlideshow; 406 } 407 } 408