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 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 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 213 public boolean isMms() { 214 return mType.equals("mms"); 215 } 216 217 public boolean isSms() { 218 return mType.equals("sms"); 219 } 220 221 public boolean isDownloaded() { 222 return (mMessageType != PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND); 223 } 224 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 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 247 public boolean isSending() { 248 return !isFailedMessage() && isOutgoingMessage(); 249 } 250 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. 264 public void setCachedFormattedMessage(CharSequence formattedMessage) { 265 mCachedFormattedMessage = formattedMessage; 266 } 267 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 278 public int getBoxId() { 279 return mBoxId; 280 } 281 282 public long getMessageId() { 283 return mMsgId; 284 } 285 286 public int getMmsDownloadStatus() { 287 return mMmsStatus & ~DownloadManager.DEFERRED_MASK; 288 } 289 290 @Override 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 { 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 402 public void setOnPduLoaded(PduLoadedCallback pduLoadedCallback) { 403 mPduLoadedCallback = pduLoadedCallback; 404 } 405 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 */ 422 void onPduLoaded(MessageItem messageItem); 423 } 424 425 public SlideshowModel getSlideshow() { 426 return mSlideshow; 427 } 428 } 429