1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.messaging.datamodel.data;
18 
19 import android.content.ContentValues;
20 import android.database.Cursor;
21 import android.database.sqlite.SQLiteStatement;
22 import android.net.Uri;
23 import android.os.Parcel;
24 import android.os.Parcelable;
25 import android.text.TextUtils;
26 
27 import com.android.messaging.datamodel.DatabaseHelper;
28 import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
29 import com.android.messaging.datamodel.DatabaseWrapper;
30 import com.android.messaging.sms.MmsUtils;
31 import com.android.messaging.util.Assert;
32 import com.android.messaging.util.BugleGservices;
33 import com.android.messaging.util.BugleGservicesKeys;
34 import com.android.messaging.util.Dates;
35 import com.android.messaging.util.DebugUtils;
36 import com.android.messaging.util.OsUtil;
37 
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.List;
41 
42 public class MessageData implements Parcelable {
43     private static final String[] sProjection = {
44         MessageColumns._ID,
45         MessageColumns.CONVERSATION_ID,
46         MessageColumns.SENDER_PARTICIPANT_ID,
47         MessageColumns.SELF_PARTICIPANT_ID,
48         MessageColumns.SENT_TIMESTAMP,
49         MessageColumns.RECEIVED_TIMESTAMP,
50         MessageColumns.SEEN,
51         MessageColumns.READ,
52         MessageColumns.PROTOCOL,
53         MessageColumns.STATUS,
54         MessageColumns.SMS_MESSAGE_URI,
55         MessageColumns.SMS_PRIORITY,
56         MessageColumns.SMS_MESSAGE_SIZE,
57         MessageColumns.MMS_SUBJECT,
58         MessageColumns.MMS_TRANSACTION_ID,
59         MessageColumns.MMS_CONTENT_LOCATION,
60         MessageColumns.MMS_EXPIRY,
61         MessageColumns.RAW_TELEPHONY_STATUS,
62         MessageColumns.RETRY_START_TIMESTAMP,
63     };
64 
65     private static final int INDEX_ID = 0;
66     private static final int INDEX_CONVERSATION_ID = 1;
67     private static final int INDEX_PARTICIPANT_ID = 2;
68     private static final int INDEX_SELF_ID = 3;
69     private static final int INDEX_SENT_TIMESTAMP = 4;
70     private static final int INDEX_RECEIVED_TIMESTAMP = 5;
71     private static final int INDEX_SEEN = 6;
72     private static final int INDEX_READ = 7;
73     private static final int INDEX_PROTOCOL = 8;
74     private static final int INDEX_BUGLE_STATUS = 9;
75     private static final int INDEX_SMS_MESSAGE_URI = 10;
76     private static final int INDEX_SMS_PRIORITY = 11;
77     private static final int INDEX_SMS_MESSAGE_SIZE = 12;
78     private static final int INDEX_MMS_SUBJECT = 13;
79     private static final int INDEX_MMS_TRANSACTION_ID = 14;
80     private static final int INDEX_MMS_CONTENT_LOCATION = 15;
81     private static final int INDEX_MMS_EXPIRY = 16;
82     private static final int INDEX_RAW_TELEPHONY_STATUS = 17;
83     private static final int INDEX_RETRY_START_TIMESTAMP = 18;
84 
85     // SQL statement to insert a "complete" message row (columns based on the projection above).
86     private static final String INSERT_MESSAGE_SQL =
87             "INSERT INTO " + DatabaseHelper.MESSAGES_TABLE + " ( "
88                     + TextUtils.join(", ", Arrays.copyOfRange(sProjection, 1,
89                             INDEX_RETRY_START_TIMESTAMP + 1))
90                     + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
91 
92     private String mMessageId;
93     private String mConversationId;
94     private String mParticipantId;
95     private String mSelfId;
96     private long mSentTimestamp;
97     private long mReceivedTimestamp;
98     private boolean mSeen;
99     private boolean mRead;
100     private int mProtocol;
101     private Uri mSmsMessageUri;
102     private int mSmsPriority;
103     private long mSmsMessageSize;
104     private String mMmsSubject;
105     private String mMmsTransactionId;
106     private String mMmsContentLocation;
107     private long mMmsExpiry;
108     private int mRawStatus;
109     private int mStatus;
110     private final ArrayList<MessagePartData> mParts;
111     private long mRetryStartTimestamp;
112 
113     // PROTOCOL Values
114     public static final int PROTOCOL_UNKNOWN = -1;              // Unknown type
115     public static final int PROTOCOL_SMS = 0;                   // SMS message
116     public static final int PROTOCOL_MMS = 1;                   // MMS message
117     public static final int PROTOCOL_MMS_PUSH_NOTIFICATION = 2; // MMS WAP push notification
118 
119     // Bugle STATUS Values
120     public static final int BUGLE_STATUS_UNKNOWN = 0;
121 
122     // Outgoing
123     public static final int BUGLE_STATUS_OUTGOING_COMPLETE                = 1;
124     public static final int BUGLE_STATUS_OUTGOING_DELIVERED               = 2;
125     // Transitions to either YET_TO_SEND or SEND_AFTER_PROCESSING depending attachments.
126     public static final int BUGLE_STATUS_OUTGOING_DRAFT                   = 3;
127     public static final int BUGLE_STATUS_OUTGOING_YET_TO_SEND             = 4;
128     public static final int BUGLE_STATUS_OUTGOING_SENDING                 = 5;
129     public static final int BUGLE_STATUS_OUTGOING_RESENDING               = 6;
130     public static final int BUGLE_STATUS_OUTGOING_AWAITING_RETRY          = 7;
131     public static final int BUGLE_STATUS_OUTGOING_FAILED                  = 8;
132     public static final int BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER = 9;
133 
134     // Incoming
135     public static final int BUGLE_STATUS_INCOMING_COMPLETE                   = 100;
136     public static final int BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD     = 101;
137     public static final int BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD   = 102;
138     public static final int BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING         = 103;
139     public static final int BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD     = 104;
140     public static final int BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING           = 105;
141     public static final int BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED            = 106;
142     public static final int BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE   = 107;
143 
getStatusDescription(int status)144     public static final String getStatusDescription(int status) {
145         switch (status) {
146             case BUGLE_STATUS_UNKNOWN:
147                 return "UNKNOWN";
148             case BUGLE_STATUS_OUTGOING_COMPLETE:
149                 return "OUTGOING_COMPLETE";
150             case BUGLE_STATUS_OUTGOING_DELIVERED:
151                 return "OUTGOING_DELIVERED";
152             case BUGLE_STATUS_OUTGOING_DRAFT:
153                 return "OUTGOING_DRAFT";
154             case BUGLE_STATUS_OUTGOING_YET_TO_SEND:
155                 return "OUTGOING_YET_TO_SEND";
156             case BUGLE_STATUS_OUTGOING_SENDING:
157                 return "OUTGOING_SENDING";
158             case BUGLE_STATUS_OUTGOING_RESENDING:
159                 return "OUTGOING_RESENDING";
160             case BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
161                 return "OUTGOING_AWAITING_RETRY";
162             case BUGLE_STATUS_OUTGOING_FAILED:
163                 return "OUTGOING_FAILED";
164             case BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
165                 return "OUTGOING_FAILED_EMERGENCY_NUMBER";
166             case BUGLE_STATUS_INCOMING_COMPLETE:
167                 return "INCOMING_COMPLETE";
168             case BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD:
169                 return "INCOMING_YET_TO_MANUAL_DOWNLOAD";
170             case BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD:
171                 return "INCOMING_RETRYING_MANUAL_DOWNLOAD";
172             case BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING:
173                 return "INCOMING_MANUAL_DOWNLOADING";
174             case BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD:
175                 return "INCOMING_RETRYING_AUTO_DOWNLOAD";
176             case BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING:
177                 return "INCOMING_AUTO_DOWNLOADING";
178             case BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED:
179                 return "INCOMING_DOWNLOAD_FAILED";
180             case BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE:
181                 return "INCOMING_EXPIRED_OR_NOT_AVAILABLE";
182             default:
183                 return String.valueOf(status) + " (check MessageData)";
184         }
185     }
186 
187     // All incoming messages expect to have status >= BUGLE_STATUS_FIRST_INCOMING
188     public static final int BUGLE_STATUS_FIRST_INCOMING = BUGLE_STATUS_INCOMING_COMPLETE;
189 
190     // Detailed MMS failures. Most of the values are defined in PduHeaders. However, a few are
191     // defined here instead. These are never returned in the MMS HTTP response, but are used
192     // internally. The values here must not conflict with any of the existing PduHeader values.
193     public static final int RAW_TELEPHONY_STATUS_UNDEFINED = MmsUtils.PDU_HEADER_VALUE_UNDEFINED;
194     public static final int RAW_TELEPHONY_STATUS_MESSAGE_TOO_BIG = 10000;
195 
196     // Unknown result code for MMS sending/downloading. This is used as the default value
197     // for result code returned from platform MMS API.
198     public static final int UNKNOWN_RESULT_CODE = 0;
199 
200     /**
201      * Create an "empty" message
202      */
MessageData()203     public MessageData() {
204         mParts = new ArrayList<MessagePartData>();
205     }
206 
getProjection()207     public static String[] getProjection() {
208         return sProjection;
209     }
210 
211     /**
212      * Create a draft message for a particular conversation based on supplied content
213      */
createDraftMessage(final String conversationId, final String selfId, final MessageData content)214     public static MessageData createDraftMessage(final String conversationId,
215             final String selfId, final MessageData content) {
216         final MessageData message = new MessageData();
217         message.mStatus = BUGLE_STATUS_OUTGOING_DRAFT;
218         message.mProtocol = PROTOCOL_UNKNOWN;
219         message.mConversationId = conversationId;
220         message.mParticipantId = selfId;
221         message.mReceivedTimestamp = System.currentTimeMillis();
222         if (content == null) {
223             message.mParts.add(MessagePartData.createTextMessagePart(""));
224         } else {
225             if (!TextUtils.isEmpty(content.mParticipantId)) {
226                 message.mParticipantId = content.mParticipantId;
227             }
228             if (!TextUtils.isEmpty(content.mMmsSubject)) {
229                 message.mMmsSubject = content.mMmsSubject;
230             }
231             for (final MessagePartData part : content.getParts()) {
232                 message.mParts.add(part);
233             }
234         }
235         message.mSelfId = selfId;
236         return message;
237     }
238 
239     /**
240      * Create a draft sms message for a particular conversation
241      */
createDraftSmsMessage(final String conversationId, final String selfId, final String messageText)242     public static MessageData createDraftSmsMessage(final String conversationId,
243             final String selfId, final String messageText) {
244         final MessageData message = new MessageData();
245         message.mStatus = BUGLE_STATUS_OUTGOING_DRAFT;
246         message.mProtocol = PROTOCOL_SMS;
247         message.mConversationId = conversationId;
248         message.mParticipantId = selfId;
249         message.mSelfId = selfId;
250         message.mParts.add(MessagePartData.createTextMessagePart(messageText));
251         message.mReceivedTimestamp = System.currentTimeMillis();
252         return message;
253     }
254 
255     /**
256      * Create a draft mms message for a particular conversation
257      */
createDraftMmsMessage(final String conversationId, final String selfId, final String messageText, final String subjectText)258     public static MessageData createDraftMmsMessage(final String conversationId,
259             final String selfId, final String messageText, final String subjectText) {
260         final MessageData message = new MessageData();
261         message.mStatus = BUGLE_STATUS_OUTGOING_DRAFT;
262         message.mProtocol = PROTOCOL_MMS;
263         message.mConversationId = conversationId;
264         message.mParticipantId = selfId;
265         message.mSelfId = selfId;
266         message.mMmsSubject = subjectText;
267         message.mReceivedTimestamp = System.currentTimeMillis();
268         if (!TextUtils.isEmpty(messageText)) {
269             message.mParts.add(MessagePartData.createTextMessagePart(messageText));
270         }
271         return message;
272     }
273 
274     /**
275      * Create a message received from a particular number in a particular conversation
276      */
createReceivedSmsMessage(final Uri uri, final String conversationId, final String participantId, final String selfId, final String messageText, final String subject, final long sent, final long recieved, final boolean seen, final boolean read)277     public static MessageData createReceivedSmsMessage(final Uri uri, final String conversationId,
278             final String participantId, final String selfId, final String messageText,
279             final String subject, final long sent, final long recieved,
280             final boolean seen, final boolean read) {
281         final MessageData message = new MessageData();
282         message.mSmsMessageUri = uri;
283         message.mConversationId = conversationId;
284         message.mParticipantId = participantId;
285         message.mSelfId = selfId;
286         message.mProtocol = PROTOCOL_SMS;
287         message.mStatus = BUGLE_STATUS_INCOMING_COMPLETE;
288         message.mMmsSubject = subject;
289         message.mReceivedTimestamp = recieved;
290         message.mSentTimestamp = sent;
291         message.mParts.add(MessagePartData.createTextMessagePart(messageText));
292         message.mSeen = seen;
293         message.mRead = read;
294         return message;
295     }
296 
297     /**
298      * Create a message not yet associated with a particular conversation
299      */
createSharedMessage(final String messageText, final String subjectText)300     public static MessageData createSharedMessage(final String messageText,
301             final String subjectText) {
302         final MessageData message = new MessageData();
303         message.mStatus = BUGLE_STATUS_OUTGOING_DRAFT;
304         message.mMmsSubject = subjectText;
305         if (!TextUtils.isEmpty(messageText)) {
306             message.mParts.add(MessagePartData.createTextMessagePart(messageText));
307         }
308         return message;
309     }
310 
311     /**
312      * Create a message from Sms table fields
313      */
createSmsMessage(final String messageUri, final String participantId, final String selfId, final String conversationId, final int bugleStatus, final boolean seen, final boolean read, final long sent, final long recieved, final String messageText)314     public static MessageData createSmsMessage(final String messageUri, final String participantId,
315             final String selfId, final String conversationId, final int bugleStatus,
316             final boolean seen, final boolean read, final long sent,
317             final long recieved, final String messageText) {
318         final MessageData message = new MessageData();
319         message.mParticipantId = participantId;
320         message.mSelfId = selfId;
321         message.mConversationId = conversationId;
322         message.mSentTimestamp = sent;
323         message.mReceivedTimestamp = recieved;
324         message.mSeen = seen;
325         message.mRead = read;
326         message.mProtocol = PROTOCOL_SMS;
327         message.mStatus = bugleStatus;
328         message.mSmsMessageUri = Uri.parse(messageUri);
329         message.mParts.add(MessagePartData.createTextMessagePart(messageText));
330         return message;
331     }
332 
333     /**
334      * Create a message from Mms table fields
335      */
createMmsMessage(final String messageUri, final String participantId, final String selfId, final String conversationId, final boolean isNotification, final int bugleStatus, final String contentLocation, final String transactionId, final int smsPriority, final String subject, final boolean seen, final boolean read, final long size, final int rawStatus, final long expiry, final long sent, final long received)336     public static MessageData createMmsMessage(final String messageUri, final String participantId,
337             final String selfId, final String conversationId, final boolean isNotification,
338             final int bugleStatus, final String contentLocation, final String transactionId,
339             final int smsPriority, final String subject, final boolean seen, final boolean read,
340             final long size, final int rawStatus, final long expiry, final long sent,
341             final long received) {
342         final MessageData message = new MessageData();
343         message.mParticipantId = participantId;
344         message.mSelfId = selfId;
345         message.mConversationId = conversationId;
346         message.mSentTimestamp = sent;
347         message.mReceivedTimestamp = received;
348         message.mMmsContentLocation = contentLocation;
349         message.mMmsTransactionId = transactionId;
350         message.mSeen = seen;
351         message.mRead = read;
352         message.mStatus = bugleStatus;
353         message.mProtocol = (isNotification ? PROTOCOL_MMS_PUSH_NOTIFICATION : PROTOCOL_MMS);
354         message.mSmsMessageUri = Uri.parse(messageUri);
355         message.mSmsPriority = smsPriority;
356         message.mSmsMessageSize = size;
357         message.mMmsSubject = subject;
358         message.mMmsExpiry = expiry;
359         message.mRawStatus = rawStatus;
360         if (bugleStatus == BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD ||
361                 bugleStatus == BUGLE_STATUS_OUTGOING_RESENDING) {
362             // Set the retry start timestamp if this message is already in process of retrying
363             // Either as autodownload is starting or sending already in progress (MMS update)
364             message.mRetryStartTimestamp = received;
365         }
366         return message;
367     }
368 
addPart(final MessagePartData part)369     public void addPart(final MessagePartData part) {
370         if (part instanceof PendingAttachmentData) {
371             // Pending attachments may only be added to shared message data that's not associated
372             // with any particular conversation, in order to store shared images.
373             Assert.isTrue(mConversationId == null);
374         }
375         mParts.add(part);
376     }
377 
getParts()378     public Iterable<MessagePartData> getParts() {
379         return mParts;
380     }
381 
bind(final Cursor cursor)382     public void bind(final Cursor cursor) {
383         mMessageId = cursor.getString(INDEX_ID);
384         mConversationId = cursor.getString(INDEX_CONVERSATION_ID);
385         mParticipantId = cursor.getString(INDEX_PARTICIPANT_ID);
386         mSelfId = cursor.getString(INDEX_SELF_ID);
387         mSentTimestamp = cursor.getLong(INDEX_SENT_TIMESTAMP);
388         mReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP);
389         mSeen = (cursor.getInt(INDEX_SEEN) != 0);
390         mRead = (cursor.getInt(INDEX_READ) != 0);
391         mProtocol = cursor.getInt(INDEX_PROTOCOL);
392         mStatus = cursor.getInt(INDEX_BUGLE_STATUS);
393         final String smsMessageUri = cursor.getString(INDEX_SMS_MESSAGE_URI);
394         mSmsMessageUri = (smsMessageUri == null) ? null : Uri.parse(smsMessageUri);
395         mSmsPriority = cursor.getInt(INDEX_SMS_PRIORITY);
396         mSmsMessageSize = cursor.getLong(INDEX_SMS_MESSAGE_SIZE);
397         mMmsExpiry = cursor.getLong(INDEX_MMS_EXPIRY);
398         mRawStatus = cursor.getInt(INDEX_RAW_TELEPHONY_STATUS);
399         mMmsSubject = cursor.getString(INDEX_MMS_SUBJECT);
400         mMmsTransactionId = cursor.getString(INDEX_MMS_TRANSACTION_ID);
401         mMmsContentLocation = cursor.getString(INDEX_MMS_CONTENT_LOCATION);
402         mRetryStartTimestamp = cursor.getLong(INDEX_RETRY_START_TIMESTAMP);
403     }
404 
405     /**
406      * Bind to the draft message data for a conversation. The conversation's self id is used as
407      * the draft's self id.
408      */
bindDraft(final Cursor cursor, final String conversationSelfId)409     public void bindDraft(final Cursor cursor, final String conversationSelfId) {
410         bind(cursor);
411         mSelfId = conversationSelfId;
412     }
413 
getParticipantId(final Cursor cursor)414     protected static String getParticipantId(final Cursor cursor) {
415         return cursor.getString(INDEX_PARTICIPANT_ID);
416     }
417 
populate(final ContentValues values)418     public void populate(final ContentValues values) {
419         values.put(MessageColumns.CONVERSATION_ID, mConversationId);
420         values.put(MessageColumns.SENDER_PARTICIPANT_ID, mParticipantId);
421         values.put(MessageColumns.SELF_PARTICIPANT_ID, mSelfId);
422         values.put(MessageColumns.SENT_TIMESTAMP, mSentTimestamp);
423         values.put(MessageColumns.RECEIVED_TIMESTAMP, mReceivedTimestamp);
424         values.put(MessageColumns.SEEN, mSeen ? 1 : 0);
425         values.put(MessageColumns.READ, mRead ? 1 : 0);
426         values.put(MessageColumns.PROTOCOL, mProtocol);
427         values.put(MessageColumns.STATUS, mStatus);
428         final String smsMessageUri = ((mSmsMessageUri == null) ? null : mSmsMessageUri.toString());
429         values.put(MessageColumns.SMS_MESSAGE_URI, smsMessageUri);
430         values.put(MessageColumns.SMS_PRIORITY, mSmsPriority);
431         values.put(MessageColumns.SMS_MESSAGE_SIZE, mSmsMessageSize);
432         values.put(MessageColumns.MMS_EXPIRY, mMmsExpiry);
433         values.put(MessageColumns.MMS_SUBJECT, mMmsSubject);
434         values.put(MessageColumns.MMS_TRANSACTION_ID, mMmsTransactionId);
435         values.put(MessageColumns.MMS_CONTENT_LOCATION, mMmsContentLocation);
436         values.put(MessageColumns.RAW_TELEPHONY_STATUS, mRawStatus);
437         values.put(MessageColumns.RETRY_START_TIMESTAMP, mRetryStartTimestamp);
438     }
439 
440     /**
441      * Note this is not thread safe so callers need to make sure they own the wrapper + statements
442      * while they call this and use the returned value.
443      */
getInsertStatement(final DatabaseWrapper db)444     public SQLiteStatement getInsertStatement(final DatabaseWrapper db) {
445         final SQLiteStatement insert = db.getStatementInTransaction(
446                 DatabaseWrapper.INDEX_INSERT_MESSAGE, INSERT_MESSAGE_SQL);
447         insert.clearBindings();
448         insert.bindString(INDEX_CONVERSATION_ID, mConversationId);
449         insert.bindString(INDEX_PARTICIPANT_ID, mParticipantId);
450         insert.bindString(INDEX_SELF_ID, mSelfId);
451         insert.bindLong(INDEX_SENT_TIMESTAMP, mSentTimestamp);
452         insert.bindLong(INDEX_RECEIVED_TIMESTAMP, mReceivedTimestamp);
453         insert.bindLong(INDEX_SEEN, mSeen ? 1 : 0);
454         insert.bindLong(INDEX_READ, mRead ? 1 : 0);
455         insert.bindLong(INDEX_PROTOCOL, mProtocol);
456         insert.bindLong(INDEX_BUGLE_STATUS, mStatus);
457         if (mSmsMessageUri != null) {
458             insert.bindString(INDEX_SMS_MESSAGE_URI, mSmsMessageUri.toString());
459         }
460         insert.bindLong(INDEX_SMS_PRIORITY, mSmsPriority);
461         insert.bindLong(INDEX_SMS_MESSAGE_SIZE, mSmsMessageSize);
462         insert.bindLong(INDEX_MMS_EXPIRY, mMmsExpiry);
463         if (mMmsSubject != null) {
464             insert.bindString(INDEX_MMS_SUBJECT, mMmsSubject);
465         }
466         if (mMmsTransactionId != null) {
467             insert.bindString(INDEX_MMS_TRANSACTION_ID, mMmsTransactionId);
468         }
469         if (mMmsContentLocation != null) {
470             insert.bindString(INDEX_MMS_CONTENT_LOCATION, mMmsContentLocation);
471         }
472         insert.bindLong(INDEX_RAW_TELEPHONY_STATUS, mRawStatus);
473         insert.bindLong(INDEX_RETRY_START_TIMESTAMP, mRetryStartTimestamp);
474         return insert;
475     }
476 
getMessageId()477     public final String getMessageId() {
478         return mMessageId;
479     }
480 
getConversationId()481     public final String getConversationId() {
482         return mConversationId;
483     }
484 
getParticipantId()485     public final String getParticipantId() {
486         return mParticipantId;
487     }
488 
getSelfId()489     public final String getSelfId() {
490         return mSelfId;
491     }
492 
getSentTimeStamp()493     public final long getSentTimeStamp() {
494         return mSentTimestamp;
495     }
496 
getReceivedTimeStamp()497     public final long getReceivedTimeStamp() {
498         return mReceivedTimestamp;
499     }
500 
getFormattedReceivedTimeStamp()501     public final String getFormattedReceivedTimeStamp() {
502         return Dates.getMessageTimeString(mReceivedTimestamp).toString();
503     }
504 
getProtocol()505     public final int getProtocol() {
506         return mProtocol;
507     }
508 
getStatus()509     public final int getStatus() {
510         return mStatus;
511     }
512 
getSmsMessageUri()513     public final Uri getSmsMessageUri() {
514         return mSmsMessageUri;
515     }
516 
getSmsPriority()517     public final int getSmsPriority() {
518         return mSmsPriority;
519     }
520 
getSmsMessageSize()521     public final long getSmsMessageSize() {
522         return mSmsMessageSize;
523     }
524 
getMmsSubject()525     public final String getMmsSubject() {
526         return mMmsSubject;
527     }
528 
setMmsSubject(final String subject)529     public final void setMmsSubject(final String subject) {
530         mMmsSubject = subject;
531     }
532 
getMmsContentLocation()533     public final String getMmsContentLocation() {
534         return mMmsContentLocation;
535     }
536 
getMmsTransactionId()537     public final String getMmsTransactionId() {
538         return mMmsTransactionId;
539     }
540 
getMessageSeen()541     public final boolean getMessageSeen() {
542         return mSeen;
543     }
544 
getMmsExpiry()545     public final long getMmsExpiry() {
546         return mMmsExpiry;
547     }
548 
549     /**
550      * For incoming MMS messages this returns the retrieve-status value
551      * For sent MMS messages this returns the response-status value
552      * See PduHeaders.java for possible values
553      * Otherwise (SMS etc) this is RAW_TELEPHONY_STATUS_UNDEFINED
554      */
getRawTelephonyStatus()555     public final int getRawTelephonyStatus() {
556         return mRawStatus;
557     }
558 
setMessageSeen(final boolean hasSeen)559     public final void setMessageSeen(final boolean hasSeen) {
560         mSeen = hasSeen;
561     }
562 
getInResendWindow(final long now)563     public final boolean getInResendWindow(final long now) {
564         final long maxAgeToResend = BugleGservices.get().getLong(
565                 BugleGservicesKeys.MESSAGE_RESEND_TIMEOUT_MS,
566                 BugleGservicesKeys.MESSAGE_RESEND_TIMEOUT_MS_DEFAULT);
567         final long age = now - mRetryStartTimestamp;
568         return age < maxAgeToResend;
569     }
570 
getInDownloadWindow(final long now)571     public final boolean getInDownloadWindow(final long now) {
572         final long maxAgeToRedownload = BugleGservices.get().getLong(
573                 BugleGservicesKeys.MESSAGE_DOWNLOAD_TIMEOUT_MS,
574                 BugleGservicesKeys.MESSAGE_DOWNLOAD_TIMEOUT_MS_DEFAULT);
575         final long age = now - mRetryStartTimestamp;
576         return age < maxAgeToRedownload;
577     }
578 
getShowDownloadMessage(final int status)579     static boolean getShowDownloadMessage(final int status) {
580         if (OsUtil.isSecondaryUser()) {
581             // Secondary users can't download mms's. Mms's are downloaded by bugle running as the
582             // primary user.
583             return false;
584         }
585         // Should show option for manual download if status is manual download or failed
586         return (status == BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED ||
587                 status == BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD ||
588                 // If debug is enabled, allow to download an expired or unavailable message.
589                 (DebugUtils.isDebugEnabled()
590                         && status == BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE));
591     }
592 
canDownloadMessage()593     public boolean canDownloadMessage() {
594         if (OsUtil.isSecondaryUser()) {
595             // Secondary users can't download mms's. Mms's are downloaded by bugle running as the
596             // primary user.
597             return false;
598         }
599         // Can download if status is retrying auto/manual downloading
600         return (mStatus == BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD ||
601                 mStatus == BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD);
602     }
603 
canRedownloadMessage()604     public boolean canRedownloadMessage() {
605         if (OsUtil.isSecondaryUser()) {
606             // Secondary users can't download mms's. Mms's are downloaded by bugle running as the
607             // primary user.
608             return false;
609         }
610         // Can redownload if status is manual download not started or download failed
611         return (mStatus == BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED ||
612                 mStatus == BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD ||
613                 // If debug is enabled, allow to download an expired or unavailable message.
614                 (DebugUtils.isDebugEnabled()
615                         && mStatus == BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE));
616     }
617 
getShowResendMessage(final int status)618     static boolean getShowResendMessage(final int status) {
619         // Should show option to resend if status is failed
620         return (status == BUGLE_STATUS_OUTGOING_FAILED);
621     }
622 
getOneClickResendMessage(final int status, final int rawStatus)623     static boolean getOneClickResendMessage(final int status, final int rawStatus) {
624         // Should show option to resend if status is failed
625         return (status == BUGLE_STATUS_OUTGOING_FAILED
626                 && rawStatus == RAW_TELEPHONY_STATUS_UNDEFINED);
627     }
628 
canResendMessage()629     public boolean canResendMessage() {
630         // Manual retry allowed only from failed
631         return (mStatus == BUGLE_STATUS_OUTGOING_FAILED);
632     }
633 
canSendMessage()634     public boolean canSendMessage() {
635         // Sending messages must be in yet_to_send or awaiting_retry state
636         return (mStatus == BUGLE_STATUS_OUTGOING_YET_TO_SEND ||
637                 mStatus == BUGLE_STATUS_OUTGOING_AWAITING_RETRY);
638     }
639 
getYetToSend()640     public final boolean getYetToSend() {
641         return (mStatus == BUGLE_STATUS_OUTGOING_YET_TO_SEND);
642     }
643 
getIsMms()644     public final boolean getIsMms() {
645         return mProtocol == MessageData.PROTOCOL_MMS
646                 || mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION;
647     }
648 
getIsMmsNotification(final int protocol)649     public static final boolean getIsMmsNotification(final int protocol) {
650         return (protocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION);
651     }
652 
getIsMmsNotification()653     public final boolean getIsMmsNotification() {
654         return getIsMmsNotification(mProtocol);
655     }
656 
getIsSms(final int protocol)657     public static final boolean getIsSms(final int protocol) {
658         return protocol == (MessageData.PROTOCOL_SMS);
659     }
660 
getIsSms()661     public final boolean getIsSms() {
662         return getIsSms(mProtocol);
663     }
664 
getIsIncoming(final int status)665     public static boolean getIsIncoming(final int status) {
666         return (status >= MessageData.BUGLE_STATUS_FIRST_INCOMING);
667     }
668 
getIsIncoming()669     public boolean getIsIncoming() {
670         return getIsIncoming(mStatus);
671     }
672 
getRetryStartTimestamp()673     public long getRetryStartTimestamp() {
674         return mRetryStartTimestamp;
675     }
676 
getMessageText()677     public final String getMessageText() {
678         final String separator = System.getProperty("line.separator");
679         final StringBuilder text = new StringBuilder();
680         for (final MessagePartData part : mParts) {
681             if (!part.isAttachment() && !TextUtils.isEmpty(part.getText())) {
682                 if (text.length() > 0) {
683                     text.append(separator);
684                 }
685                 text.append(part.getText());
686             }
687         }
688         return text.toString();
689     }
690 
691     /**
692      * Takes all captions from attachments and adds them as a prefix to the first text part or
693      * appends a text part
694      */
consolidateText()695     public final void consolidateText() {
696         final String separator = System.getProperty("line.separator");
697         final StringBuilder captionText = new StringBuilder();
698         MessagePartData firstTextPart = null;
699         int firstTextPartIndex = -1;
700         for (int i = 0; i < mParts.size(); i++) {
701             final MessagePartData part = mParts.get(i);
702             if (firstTextPart == null && !part.isAttachment()) {
703                 firstTextPart = part;
704                 firstTextPartIndex = i;
705             }
706             if (part.isAttachment() && !TextUtils.isEmpty(part.getText())) {
707                 if (captionText.length() > 0) {
708                     captionText.append(separator);
709                 }
710                 captionText.append(part.getText());
711             }
712         }
713 
714         if (captionText.length() == 0) {
715             // Nothing to consolidate
716             return;
717         }
718 
719         if (firstTextPart == null) {
720             addPart(MessagePartData.createTextMessagePart(captionText.toString()));
721         } else {
722             final String partText = firstTextPart.getText();
723             if (partText.length() > 0) {
724                 captionText.append(separator);
725                 captionText.append(partText);
726             }
727             mParts.set(firstTextPartIndex,
728                     MessagePartData.createTextMessagePart(captionText.toString()));
729         }
730     }
731 
getFirstAttachment()732     public final MessagePartData getFirstAttachment() {
733         for (final MessagePartData part : mParts) {
734             if (part.isAttachment()) {
735                 return part;
736             }
737         }
738         return null;
739     }
740 
741     /**
742      * Updates the messageId for this message.
743      * Can be used to reset the messageId prior to persisting (which will assign a new messageId)
744      *  or can be called on a message that does not yet have a valid messageId to set it.
745      */
updateMessageId(final String messageId)746     public void updateMessageId(final String messageId) {
747         Assert.isTrue(TextUtils.isEmpty(messageId) || TextUtils.isEmpty(mMessageId));
748         mMessageId = messageId;
749 
750         // TODO : This should probably also call updateMessageId on the message parts. We
751         // may also want to make messages effectively immutable once they have a valid message id.
752     }
753 
updateSendingMessage(final String conversationId, final Uri messageUri, final long timestamp)754     public final void updateSendingMessage(final String conversationId, final Uri messageUri,
755             final long timestamp) {
756         mConversationId = conversationId;
757         mSmsMessageUri = messageUri;
758         mRead = true;
759         mSeen = true;
760         mReceivedTimestamp = timestamp;
761         mSentTimestamp = timestamp;
762         mStatus = BUGLE_STATUS_OUTGOING_YET_TO_SEND;
763         mRetryStartTimestamp = timestamp;
764     }
765 
markMessageManualResend(final long timestamp)766     public final void markMessageManualResend(final long timestamp) {
767         // Manual send updates timestamp and transitions back to initial sending status.
768         mReceivedTimestamp = timestamp;
769         mSentTimestamp = timestamp;
770         mStatus = BUGLE_STATUS_OUTGOING_SENDING;
771     }
772 
markMessageSending(final long timestamp)773     public final void markMessageSending(final long timestamp) {
774         // Initial send
775         mStatus = BUGLE_STATUS_OUTGOING_SENDING;
776         mSentTimestamp = timestamp;
777     }
778 
markMessageResending(final long timestamp)779     public final void markMessageResending(final long timestamp) {
780         // Auto resend of message
781         mStatus = BUGLE_STATUS_OUTGOING_RESENDING;
782         mSentTimestamp = timestamp;
783     }
784 
markMessageSent(final long timestamp)785     public final void markMessageSent(final long timestamp) {
786         mSentTimestamp = timestamp;
787         mStatus = BUGLE_STATUS_OUTGOING_COMPLETE;
788     }
789 
markMessageFailed(final long timestamp)790     public final void markMessageFailed(final long timestamp) {
791         mSentTimestamp = timestamp;
792         mStatus = BUGLE_STATUS_OUTGOING_FAILED;
793     }
794 
markMessageFailedEmergencyNumber(final long timestamp)795     public final void markMessageFailedEmergencyNumber(final long timestamp) {
796         mSentTimestamp = timestamp;
797         mStatus = BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER;
798     }
799 
markMessageNotSent(final long timestamp)800     public final void markMessageNotSent(final long timestamp) {
801         mSentTimestamp = timestamp;
802         mStatus = BUGLE_STATUS_OUTGOING_AWAITING_RETRY;
803     }
804 
updateSizesForImageParts()805     public final void updateSizesForImageParts() {
806         for (final MessagePartData part : getParts()) {
807             part.decodeAndSaveSizeIfImage(false /* saveToStorage */);
808         }
809     }
810 
setRetryStartTimestamp(final long timestamp)811     public final void setRetryStartTimestamp(final long timestamp) {
812         mRetryStartTimestamp = timestamp;
813     }
814 
setRawTelephonyStatus(final int rawStatus)815     public final void setRawTelephonyStatus(final int rawStatus) {
816         mRawStatus = rawStatus;
817     }
818 
hasContent()819     public boolean hasContent() {
820         return !TextUtils.isEmpty(mMmsSubject) ||
821                 getFirstAttachment() != null ||
822                 !TextUtils.isEmpty(getMessageText());
823     }
824 
bindSelfId(final String selfId)825     public final void bindSelfId(final String selfId) {
826         mSelfId = selfId;
827     }
828 
bindParticipantId(final String participantId)829     public final void bindParticipantId(final String participantId) {
830         mParticipantId = participantId;
831     }
832 
MessageData(final Parcel in)833     protected MessageData(final Parcel in) {
834         mMessageId = in.readString();
835         mConversationId = in.readString();
836         mParticipantId = in.readString();
837         mSelfId = in.readString();
838         mSentTimestamp = in.readLong();
839         mReceivedTimestamp = in.readLong();
840         mSeen = (in.readInt() != 0);
841         mRead = (in.readInt() != 0);
842         mProtocol = in.readInt();
843         mStatus = in.readInt();
844         final String smsMessageUri = in.readString();
845         mSmsMessageUri = (smsMessageUri == null ? null : Uri.parse(smsMessageUri));
846         mSmsPriority = in.readInt();
847         mSmsMessageSize = in.readLong();
848         mMmsExpiry = in.readLong();
849         mMmsSubject = in.readString();
850         mMmsTransactionId = in.readString();
851         mMmsContentLocation = in.readString();
852         mRawStatus = in.readInt();
853         mRetryStartTimestamp = in.readLong();
854 
855         // Read parts
856         mParts = new ArrayList<MessagePartData>();
857         final int partCount = in.readInt();
858         for (int i = 0; i < partCount; i++) {
859             mParts.add((MessagePartData) in.readParcelable(MessagePartData.class.getClassLoader()));
860         }
861     }
862 
863     @Override
describeContents()864     public int describeContents() {
865         return 0;
866     }
867 
868     @Override
writeToParcel(final Parcel dest, final int flags)869     public void writeToParcel(final Parcel dest, final int flags) {
870         dest.writeString(mMessageId);
871         dest.writeString(mConversationId);
872         dest.writeString(mParticipantId);
873         dest.writeString(mSelfId);
874         dest.writeLong(mSentTimestamp);
875         dest.writeLong(mReceivedTimestamp);
876         dest.writeInt(mRead ? 1 : 0);
877         dest.writeInt(mSeen ? 1 : 0);
878         dest.writeInt(mProtocol);
879         dest.writeInt(mStatus);
880         final String smsMessageUri = (mSmsMessageUri == null) ? null : mSmsMessageUri.toString();
881         dest.writeString(smsMessageUri);
882         dest.writeInt(mSmsPriority);
883         dest.writeLong(mSmsMessageSize);
884         dest.writeLong(mMmsExpiry);
885         dest.writeString(mMmsSubject);
886         dest.writeString(mMmsTransactionId);
887         dest.writeString(mMmsContentLocation);
888         dest.writeInt(mRawStatus);
889         dest.writeLong(mRetryStartTimestamp);
890 
891         // Write parts
892         dest.writeInt(mParts.size());
893         for (final MessagePartData messagePartData : mParts) {
894             dest.writeParcelable(messagePartData, flags);
895         }
896     }
897 
898     public static final Parcelable.Creator<MessageData> CREATOR
899             = new Parcelable.Creator<MessageData>() {
900         @Override
901         public MessageData createFromParcel(final Parcel in) {
902             return new MessageData(in);
903         }
904 
905         @Override
906         public MessageData[] newArray(final int size) {
907             return new MessageData[size];
908         }
909     };
910 
911     @Override
toString()912     public String toString() {
913         return toString(mMessageId, mParts);
914     }
915 
toString(String messageId, List<MessagePartData> parts)916     public static String toString(String messageId, List<MessagePartData> parts) {
917         StringBuilder sb = new StringBuilder();
918         if (messageId != null) {
919             sb.append(messageId);
920             sb.append(": ");
921         }
922         for (MessagePartData part : parts) {
923             sb.append(part.toString());
924             sb.append(" ");
925         }
926         return sb.toString();
927     }
928 }
929