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)300     public static MessageData createSharedMessage(final String messageText) {
301         final MessageData message = new MessageData();
302         message.mStatus = BUGLE_STATUS_OUTGOING_DRAFT;
303         if (!TextUtils.isEmpty(messageText)) {
304             message.mParts.add(MessagePartData.createTextMessagePart(messageText));
305         }
306         return message;
307     }
308 
309     /**
310      * Create a message from Sms table fields
311      */
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)312     public static MessageData createSmsMessage(final String messageUri, final String participantId,
313             final String selfId, final String conversationId, final int bugleStatus,
314             final boolean seen, final boolean read, final long sent,
315             final long recieved, final String messageText) {
316         final MessageData message = new MessageData();
317         message.mParticipantId = participantId;
318         message.mSelfId = selfId;
319         message.mConversationId = conversationId;
320         message.mSentTimestamp = sent;
321         message.mReceivedTimestamp = recieved;
322         message.mSeen = seen;
323         message.mRead = read;
324         message.mProtocol = PROTOCOL_SMS;
325         message.mStatus = bugleStatus;
326         message.mSmsMessageUri = Uri.parse(messageUri);
327         message.mParts.add(MessagePartData.createTextMessagePart(messageText));
328         return message;
329     }
330 
331     /**
332      * Create a message from Mms table fields
333      */
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)334     public static MessageData createMmsMessage(final String messageUri, final String participantId,
335             final String selfId, final String conversationId, final boolean isNotification,
336             final int bugleStatus, final String contentLocation, final String transactionId,
337             final int smsPriority, final String subject, final boolean seen, final boolean read,
338             final long size, final int rawStatus, final long expiry, final long sent,
339             final long received) {
340         final MessageData message = new MessageData();
341         message.mParticipantId = participantId;
342         message.mSelfId = selfId;
343         message.mConversationId = conversationId;
344         message.mSentTimestamp = sent;
345         message.mReceivedTimestamp = received;
346         message.mMmsContentLocation = contentLocation;
347         message.mMmsTransactionId = transactionId;
348         message.mSeen = seen;
349         message.mRead = read;
350         message.mStatus = bugleStatus;
351         message.mProtocol = (isNotification ? PROTOCOL_MMS_PUSH_NOTIFICATION : PROTOCOL_MMS);
352         message.mSmsMessageUri = Uri.parse(messageUri);
353         message.mSmsPriority = smsPriority;
354         message.mSmsMessageSize = size;
355         message.mMmsSubject = subject;
356         message.mMmsExpiry = expiry;
357         message.mRawStatus = rawStatus;
358         if (bugleStatus == BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD ||
359                 bugleStatus == BUGLE_STATUS_OUTGOING_RESENDING) {
360             // Set the retry start timestamp if this message is already in process of retrying
361             // Either as autodownload is starting or sending already in progress (MMS update)
362             message.mRetryStartTimestamp = received;
363         }
364         return message;
365     }
366 
addPart(final MessagePartData part)367     public void addPart(final MessagePartData part) {
368         if (part instanceof PendingAttachmentData) {
369             // Pending attachments may only be added to shared message data that's not associated
370             // with any particular conversation, in order to store shared images.
371             Assert.isTrue(mConversationId == null);
372         }
373         mParts.add(part);
374     }
375 
getParts()376     public Iterable<MessagePartData> getParts() {
377         return mParts;
378     }
379 
bind(final Cursor cursor)380     public void bind(final Cursor cursor) {
381         mMessageId = cursor.getString(INDEX_ID);
382         mConversationId = cursor.getString(INDEX_CONVERSATION_ID);
383         mParticipantId = cursor.getString(INDEX_PARTICIPANT_ID);
384         mSelfId = cursor.getString(INDEX_SELF_ID);
385         mSentTimestamp = cursor.getLong(INDEX_SENT_TIMESTAMP);
386         mReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP);
387         mSeen = (cursor.getInt(INDEX_SEEN) != 0);
388         mRead = (cursor.getInt(INDEX_READ) != 0);
389         mProtocol = cursor.getInt(INDEX_PROTOCOL);
390         mStatus = cursor.getInt(INDEX_BUGLE_STATUS);
391         final String smsMessageUri = cursor.getString(INDEX_SMS_MESSAGE_URI);
392         mSmsMessageUri = (smsMessageUri == null) ? null : Uri.parse(smsMessageUri);
393         mSmsPriority = cursor.getInt(INDEX_SMS_PRIORITY);
394         mSmsMessageSize = cursor.getLong(INDEX_SMS_MESSAGE_SIZE);
395         mMmsExpiry = cursor.getLong(INDEX_MMS_EXPIRY);
396         mRawStatus = cursor.getInt(INDEX_RAW_TELEPHONY_STATUS);
397         mMmsSubject = cursor.getString(INDEX_MMS_SUBJECT);
398         mMmsTransactionId = cursor.getString(INDEX_MMS_TRANSACTION_ID);
399         mMmsContentLocation = cursor.getString(INDEX_MMS_CONTENT_LOCATION);
400         mRetryStartTimestamp = cursor.getLong(INDEX_RETRY_START_TIMESTAMP);
401     }
402 
403     /**
404      * Bind to the draft message data for a conversation. The conversation's self id is used as
405      * the draft's self id.
406      */
bindDraft(final Cursor cursor, final String conversationSelfId)407     public void bindDraft(final Cursor cursor, final String conversationSelfId) {
408         bind(cursor);
409         mSelfId = conversationSelfId;
410     }
411 
getParticipantId(final Cursor cursor)412     protected static String getParticipantId(final Cursor cursor) {
413         return cursor.getString(INDEX_PARTICIPANT_ID);
414     }
415 
populate(final ContentValues values)416     public void populate(final ContentValues values) {
417         values.put(MessageColumns.CONVERSATION_ID, mConversationId);
418         values.put(MessageColumns.SENDER_PARTICIPANT_ID, mParticipantId);
419         values.put(MessageColumns.SELF_PARTICIPANT_ID, mSelfId);
420         values.put(MessageColumns.SENT_TIMESTAMP, mSentTimestamp);
421         values.put(MessageColumns.RECEIVED_TIMESTAMP, mReceivedTimestamp);
422         values.put(MessageColumns.SEEN, mSeen ? 1 : 0);
423         values.put(MessageColumns.READ, mRead ? 1 : 0);
424         values.put(MessageColumns.PROTOCOL, mProtocol);
425         values.put(MessageColumns.STATUS, mStatus);
426         final String smsMessageUri = ((mSmsMessageUri == null) ? null : mSmsMessageUri.toString());
427         values.put(MessageColumns.SMS_MESSAGE_URI, smsMessageUri);
428         values.put(MessageColumns.SMS_PRIORITY, mSmsPriority);
429         values.put(MessageColumns.SMS_MESSAGE_SIZE, mSmsMessageSize);
430         values.put(MessageColumns.MMS_EXPIRY, mMmsExpiry);
431         values.put(MessageColumns.MMS_SUBJECT, mMmsSubject);
432         values.put(MessageColumns.MMS_TRANSACTION_ID, mMmsTransactionId);
433         values.put(MessageColumns.MMS_CONTENT_LOCATION, mMmsContentLocation);
434         values.put(MessageColumns.RAW_TELEPHONY_STATUS, mRawStatus);
435         values.put(MessageColumns.RETRY_START_TIMESTAMP, mRetryStartTimestamp);
436     }
437 
438     /**
439      * Note this is not thread safe so callers need to make sure they own the wrapper + statements
440      * while they call this and use the returned value.
441      */
getInsertStatement(final DatabaseWrapper db)442     public SQLiteStatement getInsertStatement(final DatabaseWrapper db) {
443         final SQLiteStatement insert = db.getStatementInTransaction(
444                 DatabaseWrapper.INDEX_INSERT_MESSAGE, INSERT_MESSAGE_SQL);
445         insert.clearBindings();
446         insert.bindString(INDEX_CONVERSATION_ID, mConversationId);
447         insert.bindString(INDEX_PARTICIPANT_ID, mParticipantId);
448         insert.bindString(INDEX_SELF_ID, mSelfId);
449         insert.bindLong(INDEX_SENT_TIMESTAMP, mSentTimestamp);
450         insert.bindLong(INDEX_RECEIVED_TIMESTAMP, mReceivedTimestamp);
451         insert.bindLong(INDEX_SEEN, mSeen ? 1 : 0);
452         insert.bindLong(INDEX_READ, mRead ? 1 : 0);
453         insert.bindLong(INDEX_PROTOCOL, mProtocol);
454         insert.bindLong(INDEX_BUGLE_STATUS, mStatus);
455         if (mSmsMessageUri != null) {
456             insert.bindString(INDEX_SMS_MESSAGE_URI, mSmsMessageUri.toString());
457         }
458         insert.bindLong(INDEX_SMS_PRIORITY, mSmsPriority);
459         insert.bindLong(INDEX_SMS_MESSAGE_SIZE, mSmsMessageSize);
460         insert.bindLong(INDEX_MMS_EXPIRY, mMmsExpiry);
461         if (mMmsSubject != null) {
462             insert.bindString(INDEX_MMS_SUBJECT, mMmsSubject);
463         }
464         if (mMmsTransactionId != null) {
465             insert.bindString(INDEX_MMS_TRANSACTION_ID, mMmsTransactionId);
466         }
467         if (mMmsContentLocation != null) {
468             insert.bindString(INDEX_MMS_CONTENT_LOCATION, mMmsContentLocation);
469         }
470         insert.bindLong(INDEX_RAW_TELEPHONY_STATUS, mRawStatus);
471         insert.bindLong(INDEX_RETRY_START_TIMESTAMP, mRetryStartTimestamp);
472         return insert;
473     }
474 
getMessageId()475     public final String getMessageId() {
476         return mMessageId;
477     }
478 
getConversationId()479     public final String getConversationId() {
480         return mConversationId;
481     }
482 
getParticipantId()483     public final String getParticipantId() {
484         return mParticipantId;
485     }
486 
getSelfId()487     public final String getSelfId() {
488         return mSelfId;
489     }
490 
getSentTimeStamp()491     public final long getSentTimeStamp() {
492         return mSentTimestamp;
493     }
494 
getReceivedTimeStamp()495     public final long getReceivedTimeStamp() {
496         return mReceivedTimestamp;
497     }
498 
getFormattedReceivedTimeStamp()499     public final String getFormattedReceivedTimeStamp() {
500         return Dates.getMessageTimeString(mReceivedTimestamp).toString();
501     }
502 
getProtocol()503     public final int getProtocol() {
504         return mProtocol;
505     }
506 
getStatus()507     public final int getStatus() {
508         return mStatus;
509     }
510 
getSmsMessageUri()511     public final Uri getSmsMessageUri() {
512         return mSmsMessageUri;
513     }
514 
getSmsPriority()515     public final int getSmsPriority() {
516         return mSmsPriority;
517     }
518 
getSmsMessageSize()519     public final long getSmsMessageSize() {
520         return mSmsMessageSize;
521     }
522 
getMmsSubject()523     public final String getMmsSubject() {
524         return mMmsSubject;
525     }
526 
setMmsSubject(final String subject)527     public final void setMmsSubject(final String subject) {
528         mMmsSubject = subject;
529     }
530 
getMmsContentLocation()531     public final String getMmsContentLocation() {
532         return mMmsContentLocation;
533     }
534 
getMmsTransactionId()535     public final String getMmsTransactionId() {
536         return mMmsTransactionId;
537     }
538 
getMessageSeen()539     public final boolean getMessageSeen() {
540         return mSeen;
541     }
542 
getMmsExpiry()543     public final long getMmsExpiry() {
544         return mMmsExpiry;
545     }
546 
547     /**
548      * For incoming MMS messages this returns the retrieve-status value
549      * For sent MMS messages this returns the response-status value
550      * See PduHeaders.java for possible values
551      * Otherwise (SMS etc) this is RAW_TELEPHONY_STATUS_UNDEFINED
552      */
getRawTelephonyStatus()553     public final int getRawTelephonyStatus() {
554         return mRawStatus;
555     }
556 
setMessageSeen(final boolean hasSeen)557     public final void setMessageSeen(final boolean hasSeen) {
558         mSeen = hasSeen;
559     }
560 
getInResendWindow(final long now)561     public final boolean getInResendWindow(final long now) {
562         final long maxAgeToResend = BugleGservices.get().getLong(
563                 BugleGservicesKeys.MESSAGE_RESEND_TIMEOUT_MS,
564                 BugleGservicesKeys.MESSAGE_RESEND_TIMEOUT_MS_DEFAULT);
565         final long age = now - mRetryStartTimestamp;
566         return age < maxAgeToResend;
567     }
568 
getInDownloadWindow(final long now)569     public final boolean getInDownloadWindow(final long now) {
570         final long maxAgeToRedownload = BugleGservices.get().getLong(
571                 BugleGservicesKeys.MESSAGE_DOWNLOAD_TIMEOUT_MS,
572                 BugleGservicesKeys.MESSAGE_DOWNLOAD_TIMEOUT_MS_DEFAULT);
573         final long age = now - mRetryStartTimestamp;
574         return age < maxAgeToRedownload;
575     }
576 
getShowDownloadMessage(final int status)577     static boolean getShowDownloadMessage(final int status) {
578         if (OsUtil.isSecondaryUser()) {
579             // Secondary users can't download mms's. Mms's are downloaded by bugle running as the
580             // primary user.
581             return false;
582         }
583         // Should show option for manual download iff status is manual download or failed
584         return (status == BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED ||
585                 status == BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD ||
586                 // If debug is enabled, allow to download an expired or unavailable message.
587                 (DebugUtils.isDebugEnabled()
588                         && status == BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE));
589     }
590 
canDownloadMessage()591     public boolean canDownloadMessage() {
592         if (OsUtil.isSecondaryUser()) {
593             // Secondary users can't download mms's. Mms's are downloaded by bugle running as the
594             // primary user.
595             return false;
596         }
597         // Can download iff status is retrying auto/manual downloading
598         return (mStatus == BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD ||
599                 mStatus == BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD);
600     }
601 
canRedownloadMessage()602     public boolean canRedownloadMessage() {
603         if (OsUtil.isSecondaryUser()) {
604             // Secondary users can't download mms's. Mms's are downloaded by bugle running as the
605             // primary user.
606             return false;
607         }
608         // Can redownload iff status is manual download not started or download failed
609         return (mStatus == BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED ||
610                 mStatus == BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD ||
611                 // If debug is enabled, allow to download an expired or unavailable message.
612                 (DebugUtils.isDebugEnabled()
613                         && mStatus == BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE));
614     }
615 
getShowResendMessage(final int status)616     static boolean getShowResendMessage(final int status) {
617         // Should show option to resend iff status is failed
618         return (status == BUGLE_STATUS_OUTGOING_FAILED);
619     }
620 
getOneClickResendMessage(final int status, final int rawStatus)621     static boolean getOneClickResendMessage(final int status, final int rawStatus) {
622         // Should show option to resend iff status is failed
623         return (status == BUGLE_STATUS_OUTGOING_FAILED
624                 && rawStatus == RAW_TELEPHONY_STATUS_UNDEFINED);
625     }
626 
canResendMessage()627     public boolean canResendMessage() {
628         // Manual retry allowed only from failed
629         return (mStatus == BUGLE_STATUS_OUTGOING_FAILED);
630     }
631 
canSendMessage()632     public boolean canSendMessage() {
633         // Sending messages must be in yet_to_send or awaiting_retry state
634         return (mStatus == BUGLE_STATUS_OUTGOING_YET_TO_SEND ||
635                 mStatus == BUGLE_STATUS_OUTGOING_AWAITING_RETRY);
636     }
637 
getYetToSend()638     public final boolean getYetToSend() {
639         return (mStatus == BUGLE_STATUS_OUTGOING_YET_TO_SEND);
640     }
641 
getIsMms()642     public final boolean getIsMms() {
643         return mProtocol == MessageData.PROTOCOL_MMS
644                 || mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION;
645     }
646 
getIsMmsNotification(final int protocol)647     public static final boolean getIsMmsNotification(final int protocol) {
648         return (protocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION);
649     }
650 
getIsMmsNotification()651     public final boolean getIsMmsNotification() {
652         return getIsMmsNotification(mProtocol);
653     }
654 
getIsSms(final int protocol)655     public static final boolean getIsSms(final int protocol) {
656         return protocol == (MessageData.PROTOCOL_SMS);
657     }
658 
getIsSms()659     public final boolean getIsSms() {
660         return getIsSms(mProtocol);
661     }
662 
getIsIncoming(final int status)663     public static boolean getIsIncoming(final int status) {
664         return (status >= MessageData.BUGLE_STATUS_FIRST_INCOMING);
665     }
666 
getIsIncoming()667     public boolean getIsIncoming() {
668         return getIsIncoming(mStatus);
669     }
670 
getRetryStartTimestamp()671     public long getRetryStartTimestamp() {
672         return mRetryStartTimestamp;
673     }
674 
getMessageText()675     public final String getMessageText() {
676         final String separator = System.getProperty("line.separator");
677         final StringBuilder text = new StringBuilder();
678         for (final MessagePartData part : mParts) {
679             if (!part.isAttachment() && !TextUtils.isEmpty(part.getText())) {
680                 if (text.length() > 0) {
681                     text.append(separator);
682                 }
683                 text.append(part.getText());
684             }
685         }
686         return text.toString();
687     }
688 
689     /**
690      * Takes all captions from attachments and adds them as a prefix to the first text part or
691      * appends a text part
692      */
consolidateText()693     public final void consolidateText() {
694         final String separator = System.getProperty("line.separator");
695         final StringBuilder captionText = new StringBuilder();
696         MessagePartData firstTextPart = null;
697         int firstTextPartIndex = -1;
698         for (int i = 0; i < mParts.size(); i++) {
699             final MessagePartData part = mParts.get(i);
700             if (firstTextPart == null && !part.isAttachment()) {
701                 firstTextPart = part;
702                 firstTextPartIndex = i;
703             }
704             if (part.isAttachment() && !TextUtils.isEmpty(part.getText())) {
705                 if (captionText.length() > 0) {
706                     captionText.append(separator);
707                 }
708                 captionText.append(part.getText());
709             }
710         }
711 
712         if (captionText.length() == 0) {
713             // Nothing to consolidate
714             return;
715         }
716 
717         if (firstTextPart == null) {
718             addPart(MessagePartData.createTextMessagePart(captionText.toString()));
719         } else {
720             final String partText = firstTextPart.getText();
721             if (partText.length() > 0) {
722                 captionText.append(separator);
723                 captionText.append(partText);
724             }
725             mParts.set(firstTextPartIndex,
726                     MessagePartData.createTextMessagePart(captionText.toString()));
727         }
728     }
729 
getFirstAttachment()730     public final MessagePartData getFirstAttachment() {
731         for (final MessagePartData part : mParts) {
732             if (part.isAttachment()) {
733                 return part;
734             }
735         }
736         return null;
737     }
738 
739     /**
740      * Updates the messageId for this message.
741      * Can be used to reset the messageId prior to persisting (which will assign a new messageId)
742      *  or can be called on a message that does not yet have a valid messageId to set it.
743      */
updateMessageId(final String messageId)744     public void updateMessageId(final String messageId) {
745         Assert.isTrue(TextUtils.isEmpty(messageId) || TextUtils.isEmpty(mMessageId));
746         mMessageId = messageId;
747 
748         // TODO : This should probably also call updateMessageId on the message parts. We
749         // may also want to make messages effectively immutable once they have a valid message id.
750     }
751 
updateSendingMessage(final String conversationId, final Uri messageUri, final long timestamp)752     public final void updateSendingMessage(final String conversationId, final Uri messageUri,
753             final long timestamp) {
754         mConversationId = conversationId;
755         mSmsMessageUri = messageUri;
756         mRead = true;
757         mSeen = true;
758         mReceivedTimestamp = timestamp;
759         mSentTimestamp = timestamp;
760         mStatus = BUGLE_STATUS_OUTGOING_YET_TO_SEND;
761         mRetryStartTimestamp = timestamp;
762     }
763 
markMessageManualResend(final long timestamp)764     public final void markMessageManualResend(final long timestamp) {
765         // Manual send updates timestamp and transitions back to initial sending status.
766         mReceivedTimestamp = timestamp;
767         mSentTimestamp = timestamp;
768         mStatus = BUGLE_STATUS_OUTGOING_SENDING;
769     }
770 
markMessageSending(final long timestamp)771     public final void markMessageSending(final long timestamp) {
772         // Initial send
773         mStatus = BUGLE_STATUS_OUTGOING_SENDING;
774         mSentTimestamp = timestamp;
775     }
776 
markMessageResending(final long timestamp)777     public final void markMessageResending(final long timestamp) {
778         // Auto resend of message
779         mStatus = BUGLE_STATUS_OUTGOING_RESENDING;
780         mSentTimestamp = timestamp;
781     }
782 
markMessageSent(final long timestamp)783     public final void markMessageSent(final long timestamp) {
784         mSentTimestamp = timestamp;
785         mStatus = BUGLE_STATUS_OUTGOING_COMPLETE;
786     }
787 
markMessageFailed(final long timestamp)788     public final void markMessageFailed(final long timestamp) {
789         mSentTimestamp = timestamp;
790         mStatus = BUGLE_STATUS_OUTGOING_FAILED;
791     }
792 
markMessageFailedEmergencyNumber(final long timestamp)793     public final void markMessageFailedEmergencyNumber(final long timestamp) {
794         mSentTimestamp = timestamp;
795         mStatus = BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER;
796     }
797 
markMessageNotSent(final long timestamp)798     public final void markMessageNotSent(final long timestamp) {
799         mSentTimestamp = timestamp;
800         mStatus = BUGLE_STATUS_OUTGOING_AWAITING_RETRY;
801     }
802 
updateSizesForImageParts()803     public final void updateSizesForImageParts() {
804         for (final MessagePartData part : getParts()) {
805             part.decodeAndSaveSizeIfImage(false /* saveToStorage */);
806         }
807     }
808 
setRetryStartTimestamp(final long timestamp)809     public final void setRetryStartTimestamp(final long timestamp) {
810         mRetryStartTimestamp = timestamp;
811     }
812 
setRawTelephonyStatus(final int rawStatus)813     public final void setRawTelephonyStatus(final int rawStatus) {
814         mRawStatus = rawStatus;
815     }
816 
hasContent()817     public boolean hasContent() {
818         return !TextUtils.isEmpty(mMmsSubject) ||
819                 getFirstAttachment() != null ||
820                 !TextUtils.isEmpty(getMessageText());
821     }
822 
bindSelfId(final String selfId)823     public final void bindSelfId(final String selfId) {
824         mSelfId = selfId;
825     }
826 
bindParticipantId(final String participantId)827     public final void bindParticipantId(final String participantId) {
828         mParticipantId = participantId;
829     }
830 
MessageData(final Parcel in)831     protected MessageData(final Parcel in) {
832         mMessageId = in.readString();
833         mConversationId = in.readString();
834         mParticipantId = in.readString();
835         mSelfId = in.readString();
836         mSentTimestamp = in.readLong();
837         mReceivedTimestamp = in.readLong();
838         mSeen = (in.readInt() != 0);
839         mRead = (in.readInt() != 0);
840         mProtocol = in.readInt();
841         mStatus = in.readInt();
842         final String smsMessageUri = in.readString();
843         mSmsMessageUri = (smsMessageUri == null ? null : Uri.parse(smsMessageUri));
844         mSmsPriority = in.readInt();
845         mSmsMessageSize = in.readLong();
846         mMmsExpiry = in.readLong();
847         mMmsSubject = in.readString();
848         mMmsTransactionId = in.readString();
849         mMmsContentLocation = in.readString();
850         mRawStatus = in.readInt();
851         mRetryStartTimestamp = in.readLong();
852 
853         // Read parts
854         mParts = new ArrayList<MessagePartData>();
855         final int partCount = in.readInt();
856         for (int i = 0; i < partCount; i++) {
857             mParts.add((MessagePartData) in.readParcelable(MessagePartData.class.getClassLoader()));
858         }
859     }
860 
861     @Override
describeContents()862     public int describeContents() {
863         return 0;
864     }
865 
866     @Override
writeToParcel(final Parcel dest, final int flags)867     public void writeToParcel(final Parcel dest, final int flags) {
868         dest.writeString(mMessageId);
869         dest.writeString(mConversationId);
870         dest.writeString(mParticipantId);
871         dest.writeString(mSelfId);
872         dest.writeLong(mSentTimestamp);
873         dest.writeLong(mReceivedTimestamp);
874         dest.writeInt(mRead ? 1 : 0);
875         dest.writeInt(mSeen ? 1 : 0);
876         dest.writeInt(mProtocol);
877         dest.writeInt(mStatus);
878         final String smsMessageUri = (mSmsMessageUri == null) ? null : mSmsMessageUri.toString();
879         dest.writeString(smsMessageUri);
880         dest.writeInt(mSmsPriority);
881         dest.writeLong(mSmsMessageSize);
882         dest.writeLong(mMmsExpiry);
883         dest.writeString(mMmsSubject);
884         dest.writeString(mMmsTransactionId);
885         dest.writeString(mMmsContentLocation);
886         dest.writeInt(mRawStatus);
887         dest.writeLong(mRetryStartTimestamp);
888 
889         // Write parts
890         dest.writeInt(mParts.size());
891         for (final MessagePartData messagePartData : mParts) {
892             dest.writeParcelable(messagePartData, flags);
893         }
894     }
895 
896     public static final Parcelable.Creator<MessageData> CREATOR
897             = new Parcelable.Creator<MessageData>() {
898         @Override
899         public MessageData createFromParcel(final Parcel in) {
900             return new MessageData(in);
901         }
902 
903         @Override
904         public MessageData[] newArray(final int size) {
905             return new MessageData[size];
906         }
907     };
908 
909     @Override
toString()910     public String toString() {
911         return toString(mMessageId, mParts);
912     }
913 
toString(String messageId, List<MessagePartData> parts)914     public static String toString(String messageId, List<MessagePartData> parts) {
915         StringBuilder sb = new StringBuilder();
916         if (messageId != null) {
917             sb.append(messageId);
918             sb.append(": ");
919         }
920         for (MessagePartData part : parts) {
921             sb.append(part.toString());
922             sb.append(" ");
923         }
924         return sb.toString();
925     }
926 }
927