MessageItem.java revision 0e6ae5265245a13e1bef96c6aad1e3f2c1c7236e
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 // 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 String label = context.getString( 142 Sms.isOutgoingFolder(mBoxId) ? R.string.sent_on : R.string.received_on); 143 mTimestamp = String.format(label, 144 MessageUtils.formatTimeStampString(context, date)); 145 } 146 147 mLocked = cursor.getInt(columnsMap.mColumnSmsLocked) != 0; 148 mErrorCode = cursor.getInt(columnsMap.mColumnSmsErrorCode); 149 } else if ("mms".equals(type)) { 150 mMessageUri = ContentUris.withAppendedId(Mms.CONTENT_URI, mMsgId); 151 mBoxId = cursor.getInt(columnsMap.mColumnMmsMessageBox); 152 mMessageType = cursor.getInt(columnsMap.mColumnMmsMessageType); 153 mErrorType = cursor.getInt(columnsMap.mColumnMmsErrorType); 154 String subject = cursor.getString(columnsMap.mColumnMmsSubject); 155 if (!TextUtils.isEmpty(subject)) { 156 EncodedStringValue v = new EncodedStringValue( 157 cursor.getInt(columnsMap.mColumnMmsSubjectCharset), 158 PduPersister.getBytes(subject)); 159 mSubject = v.getString(); 160 } 161 mLocked = cursor.getInt(columnsMap.mColumnMmsLocked) != 0; 162 163 long timestamp = 0L; 164 PduPersister p = PduPersister.getPduPersister(mContext); 165 if (PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND == mMessageType) { 166 mDeliveryStatus = DeliveryStatus.NONE; 167 NotificationInd notifInd = (NotificationInd) p.load(mMessageUri); 168 interpretFrom(notifInd.getFrom(), mMessageUri); 169 // Borrow the mBody to hold the URL of the message. 170 mBody = new String(notifInd.getContentLocation()); 171 mMessageSize = (int) notifInd.getMessageSize(); 172 timestamp = notifInd.getExpiry() * 1000L; 173 } else { 174 MultimediaMessagePdu msg = (MultimediaMessagePdu) p.load(mMessageUri); 175 mSlideshow = SlideshowModel.createFromPduBody(context, msg.getBody()); 176 mAttachmentType = MessageUtils.getAttachmentType(mSlideshow); 177 178 if (mMessageType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF) { 179 RetrieveConf retrieveConf = (RetrieveConf) msg; 180 interpretFrom(retrieveConf.getFrom(), mMessageUri); 181 timestamp = retrieveConf.getDate() * 1000L; 182 } else { 183 // Use constant string for outgoing messages 184 mContact = mAddress = context.getString(R.string.messagelist_sender_self); 185 timestamp = ((SendReq) msg).getDate() * 1000L; 186 } 187 188 189 String report = cursor.getString(columnsMap.mColumnMmsDeliveryReport); 190 if ((report == null) || !mAddress.equals(context.getString( 191 R.string.messagelist_sender_self))) { 192 mDeliveryStatus = DeliveryStatus.NONE; 193 } else { 194 int reportInt; 195 try { 196 reportInt = Integer.parseInt(report); 197 if (reportInt == PduHeaders.VALUE_YES) { 198 mDeliveryStatus = DeliveryStatus.RECEIVED; 199 } else { 200 mDeliveryStatus = DeliveryStatus.NONE; 201 } 202 } catch (NumberFormatException nfe) { 203 Log.e(TAG, "Value for delivery report was invalid."); 204 mDeliveryStatus = DeliveryStatus.NONE; 205 } 206 } 207 208 report = cursor.getString(columnsMap.mColumnMmsReadReport); 209 if ((report == null) || !mAddress.equals(context.getString( 210 R.string.messagelist_sender_self))) { 211 mReadReport = false; 212 } else { 213 int reportInt; 214 try { 215 reportInt = Integer.parseInt(report); 216 mReadReport = (reportInt == PduHeaders.VALUE_YES); 217 } catch (NumberFormatException nfe) { 218 Log.e(TAG, "Value for read report was invalid."); 219 mReadReport = false; 220 } 221 } 222 223 SlideModel slide = mSlideshow.get(0); 224 if ((slide != null) && slide.hasText()) { 225 TextModel tm = slide.getText(); 226 if (tm.isDrmProtected()) { 227 mBody = mContext.getString(R.string.drm_protected_text); 228 } else { 229 mBody = tm.getText(); 230 } 231 mTextContentType = tm.getContentType(); 232 } 233 234 mMessageSize = mSlideshow.getCurrentMessageSize(); 235 } 236 237 if (!isOutgoingMessage()) { 238 mTimestamp = context.getString(getTimestampStrId(), 239 MessageUtils.formatTimeStampString(context, timestamp)); 240 } 241 } else { 242 throw new MmsException("Unknown type of the message: " + type); 243 } 244 } 245 246 private void interpretFrom(EncodedStringValue from, Uri messageUri) { 247 if (from != null) { 248 mAddress = from.getString(); 249 } else { 250 // In the rare case when getting the "from" address from the pdu fails, 251 // (e.g. from == null) fall back to a slower, yet more reliable method of 252 // getting the address from the "addr" table. This is what the Messaging 253 // notification system uses. 254 mAddress = AddressUtils.getFrom(mContext, messageUri); 255 } 256 mContact = TextUtils.isEmpty(mAddress) ? "" : Contact.get(mAddress, false).getName(); 257 } 258 259 private int getTimestampStrId() { 260 if (PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND == mMessageType) { 261 return R.string.expire_on; 262 } else if (PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF == mMessageType) { 263 return R.string.received_on; 264 } else { 265 return R.string.sent_on; 266 } 267 } 268 269 public boolean isMms() { 270 return mType.equals("mms"); 271 } 272 273 public boolean isSms() { 274 return mType.equals("sms"); 275 } 276 277 public boolean isDownloaded() { 278 return (mMessageType != PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND); 279 } 280 281 public boolean isOutgoingMessage() { 282 boolean isOutgoingMms = isMms() && (mBoxId == Mms.MESSAGE_BOX_OUTBOX); 283 boolean isOutgoingSms = isSms() 284 && ((mBoxId == Sms.MESSAGE_TYPE_FAILED) 285 || (mBoxId == Sms.MESSAGE_TYPE_OUTBOX) 286 || (mBoxId == Sms.MESSAGE_TYPE_QUEUED)); 287 return isOutgoingMms || isOutgoingSms; 288 } 289 290 public boolean isSending() { 291 return !isFailedMessage() && isOutgoingMessage(); 292 } 293 294 public boolean isFailedMessage() { 295 boolean isFailedMms = isMms() 296 && (mErrorType >= MmsSms.ERR_TYPE_GENERIC_PERMANENT); 297 boolean isFailedSms = isSms() 298 && (mBoxId == Sms.MESSAGE_TYPE_FAILED); 299 return isFailedMms || isFailedSms; 300 } 301 302 // Note: This is the only mutable field in this class. Think of 303 // mCachedFormattedMessage as a C++ 'mutable' field on a const 304 // object, with this being a lazy accessor whose logic to set it 305 // is outside the class for model/view separation reasons. In any 306 // case, please keep this class conceptually immutable. 307 public void setCachedFormattedMessage(CharSequence formattedMessage) { 308 mCachedFormattedMessage = formattedMessage; 309 } 310 311 public CharSequence getCachedFormattedMessage() { 312 boolean isSending = isSending(); 313 if (isSending != mLastSendingState) { 314 mLastSendingState = isSending; 315 mCachedFormattedMessage = null; // clear cache so we'll rebuild the message 316 // to show "Sending..." or the sent date. 317 } 318 return mCachedFormattedMessage; 319 } 320 321 public int getBoxId() { 322 return mBoxId; 323 } 324 325 @Override 326 public String toString() { 327 return "type: " + mType + 328 " box: " + mBoxId + 329 " uri: " + mMessageUri + 330 " address: " + mAddress + 331 " contact: " + mContact + 332 " read: " + mReadReport + 333 " delivery status: " + mDeliveryStatus; 334 } 335} 336