• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentValues;
21 import android.database.Cursor;
22 import android.database.sqlite.SQLiteDoneException;
23 import android.database.sqlite.SQLiteStatement;
24 import android.net.Uri;
25 import android.os.ParcelFileDescriptor;
26 import androidx.collection.ArrayMap;
27 import androidx.collection.SimpleArrayMap;
28 import android.text.TextUtils;
29 
30 import com.android.messaging.Factory;
31 import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
32 import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns;
33 import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
34 import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
35 import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
36 import com.android.messaging.datamodel.ParticipantRefresh.ConversationParticipantsQuery;
37 import com.android.messaging.datamodel.data.ConversationListItemData;
38 import com.android.messaging.datamodel.data.MessageData;
39 import com.android.messaging.datamodel.data.MessagePartData;
40 import com.android.messaging.datamodel.data.ParticipantData;
41 import com.android.messaging.sms.MmsUtils;
42 import com.android.messaging.ui.UIIntents;
43 import com.android.messaging.util.Assert;
44 import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
45 import com.android.messaging.util.AvatarUriUtil;
46 import com.android.messaging.util.ContentType;
47 import com.android.messaging.util.LogUtil;
48 import com.android.messaging.util.OsUtil;
49 import com.android.messaging.util.PhoneUtils;
50 import com.android.messaging.util.UriUtil;
51 import com.android.messaging.widget.WidgetConversationProvider;
52 import com.google.common.annotations.VisibleForTesting;
53 
54 import java.io.IOException;
55 import java.util.ArrayList;
56 import java.util.HashSet;
57 import java.util.List;
58 import javax.annotation.Nullable;
59 
60 
61 /**
62  * This class manages updating our local database
63  */
64 public class BugleDatabaseOperations {
65 
66     private static final String TAG = LogUtil.BUGLE_DATABASE_TAG;
67 
68     // Global cache of phone numbers -> participant id mapping since this call is expensive.
69     private static final ArrayMap<String, String> sNormalizedPhoneNumberToParticipantIdCache =
70             new ArrayMap<String, String>();
71 
72     /**
73      * Convert list of recipient strings (email/phone number) into list of ConversationParticipants
74      *
75      * @param recipients The recipient list
76      * @param refSubId The subId used to normalize phone numbers in the recipients
77      */
getConversationParticipantsFromRecipients( final List<String> recipients, final int refSubId)78     static ArrayList<ParticipantData> getConversationParticipantsFromRecipients(
79             final List<String> recipients, final int refSubId) {
80         // Generate a list of partially formed participants
81         final ArrayList<ParticipantData> participants = new
82                 ArrayList<ParticipantData>();
83 
84         if (recipients != null) {
85             for (final String recipient : recipients) {
86                 participants.add(ParticipantData.getFromRawPhoneBySimLocale(recipient, refSubId));
87             }
88         }
89         return participants;
90     }
91 
92     /**
93      * Sanitize a given list of conversation participants by de-duping and stripping out self
94      * phone number in group conversation.
95      */
96     @DoesNotRunOnMainThread
sanitizeConversationParticipants(final List<ParticipantData> participants)97     public static void sanitizeConversationParticipants(final List<ParticipantData> participants) {
98         Assert.isNotMainThread();
99         if (participants.size() > 0) {
100             // First remove redundant phone numbers
101             final HashSet<String> recipients = new HashSet<String>();
102             for (int i = participants.size() - 1; i >= 0; i--) {
103                 final String recipient = participants.get(i).getNormalizedDestination();
104                 if (!recipients.contains(recipient)) {
105                     recipients.add(recipient);
106                 } else {
107                     participants.remove(i);
108                 }
109             }
110             if (participants.size() > 1) {
111                 // Remove self phone number from group conversation.
112                 final HashSet<String> selfNumbers =
113                         PhoneUtils.getDefault().getNormalizedSelfNumbers();
114                 int removed = 0;
115                 // Do this two-pass scan to avoid unnecessary memory allocation.
116                 // Prescan to count the self numbers in the list
117                 for (final ParticipantData p : participants) {
118                     if (selfNumbers.contains(p.getNormalizedDestination())) {
119                         removed++;
120                     }
121                 }
122                 // If all are self numbers, maybe that's what the user wants, just leave
123                 // the participants as is. Otherwise, do another scan to remove self numbers.
124                 if (removed < participants.size()) {
125                     for (int i = participants.size() - 1; i >= 0; i--) {
126                         final String recipient = participants.get(i).getNormalizedDestination();
127                         if (selfNumbers.contains(recipient)) {
128                             participants.remove(i);
129                         }
130                     }
131                 }
132             }
133         }
134     }
135 
136     /**
137      * Convert list of ConversationParticipants into recipient strings (email/phone number)
138      */
139     @DoesNotRunOnMainThread
getRecipientsFromConversationParticipants( final List<ParticipantData> participants)140     public static ArrayList<String> getRecipientsFromConversationParticipants(
141             final List<ParticipantData> participants) {
142         Assert.isNotMainThread();
143         // First find the thread id for this list of participants.
144         final ArrayList<String> recipients = new ArrayList<String>();
145 
146         for (final ParticipantData participant : participants) {
147             recipients.add(participant.getSendDestination());
148         }
149         return recipients;
150     }
151 
152     /**
153      * Get or create a conversation based on the message's thread id
154      *
155      * NOTE: There are phones on which you can't get the recipients from the thread id for SMS
156      * until you have a message, so use getOrCreateConversationFromRecipient instead.
157      *
158      * TODO: Should this be in MMS/SMS code?
159      *
160      * @param db the database
161      * @param threadId The message's thread
162      * @param senderBlocked Flag whether sender of message is in blocked people list
163      * @param refSubId The reference subId for canonicalize phone numbers
164      * @return conversationId
165      */
166     @DoesNotRunOnMainThread
getOrCreateConversationFromThreadId(final DatabaseWrapper db, final long threadId, final boolean senderBlocked, final int refSubId)167     public static String getOrCreateConversationFromThreadId(final DatabaseWrapper db,
168             final long threadId, final boolean senderBlocked, final int refSubId) {
169         Assert.isNotMainThread();
170         final List<String> recipients = MmsUtils.getRecipientsByThread(threadId);
171         final ArrayList<ParticipantData> participants =
172                 getConversationParticipantsFromRecipients(recipients, refSubId);
173 
174         return getOrCreateConversation(db, threadId, senderBlocked, participants, false, false,
175                 null);
176     }
177 
178     /**
179      * Get or create a conversation based on provided recipient
180      *
181      * @param db the database
182      * @param threadId The message's thread
183      * @param senderBlocked Flag whether sender of message is in blocked people list
184      * @param recipient recipient for thread
185      * @return conversationId
186      */
187     @DoesNotRunOnMainThread
getOrCreateConversationFromRecipient(final DatabaseWrapper db, final long threadId, final boolean senderBlocked, final ParticipantData recipient)188     public static String getOrCreateConversationFromRecipient(final DatabaseWrapper db,
189             final long threadId, final boolean senderBlocked, final ParticipantData recipient) {
190         Assert.isNotMainThread();
191         final ArrayList<ParticipantData> recipients = new ArrayList<>(1);
192         recipients.add(recipient);
193         return getOrCreateConversation(db, threadId, senderBlocked, recipients, false, false, null);
194     }
195 
196     /**
197      * Get or create a conversation based on provided participants
198      *
199      * @param db the database
200      * @param threadId The message's thread
201      * @param archived Flag whether the conversation should be created archived
202      * @param participants list of conversation participants
203      * @param noNotification If notification should be disabled
204      * @param noVibrate If vibrate on notification should be disabled
205      * @param soundUri If there is custom sound URI
206      * @return a conversation id
207      */
208     @DoesNotRunOnMainThread
getOrCreateConversation(final DatabaseWrapper db, final long threadId, final boolean archived, final ArrayList<ParticipantData> participants, boolean noNotification, boolean noVibrate, String soundUri)209     public static String getOrCreateConversation(final DatabaseWrapper db, final long threadId,
210             final boolean archived, final ArrayList<ParticipantData> participants,
211             boolean noNotification, boolean noVibrate, String soundUri) {
212         Assert.isNotMainThread();
213 
214         // Check to see if this conversation is already in out local db cache
215         String conversationId = BugleDatabaseOperations.getExistingConversation(db, threadId,
216                 false);
217 
218         if (conversationId == null) {
219             final String conversationName = ConversationListItemData.generateConversationName(
220                     participants);
221 
222             // Create the conversation with the default self participant which always maps to
223             // the system default subscription.
224             final ParticipantData self = ParticipantData.getSelfParticipant(
225                     ParticipantData.DEFAULT_SELF_SUB_ID);
226 
227             db.beginTransaction();
228             try {
229                 // Look up the "self" participantId (creating if necessary)
230                 final String selfId =
231                         BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
232                 // Create a new conversation
233                 conversationId = BugleDatabaseOperations.createConversationInTransaction(
234                         db, threadId, conversationName, selfId, participants, archived,
235                         noNotification, noVibrate, soundUri);
236                 db.setTransactionSuccessful();
237             } finally {
238                 db.endTransaction();
239             }
240         }
241 
242         return conversationId;
243     }
244 
245     /**
246      * Get a conversation from the local DB based on the message's thread id.
247      *
248      * @param dbWrapper     The database
249      * @param threadId      The message's thread in the SMS database
250      * @param senderBlocked Flag whether sender of message is in blocked people list
251      * @return The existing conversation id or null
252      */
253     @VisibleForTesting
254     @DoesNotRunOnMainThread
getExistingConversation(final DatabaseWrapper dbWrapper, final long threadId, final boolean senderBlocked)255     public static String getExistingConversation(final DatabaseWrapper dbWrapper,
256             final long threadId, final boolean senderBlocked) {
257         Assert.isNotMainThread();
258         String conversationId = null;
259 
260         Cursor cursor = null;
261         try {
262             // Look for an existing conversation in the db with this thread id
263             cursor = dbWrapper.rawQuery("SELECT " + ConversationColumns._ID
264                             + " FROM " + DatabaseHelper.CONVERSATIONS_TABLE
265                             + " WHERE " + ConversationColumns.SMS_THREAD_ID + "=" + threadId,
266                     null);
267 
268             if (cursor.moveToFirst()) {
269                 Assert.isTrue(cursor.getCount() == 1);
270                 conversationId = cursor.getString(0);
271             }
272         } finally {
273             if (cursor != null) {
274                 cursor.close();
275             }
276         }
277 
278         return conversationId;
279     }
280 
281     /**
282      * Get the thread id for an existing conversation from the local DB.
283      *
284      * @param dbWrapper The database
285      * @param conversationId The conversation to look up thread for
286      * @return The thread id. Returns -1 if the conversation was not found or if it was found
287      * but the thread column was NULL.
288      */
289     @DoesNotRunOnMainThread
getThreadId(final DatabaseWrapper dbWrapper, final String conversationId)290     public static long getThreadId(final DatabaseWrapper dbWrapper, final String conversationId) {
291         Assert.isNotMainThread();
292         long threadId = -1;
293 
294         Cursor cursor = null;
295         try {
296             cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
297                     new String[] { ConversationColumns.SMS_THREAD_ID },
298                     ConversationColumns._ID + " =?",
299                     new String[] { conversationId },
300                     null, null, null);
301 
302             if (cursor.moveToFirst()) {
303                 Assert.isTrue(cursor.getCount() == 1);
304                 if (!cursor.isNull(0)) {
305                     threadId = cursor.getLong(0);
306                 }
307             }
308         } finally {
309             if (cursor != null) {
310                 cursor.close();
311             }
312         }
313 
314         return threadId;
315     }
316 
317     @DoesNotRunOnMainThread
isBlockedDestination(final DatabaseWrapper db, final String destination)318     public static boolean isBlockedDestination(final DatabaseWrapper db, final String destination) {
319         Assert.isNotMainThread();
320         return isBlockedParticipant(db, destination, ParticipantColumns.NORMALIZED_DESTINATION);
321     }
322 
isBlockedParticipant(final DatabaseWrapper db, final String participantId)323     static boolean isBlockedParticipant(final DatabaseWrapper db, final String participantId) {
324         return isBlockedParticipant(db, participantId, ParticipantColumns._ID);
325     }
326 
isBlockedParticipant(final DatabaseWrapper db, final String value, final String column)327     static boolean isBlockedParticipant(final DatabaseWrapper db, final String value,
328             final String column) {
329         Cursor cursor = null;
330         try {
331             cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE,
332                     new String[] { ParticipantColumns.BLOCKED },
333                     column + "=? AND " + ParticipantColumns.SUB_ID + "=?",
334                     new String[] { value,
335                     Integer.toString(ParticipantData.OTHER_THAN_SELF_SUB_ID) },
336                     null, null, null);
337 
338             Assert.inRange(cursor.getCount(), 0, 1);
339             if (cursor.moveToFirst()) {
340                 return cursor.getInt(0) == 1;
341             }
342         } finally {
343             if (cursor != null) {
344                 cursor.close();
345             }
346         }
347         return false;  // if there's no row, it's not blocked :-)
348     }
349 
350     /**
351      * Create a conversation in the local DB based on the message's thread id.
352      *
353      * It's up to the caller to make sure that this is all inside a transaction.  It will return
354      * null if it's not in the local DB.
355      *
356      * @param dbWrapper     The database
357      * @param threadId      The message's thread
358      * @param selfId        The selfId to make default for this conversation
359      * @param archived      Flag whether the conversation should be created archived
360      * @param noNotification If notification should be disabled
361      * @param noVibrate     If vibrate on notification should be disabled
362      * @param soundUri      The customized sound
363      * @return The existing conversation id or new conversation id
364      */
createConversationInTransaction(final DatabaseWrapper dbWrapper, final long threadId, final String conversationName, final String selfId, final List<ParticipantData> participants, final boolean archived, boolean noNotification, boolean noVibrate, String soundUri)365     static String createConversationInTransaction(final DatabaseWrapper dbWrapper,
366             final long threadId, final String conversationName, final String selfId,
367             final List<ParticipantData> participants, final boolean archived,
368             boolean noNotification, boolean noVibrate, String soundUri) {
369         // We want conversation and participant creation to be atomic
370         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
371         boolean hasEmailAddress = false;
372         for (final ParticipantData participant : participants) {
373             Assert.isTrue(!participant.isSelf());
374             if (participant.isEmail()) {
375                 hasEmailAddress = true;
376             }
377         }
378 
379         // TODO : Conversations state - normal vs. archived
380 
381         // Insert a new local conversation for this thread id
382         final ContentValues values = new ContentValues();
383         values.put(ConversationColumns.SMS_THREAD_ID, threadId);
384         // Start with conversation hidden - sending a message or saving a draft will change that
385         values.put(ConversationColumns.SORT_TIMESTAMP, 0L);
386         values.put(ConversationColumns.CURRENT_SELF_ID, selfId);
387         values.put(ConversationColumns.PARTICIPANT_COUNT, participants.size());
388         values.put(ConversationColumns.INCLUDE_EMAIL_ADDRESS, (hasEmailAddress ? 1 : 0));
389         if (archived) {
390             values.put(ConversationColumns.ARCHIVE_STATUS, 1);
391         }
392         if (noNotification) {
393             values.put(ConversationColumns.NOTIFICATION_ENABLED, 0);
394         }
395         if (noVibrate) {
396             values.put(ConversationColumns.NOTIFICATION_VIBRATION, 0);
397         }
398         if (!TextUtils.isEmpty(soundUri)) {
399             values.put(ConversationColumns.NOTIFICATION_SOUND_URI, soundUri);
400         }
401 
402         fillParticipantData(values, participants);
403 
404         final long conversationRowId = dbWrapper.insert(DatabaseHelper.CONVERSATIONS_TABLE, null,
405                 values);
406 
407         Assert.isTrue(conversationRowId != -1);
408         if (conversationRowId == -1) {
409             LogUtil.e(TAG, "BugleDatabaseOperations : failed to insert conversation into table");
410             return null;
411         }
412 
413         final String conversationId = Long.toString(conversationRowId);
414 
415         // Make sure that participants are added for this conversation
416         for (final ParticipantData participant : participants) {
417             // TODO: Use blocking information
418             addParticipantToConversation(dbWrapper, participant, conversationId);
419         }
420 
421         // Now fully resolved participants available can update conversation name / avatar.
422         // b/16437575: We cannot use the participants directly, but instead have to call
423         // getParticipantsForConversation() to retrieve the actual participants. This is needed
424         // because the call to addParticipantToConversation() won't fill up the ParticipantData
425         // if the participant already exists in the participant table. For example, say you have
426         // an existing conversation with John. Now if you create a new group conversation with
427         // Jeff & John with only their phone numbers, then when we try to add John's number to the
428         // group conversation, we see that he's already in the participant table, therefore we
429         // short-circuit any steps to actually fill out the ParticipantData for John other than
430         // just returning his participant id. Eventually, the ParticipantData we have is still the
431         // raw data with just the phone number. getParticipantsForConversation(), on the other
432         // hand, will fill out all the info for each participant from the participants table.
433         updateConversationNameAndAvatarInTransaction(dbWrapper, conversationId,
434                 getParticipantsForConversation(dbWrapper, conversationId));
435 
436         return conversationId;
437     }
438 
fillParticipantData(final ContentValues values, final List<ParticipantData> participants)439     private static void fillParticipantData(final ContentValues values,
440             final List<ParticipantData> participants) {
441         if (participants != null && !participants.isEmpty()) {
442             final Uri avatarUri = AvatarUriUtil.createAvatarUri(participants);
443             values.put(ConversationColumns.ICON, avatarUri.toString());
444 
445             long contactId;
446             String lookupKey;
447             String destination;
448             if (participants.size() == 1) {
449                 final ParticipantData firstParticipant = participants.get(0);
450                 contactId = firstParticipant.getContactId();
451                 lookupKey = firstParticipant.getLookupKey();
452                 destination = firstParticipant.getNormalizedDestination();
453             } else {
454                 contactId = 0;
455                 lookupKey = null;
456                 destination = null;
457             }
458 
459             values.put(ConversationColumns.PARTICIPANT_CONTACT_ID, contactId);
460             values.put(ConversationColumns.PARTICIPANT_LOOKUP_KEY, lookupKey);
461             values.put(ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION, destination);
462         }
463     }
464 
465     /**
466      * Delete conversation and associated messages/parts
467      */
468     @DoesNotRunOnMainThread
deleteConversation(final DatabaseWrapper dbWrapper, final String conversationId, final long cutoffTimestamp)469     public static boolean deleteConversation(final DatabaseWrapper dbWrapper,
470             final String conversationId, final long cutoffTimestamp) {
471         Assert.isNotMainThread();
472         dbWrapper.beginTransaction();
473         boolean conversationDeleted = false;
474         boolean conversationMessagesDeleted = false;
475         try {
476             // Delete existing messages
477             if (cutoffTimestamp == Long.MAX_VALUE) {
478                 // Delete parts and messages
479                 dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
480                         MessageColumns.CONVERSATION_ID + "=?", new String[] { conversationId });
481                 conversationMessagesDeleted = true;
482             } else {
483                 // Delete all messages prior to the cutoff
484                 dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
485                         MessageColumns.CONVERSATION_ID + "=? AND "
486                                 + MessageColumns.RECEIVED_TIMESTAMP + "<=?",
487                                 new String[] { conversationId, Long.toString(cutoffTimestamp) });
488 
489                 // Delete any draft message. The delete above may not always include the draft,
490                 // because under certain scenarios (e.g. sending messages in progress), the draft
491                 // timestamp can be larger than the cutoff time, which is generally the conversation
492                 // sort timestamp. Because of how the sms/mms provider works on some newer
493                 // devices, it's important that we never delete all the messages in a conversation
494                 // without also deleting the conversation itself (see b/20262204 for details).
495                 dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
496                         MessageColumns.STATUS + "=? AND " + MessageColumns.CONVERSATION_ID + "=?",
497                         new String[] {
498                             Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_DRAFT),
499                             conversationId
500                         });
501 
502                 // Check to see if there are any messages left in the conversation
503                 final long count = dbWrapper.queryNumEntries(DatabaseHelper.MESSAGES_TABLE,
504                         MessageColumns.CONVERSATION_ID + "=?", new String[] { conversationId });
505                 conversationMessagesDeleted = (count == 0);
506 
507                 // Log detail information if there are still messages left in the conversation
508                 if (!conversationMessagesDeleted) {
509                     final long maxTimestamp =
510                             getConversationMaxTimestamp(dbWrapper, conversationId);
511                     LogUtil.w(TAG, "BugleDatabaseOperations:"
512                             + " cannot delete all messages in a conversation"
513                             + ", after deletion: count=" + count
514                             + ", max timestamp=" + maxTimestamp
515                             + ", cutoff timestamp=" + cutoffTimestamp);
516                 }
517             }
518 
519             if (conversationMessagesDeleted) {
520                 // Delete conversation row
521                 final int count = dbWrapper.delete(DatabaseHelper.CONVERSATIONS_TABLE,
522                         ConversationColumns._ID + "=?", new String[] { conversationId });
523                 conversationDeleted = (count > 0);
524             }
525             dbWrapper.setTransactionSuccessful();
526         } finally {
527             dbWrapper.endTransaction();
528         }
529         return conversationDeleted;
530     }
531 
532     private static final String MAX_RECEIVED_TIMESTAMP =
533             "MAX(" + MessageColumns.RECEIVED_TIMESTAMP + ")";
534     /**
535      * Get the max received timestamp of a conversation's messages
536      */
getConversationMaxTimestamp(final DatabaseWrapper dbWrapper, final String conversationId)537     private static long getConversationMaxTimestamp(final DatabaseWrapper dbWrapper,
538             final String conversationId) {
539         final Cursor cursor = dbWrapper.query(
540                 DatabaseHelper.MESSAGES_TABLE,
541                 new String[]{ MAX_RECEIVED_TIMESTAMP },
542                 MessageColumns.CONVERSATION_ID + "=?",
543                 new String[]{ conversationId },
544                 null, null, null);
545         if (cursor != null) {
546             try {
547                 if (cursor.moveToFirst()) {
548                     return cursor.getLong(0);
549                 }
550             } finally {
551                 cursor.close();
552             }
553         }
554         return 0;
555     }
556 
557     @DoesNotRunOnMainThread
updateConversationMetadataInTransaction(final DatabaseWrapper dbWrapper, final String conversationId, final String messageId, final long latestTimestamp, final boolean keepArchived, final String smsServiceCenter, final boolean shouldAutoSwitchSelfId)558     public static void updateConversationMetadataInTransaction(final DatabaseWrapper dbWrapper,
559             final String conversationId, final String messageId, final long latestTimestamp,
560             final boolean keepArchived, final String smsServiceCenter,
561             final boolean shouldAutoSwitchSelfId) {
562         Assert.isNotMainThread();
563         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
564 
565         final ContentValues values = new ContentValues();
566         values.put(ConversationColumns.LATEST_MESSAGE_ID, messageId);
567         values.put(ConversationColumns.SORT_TIMESTAMP, latestTimestamp);
568         if (!TextUtils.isEmpty(smsServiceCenter)) {
569             values.put(ConversationColumns.SMS_SERVICE_CENTER, smsServiceCenter);
570         }
571 
572         // When the conversation gets updated with new messages, unarchive the conversation unless
573         // the sender is blocked, or we have been told to keep it archived.
574         if (!keepArchived) {
575             values.put(ConversationColumns.ARCHIVE_STATUS, 0);
576         }
577 
578         final MessageData message = readMessage(dbWrapper, messageId);
579         addSnippetTextAndPreviewToContentValues(message, false /* showDraft */, values);
580 
581         if (shouldAutoSwitchSelfId) {
582             addSelfIdAutoSwitchInfoToContentValues(dbWrapper, message, conversationId, values);
583         }
584 
585         // Conversation always exists as this method is called from ActionService only after
586         // reading and if necessary creating the conversation.
587         updateConversationRow(dbWrapper, conversationId, values);
588 
589         if (shouldAutoSwitchSelfId && OsUtil.isAtLeastL_MR1()) {
590             // Normally, the draft message compose UI trusts its UI state for providing up-to-date
591             // conversation self id. Therefore, notify UI through local broadcast receiver about
592             // this external change so the change can be properly reflected.
593             UIIntents.get().broadcastConversationSelfIdChange(dbWrapper.getContext(),
594                     conversationId, getConversationSelfId(dbWrapper, conversationId));
595         }
596     }
597 
598     @DoesNotRunOnMainThread
updateConversationMetadataInTransaction(final DatabaseWrapper db, final String conversationId, final String messageId, final long latestTimestamp, final boolean keepArchived, final boolean shouldAutoSwitchSelfId)599     public static void updateConversationMetadataInTransaction(final DatabaseWrapper db,
600             final String conversationId, final String messageId, final long latestTimestamp,
601             final boolean keepArchived, final boolean shouldAutoSwitchSelfId) {
602         Assert.isNotMainThread();
603         updateConversationMetadataInTransaction(
604                 db, conversationId, messageId, latestTimestamp, keepArchived, null,
605                 shouldAutoSwitchSelfId);
606     }
607 
608     @DoesNotRunOnMainThread
updateConversationArchiveStatusInTransaction(final DatabaseWrapper dbWrapper, final String conversationId, final boolean isArchived)609     public static void updateConversationArchiveStatusInTransaction(final DatabaseWrapper dbWrapper,
610             final String conversationId, final boolean isArchived) {
611         Assert.isNotMainThread();
612         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
613         final ContentValues values = new ContentValues();
614         values.put(ConversationColumns.ARCHIVE_STATUS, isArchived ? 1 : 0);
615         updateConversationRowIfExists(dbWrapper, conversationId, values);
616     }
617 
addSnippetTextAndPreviewToContentValues(final MessageData message, final boolean showDraft, final ContentValues values)618     static void addSnippetTextAndPreviewToContentValues(final MessageData message,
619             final boolean showDraft, final ContentValues values) {
620         values.put(ConversationColumns.SHOW_DRAFT, showDraft ? 1 : 0);
621         values.put(ConversationColumns.SNIPPET_TEXT, message.getMessageText());
622         values.put(ConversationColumns.SUBJECT_TEXT, message.getMmsSubject());
623 
624         String type = null;
625         String uriString = null;
626         for (final MessagePartData part : message.getParts()) {
627             if (part.isAttachment() &&
628                     ContentType.isConversationListPreviewableType(part.getContentType())) {
629                 uriString = part.getContentUri().toString();
630                 type = part.getContentType();
631                 break;
632             }
633         }
634         values.put(ConversationColumns.PREVIEW_CONTENT_TYPE, type);
635         values.put(ConversationColumns.PREVIEW_URI, uriString);
636     }
637 
638     /**
639      * Adds self-id auto switch info for a conversation if the last message has a different
640      * subscription than the conversation's.
641      * @return true if self id will need to be changed, false otherwise.
642      */
addSelfIdAutoSwitchInfoToContentValues(final DatabaseWrapper dbWrapper, final MessageData message, final String conversationId, final ContentValues values)643     static boolean addSelfIdAutoSwitchInfoToContentValues(final DatabaseWrapper dbWrapper,
644             final MessageData message, final String conversationId, final ContentValues values) {
645         // Only auto switch conversation self for incoming messages.
646         if (!OsUtil.isAtLeastL_MR1() || !message.getIsIncoming()) {
647             return false;
648         }
649 
650         final String conversationSelfId = getConversationSelfId(dbWrapper, conversationId);
651         final String messageSelfId = message.getSelfId();
652 
653         if (conversationSelfId == null || messageSelfId == null) {
654             return false;
655         }
656 
657         // Get the sub IDs in effect for both the message and the conversation and compare them:
658         // 1. If message is unbound (using default sub id), then the message was sent with
659         //    pre-MSIM support. Don't auto-switch because we don't know the subscription for the
660         //    message.
661         // 2. If message is bound,
662         //    i. If conversation is unbound, use the system default sub id as its effective sub.
663         //    ii. If conversation is bound, use its subscription directly.
664         //    Compare the message sub id with the conversation's effective sub id. If they are
665         //    different, auto-switch the conversation to the message's sub.
666         final ParticipantData conversationSelf = getExistingParticipant(dbWrapper,
667                 conversationSelfId);
668         final ParticipantData messageSelf = getExistingParticipant(dbWrapper, messageSelfId);
669         if (!messageSelf.isActiveSubscription()) {
670             // Don't switch if the message subscription is no longer active.
671             return false;
672         }
673         final int messageSubId = messageSelf.getSubId();
674         if (messageSubId == ParticipantData.DEFAULT_SELF_SUB_ID) {
675             return false;
676         }
677 
678         final int conversationEffectiveSubId =
679                 PhoneUtils.getDefault().getEffectiveSubId(conversationSelf.getSubId());
680 
681         if (conversationEffectiveSubId != messageSubId) {
682             return addConversationSelfIdToContentValues(dbWrapper, messageSelf.getId(), values);
683         }
684         return false;
685     }
686 
687     /**
688      * Adds conversation self id updates to ContentValues given. This performs check on the selfId
689      * to ensure it's valid and active.
690      * @return true if self id will need to be changed, false otherwise.
691      */
addConversationSelfIdToContentValues(final DatabaseWrapper dbWrapper, final String selfId, final ContentValues values)692     static boolean addConversationSelfIdToContentValues(final DatabaseWrapper dbWrapper,
693             final String selfId, final ContentValues values) {
694         // Make sure the selfId passed in is valid and active.
695         final String selection = ParticipantColumns._ID + "=? AND " +
696                 ParticipantColumns.SIM_SLOT_ID + "<>?";
697         Cursor cursor = null;
698         try {
699             cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
700                     new String[] { ParticipantColumns._ID }, selection,
701                     new String[] { selfId, String.valueOf(ParticipantData.INVALID_SLOT_ID) },
702                     null, null, null);
703 
704             if (cursor != null && cursor.getCount() > 0) {
705                 values.put(ConversationColumns.CURRENT_SELF_ID, selfId);
706                 return true;
707             }
708         } finally {
709             if (cursor != null) {
710                 cursor.close();
711             }
712         }
713         return false;
714     }
715 
updateConversationDraftSnippetAndPreviewInTransaction( final DatabaseWrapper dbWrapper, final String conversationId, final MessageData draftMessage)716     private static void updateConversationDraftSnippetAndPreviewInTransaction(
717             final DatabaseWrapper dbWrapper, final String conversationId,
718             final MessageData draftMessage) {
719         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
720 
721         long sortTimestamp = 0L;
722         Cursor cursor = null;
723         try {
724             // Check to find the latest message in the conversation
725             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
726                     REFRESH_CONVERSATION_MESSAGE_PROJECTION,
727                     MessageColumns.CONVERSATION_ID + "=?",
728                     new String[]{conversationId}, null, null,
729                     MessageColumns.RECEIVED_TIMESTAMP + " DESC", "1" /* limit */);
730 
731             if (cursor.moveToFirst()) {
732                 sortTimestamp = cursor.getLong(1);
733             }
734         } finally {
735             if (cursor != null) {
736                 cursor.close();
737             }
738         }
739 
740 
741         final ContentValues values = new ContentValues();
742         if (draftMessage == null || !draftMessage.hasContent()) {
743             values.put(ConversationColumns.SHOW_DRAFT, 0);
744             values.put(ConversationColumns.DRAFT_SNIPPET_TEXT, "");
745             values.put(ConversationColumns.DRAFT_SUBJECT_TEXT, "");
746             values.put(ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE, "");
747             values.put(ConversationColumns.DRAFT_PREVIEW_URI, "");
748         } else {
749             sortTimestamp = Math.max(sortTimestamp, draftMessage.getReceivedTimeStamp());
750             values.put(ConversationColumns.SHOW_DRAFT, 1);
751             values.put(ConversationColumns.DRAFT_SNIPPET_TEXT, draftMessage.getMessageText());
752             values.put(ConversationColumns.DRAFT_SUBJECT_TEXT, draftMessage.getMmsSubject());
753             String type = null;
754             String uriString = null;
755             for (final MessagePartData part : draftMessage.getParts()) {
756                 if (part.isAttachment() &&
757                         ContentType.isConversationListPreviewableType(part.getContentType())) {
758                     uriString = part.getContentUri().toString();
759                     type = part.getContentType();
760                     break;
761                 }
762             }
763             values.put(ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE, type);
764             values.put(ConversationColumns.DRAFT_PREVIEW_URI, uriString);
765         }
766         values.put(ConversationColumns.SORT_TIMESTAMP, sortTimestamp);
767         // Called in transaction after reading conversation row
768         updateConversationRow(dbWrapper, conversationId, values);
769     }
770 
771     @DoesNotRunOnMainThread
updateConversationRowIfExists(final DatabaseWrapper dbWrapper, final String conversationId, final ContentValues values)772     public static boolean updateConversationRowIfExists(final DatabaseWrapper dbWrapper,
773             final String conversationId, final ContentValues values) {
774         Assert.isNotMainThread();
775         return updateRowIfExists(dbWrapper, DatabaseHelper.CONVERSATIONS_TABLE,
776                 ConversationColumns._ID, conversationId, values);
777     }
778 
779     @DoesNotRunOnMainThread
updateConversationRow(final DatabaseWrapper dbWrapper, final String conversationId, final ContentValues values)780     public static void updateConversationRow(final DatabaseWrapper dbWrapper,
781             final String conversationId, final ContentValues values) {
782         Assert.isNotMainThread();
783         final boolean exists = updateConversationRowIfExists(dbWrapper, conversationId, values);
784         Assert.isTrue(exists);
785     }
786 
787     @DoesNotRunOnMainThread
updateMessageRowIfExists(final DatabaseWrapper dbWrapper, final String messageId, final ContentValues values)788     public static boolean updateMessageRowIfExists(final DatabaseWrapper dbWrapper,
789             final String messageId, final ContentValues values) {
790         Assert.isNotMainThread();
791         return updateRowIfExists(dbWrapper, DatabaseHelper.MESSAGES_TABLE, MessageColumns._ID,
792                 messageId, values);
793     }
794 
795     @DoesNotRunOnMainThread
updateMessageRow(final DatabaseWrapper dbWrapper, final String messageId, final ContentValues values)796     public static void updateMessageRow(final DatabaseWrapper dbWrapper,
797             final String messageId, final ContentValues values) {
798         Assert.isNotMainThread();
799         final boolean exists = updateMessageRowIfExists(dbWrapper, messageId, values);
800         Assert.isTrue(exists);
801     }
802 
803     @DoesNotRunOnMainThread
updatePartRowIfExists(final DatabaseWrapper dbWrapper, final String partId, final ContentValues values)804     public static boolean updatePartRowIfExists(final DatabaseWrapper dbWrapper,
805             final String partId, final ContentValues values) {
806         Assert.isNotMainThread();
807         return updateRowIfExists(dbWrapper, DatabaseHelper.PARTS_TABLE, PartColumns._ID,
808                 partId, values);
809     }
810 
811     /**
812      * Returns the default conversation name based on its participants.
813      */
getDefaultConversationName(final List<ParticipantData> participants)814     private static String getDefaultConversationName(final List<ParticipantData> participants) {
815         return ConversationListItemData.generateConversationName(participants);
816     }
817 
818     /**
819      * Updates a given conversation's name based on its participants.
820      */
821     @DoesNotRunOnMainThread
updateConversationNameAndAvatarInTransaction( final DatabaseWrapper dbWrapper, final String conversationId)822     public static void updateConversationNameAndAvatarInTransaction(
823             final DatabaseWrapper dbWrapper, final String conversationId) {
824         Assert.isNotMainThread();
825         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
826 
827         final ArrayList<ParticipantData> participants =
828                 getParticipantsForConversation(dbWrapper, conversationId);
829         updateConversationNameAndAvatarInTransaction(dbWrapper, conversationId, participants);
830     }
831 
832     /**
833      * Updates a given conversation's name based on its participants.
834      */
updateConversationNameAndAvatarInTransaction( final DatabaseWrapper dbWrapper, final String conversationId, final List<ParticipantData> participants)835     private static void updateConversationNameAndAvatarInTransaction(
836             final DatabaseWrapper dbWrapper, final String conversationId,
837             final List<ParticipantData> participants) {
838         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
839 
840         final ContentValues values = new ContentValues();
841         values.put(ConversationColumns.NAME,
842                 getDefaultConversationName(participants));
843 
844         // Fill in IS_ENTERPRISE.
845         final boolean hasAnyEnterpriseContact =
846                 ConversationListItemData.hasAnyEnterpriseContact(participants);
847         values.put(ConversationColumns.IS_ENTERPRISE, hasAnyEnterpriseContact);
848 
849         fillParticipantData(values, participants);
850 
851         // Used by background thread when refreshing conversation so conversation could be deleted.
852         updateConversationRowIfExists(dbWrapper, conversationId, values);
853 
854         WidgetConversationProvider.notifyConversationRenamed(Factory.get().getApplicationContext(),
855                 conversationId);
856     }
857 
858     /**
859      * Updates a given conversation's self id.
860      */
861     @DoesNotRunOnMainThread
updateConversationSelfIdInTransaction( final DatabaseWrapper dbWrapper, final String conversationId, final String selfId)862     public static void updateConversationSelfIdInTransaction(
863             final DatabaseWrapper dbWrapper, final String conversationId, final String selfId) {
864         Assert.isNotMainThread();
865         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
866         final ContentValues values = new ContentValues();
867         if (addConversationSelfIdToContentValues(dbWrapper, selfId, values)) {
868             updateConversationRowIfExists(dbWrapper, conversationId, values);
869         }
870     }
871 
872     @DoesNotRunOnMainThread
getConversationSelfId(final DatabaseWrapper dbWrapper, final String conversationId)873     public static String getConversationSelfId(final DatabaseWrapper dbWrapper,
874             final String conversationId) {
875         Assert.isNotMainThread();
876         Cursor cursor = null;
877         try {
878             cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
879                     new String[] { ConversationColumns.CURRENT_SELF_ID },
880                     ConversationColumns._ID + "=?",
881                     new String[] { conversationId },
882                     null, null, null);
883             Assert.inRange(cursor.getCount(), 0, 1);
884             if (cursor.moveToFirst()) {
885                 return cursor.getString(0);
886             }
887         } finally {
888             if (cursor != null) {
889                 cursor.close();
890             }
891         }
892         return null;
893     }
894 
895     /**
896      * Frees up memory associated with phone number to participant id matching.
897      */
898     @DoesNotRunOnMainThread
clearParticipantIdCache()899     public static void clearParticipantIdCache() {
900         Assert.isNotMainThread();
901         synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
902             sNormalizedPhoneNumberToParticipantIdCache.clear();
903         }
904     }
905 
906     @DoesNotRunOnMainThread
getRecipientsForConversation(final DatabaseWrapper dbWrapper, final String conversationId)907     public static ArrayList<String> getRecipientsForConversation(final DatabaseWrapper dbWrapper,
908             final String conversationId) {
909         Assert.isNotMainThread();
910         final ArrayList<ParticipantData> participants =
911                 getParticipantsForConversation(dbWrapper, conversationId);
912 
913         final ArrayList<String> recipients = new ArrayList<String>();
914         for (final ParticipantData participant : participants) {
915             recipients.add(participant.getSendDestination());
916         }
917 
918         return recipients;
919     }
920 
921     @DoesNotRunOnMainThread
getSmsServiceCenterForConversation(final DatabaseWrapper dbWrapper, final String conversationId)922     public static String getSmsServiceCenterForConversation(final DatabaseWrapper dbWrapper,
923             final String conversationId) {
924         Assert.isNotMainThread();
925         Cursor cursor = null;
926         try {
927             cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
928                     new String[] { ConversationColumns.SMS_SERVICE_CENTER },
929                     ConversationColumns._ID + "=?",
930                     new String[] { conversationId },
931                     null, null, null);
932             Assert.inRange(cursor.getCount(), 0, 1);
933             if (cursor.moveToFirst()) {
934                 return cursor.getString(0);
935             }
936         } finally {
937             if (cursor != null) {
938                 cursor.close();
939             }
940         }
941         return null;
942     }
943 
944     @DoesNotRunOnMainThread
getExistingParticipant(final DatabaseWrapper dbWrapper, final String participantId)945     public static ParticipantData getExistingParticipant(final DatabaseWrapper dbWrapper,
946             final String participantId) {
947         Assert.isNotMainThread();
948         ParticipantData participant = null;
949         Cursor cursor = null;
950         try {
951             cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
952                     ParticipantData.ParticipantsQuery.PROJECTION,
953                     ParticipantColumns._ID + " =?",
954                     new String[] { participantId }, null, null, null);
955             Assert.inRange(cursor.getCount(), 0, 1);
956             if (cursor.moveToFirst()) {
957                 participant = ParticipantData.getFromCursor(cursor);
958             }
959         } finally {
960             if (cursor != null) {
961                 cursor.close();
962             }
963         }
964 
965         return participant;
966     }
967 
getSelfSubscriptionId(final DatabaseWrapper dbWrapper, final String selfParticipantId)968     public static int getSelfSubscriptionId(final DatabaseWrapper dbWrapper,
969             final String selfParticipantId) {
970         final ParticipantData selfParticipant = BugleDatabaseOperations.getExistingParticipant(
971                 dbWrapper, selfParticipantId);
972         if (selfParticipant != null) {
973             Assert.isTrue(selfParticipant.isSelf());
974             return selfParticipant.getSubId();
975         }
976         return ParticipantData.DEFAULT_SELF_SUB_ID;
977     }
978 
979     @VisibleForTesting
980     @DoesNotRunOnMainThread
getParticipantsForConversation( final DatabaseWrapper dbWrapper, final String conversationId)981     public static ArrayList<ParticipantData> getParticipantsForConversation(
982             final DatabaseWrapper dbWrapper, final String conversationId) {
983         Assert.isNotMainThread();
984         final ArrayList<ParticipantData> participants =
985                 new ArrayList<ParticipantData>();
986         Cursor cursor = null;
987         try {
988             cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
989                     ParticipantData.ParticipantsQuery.PROJECTION,
990                     ParticipantColumns._ID + " IN ( " + "SELECT "
991                             + ConversationParticipantsColumns.PARTICIPANT_ID + " AS "
992                             + ParticipantColumns._ID
993                             + " FROM " + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE
994                             + " WHERE " + ConversationParticipantsColumns.CONVERSATION_ID + " =? )",
995                             new String[] { conversationId }, null, null, null);
996 
997             while (cursor.moveToNext()) {
998                 participants.add(ParticipantData.getFromCursor(cursor));
999             }
1000         } finally {
1001             if (cursor != null) {
1002                 cursor.close();
1003             }
1004         }
1005 
1006         return participants;
1007     }
1008 
1009     @DoesNotRunOnMainThread
readMessage(final DatabaseWrapper dbWrapper, final String messageId)1010     public static MessageData readMessage(final DatabaseWrapper dbWrapper, final String messageId) {
1011         Assert.isNotMainThread();
1012         final MessageData message = readMessageData(dbWrapper, messageId);
1013         if (message != null) {
1014             readMessagePartsData(dbWrapper, message, false);
1015         }
1016         return message;
1017     }
1018 
1019     @VisibleForTesting
readMessagePartData(final DatabaseWrapper dbWrapper, final String partId)1020     static MessagePartData readMessagePartData(final DatabaseWrapper dbWrapper,
1021             final String partId) {
1022         MessagePartData messagePartData = null;
1023         Cursor cursor = null;
1024         try {
1025             cursor = dbWrapper.query(DatabaseHelper.PARTS_TABLE,
1026                     MessagePartData.getProjection(), PartColumns._ID + "=?",
1027                     new String[] { partId }, null, null, null);
1028             Assert.inRange(cursor.getCount(), 0, 1);
1029             if (cursor.moveToFirst()) {
1030                 messagePartData = MessagePartData.createFromCursor(cursor);
1031             }
1032         } finally {
1033             if (cursor != null) {
1034                 cursor.close();
1035             }
1036         }
1037         return messagePartData;
1038     }
1039 
1040     @DoesNotRunOnMainThread
readMessageData(final DatabaseWrapper dbWrapper, final Uri smsMessageUri)1041     public static MessageData readMessageData(final DatabaseWrapper dbWrapper,
1042             final Uri smsMessageUri) {
1043         Assert.isNotMainThread();
1044         MessageData message = null;
1045         Cursor cursor = null;
1046         try {
1047             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
1048                     MessageData.getProjection(), MessageColumns.SMS_MESSAGE_URI + "=?",
1049                     new String[] { smsMessageUri.toString() }, null, null, null);
1050             Assert.inRange(cursor.getCount(), 0, 1);
1051             if (cursor.moveToFirst()) {
1052                 message = new MessageData();
1053                 message.bind(cursor);
1054             }
1055         } finally {
1056             if (cursor != null) {
1057                 cursor.close();
1058             }
1059         }
1060         return message;
1061     }
1062 
1063     @DoesNotRunOnMainThread
readMessageData(final DatabaseWrapper dbWrapper, final String messageId)1064     public static MessageData readMessageData(final DatabaseWrapper dbWrapper,
1065             final String messageId) {
1066         Assert.isNotMainThread();
1067         MessageData message = null;
1068         Cursor cursor = null;
1069         try {
1070             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
1071                     MessageData.getProjection(), MessageColumns._ID + "=?",
1072                     new String[] { messageId }, null, null, null);
1073             Assert.inRange(cursor.getCount(), 0, 1);
1074             if (cursor.moveToFirst()) {
1075                 message = new MessageData();
1076                 message.bind(cursor);
1077             }
1078         } finally {
1079             if (cursor != null) {
1080                 cursor.close();
1081             }
1082         }
1083         return message;
1084     }
1085 
1086     /**
1087      * Read all the parts for a message
1088      * @param dbWrapper database
1089      * @param message read parts for this message
1090      * @param checkAttachmentFilesExist check each attachment file and only include if file exists
1091      */
readMessagePartsData(final DatabaseWrapper dbWrapper, final MessageData message, final boolean checkAttachmentFilesExist)1092     private static void readMessagePartsData(final DatabaseWrapper dbWrapper,
1093             final MessageData message, final boolean checkAttachmentFilesExist) {
1094         final ContentResolver contentResolver =
1095                 Factory.get().getApplicationContext().getContentResolver();
1096         Cursor cursor = null;
1097         try {
1098             cursor = dbWrapper.query(DatabaseHelper.PARTS_TABLE,
1099                     MessagePartData.getProjection(), PartColumns.MESSAGE_ID + "=?",
1100                     new String[] { message.getMessageId() }, null, null, null);
1101             while (cursor.moveToNext()) {
1102                 final MessagePartData messagePartData = MessagePartData.createFromCursor(cursor);
1103                 if (checkAttachmentFilesExist && messagePartData.isAttachment() &&
1104                         !UriUtil.isBugleAppResource(messagePartData.getContentUri())) {
1105                     try {
1106                         // Test that the file exists before adding the attachment to the draft
1107                         final ParcelFileDescriptor fileDescriptor =
1108                                 contentResolver.openFileDescriptor(
1109                                         messagePartData.getContentUri(), "r");
1110                         if (fileDescriptor != null) {
1111                             fileDescriptor.close();
1112                             message.addPart(messagePartData);
1113                         }
1114                     } catch (final IOException e) {
1115                         // The attachment's temp storage no longer exists, just ignore the file
1116                     } catch (final SecurityException e) {
1117                         // Likely thrown by openFileDescriptor due to an expired access grant.
1118                         if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
1119                             LogUtil.d(LogUtil.BUGLE_TAG, "uri: " + messagePartData.getContentUri());
1120                         }
1121                     }
1122                 } else {
1123                     message.addPart(messagePartData);
1124                 }
1125             }
1126         } finally {
1127             if (cursor != null) {
1128                 cursor.close();
1129             }
1130         }
1131     }
1132 
1133     /**
1134      * Write a message part to our local database
1135      *
1136      * @param dbWrapper     The database
1137      * @param messagePart   The message part to insert
1138      * @return The row id of the newly inserted part
1139      */
insertNewMessagePartInTransaction(final DatabaseWrapper dbWrapper, final MessagePartData messagePart, final String conversationId)1140     static String insertNewMessagePartInTransaction(final DatabaseWrapper dbWrapper,
1141             final MessagePartData messagePart, final String conversationId) {
1142         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1143         Assert.isTrue(!TextUtils.isEmpty(messagePart.getMessageId()));
1144 
1145         // Insert a new part row
1146         final SQLiteStatement insert = messagePart.getInsertStatement(dbWrapper, conversationId);
1147         final long rowNumber = insert.executeInsert();
1148 
1149         Assert.inRange(rowNumber, 0, Long.MAX_VALUE);
1150         final String partId = Long.toString(rowNumber);
1151 
1152         // Update the part id
1153         messagePart.updatePartId(partId);
1154 
1155         return partId;
1156     }
1157 
1158     /**
1159      * Insert a message and its parts into the table
1160      */
1161     @DoesNotRunOnMainThread
insertNewMessageInTransaction(final DatabaseWrapper dbWrapper, final MessageData message)1162     public static void insertNewMessageInTransaction(final DatabaseWrapper dbWrapper,
1163             final MessageData message) {
1164         Assert.isNotMainThread();
1165         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1166 
1167         // Insert message row
1168         final SQLiteStatement insert = message.getInsertStatement(dbWrapper);
1169         final long rowNumber = insert.executeInsert();
1170 
1171         Assert.inRange(rowNumber, 0, Long.MAX_VALUE);
1172         final String messageId = Long.toString(rowNumber);
1173         message.updateMessageId(messageId);
1174         //  Insert new parts
1175         for (final MessagePartData messagePart : message.getParts()) {
1176             messagePart.updateMessageId(messageId);
1177             insertNewMessagePartInTransaction(dbWrapper, messagePart, message.getConversationId());
1178         }
1179     }
1180 
1181     /**
1182      * Update a message and add its parts into the table
1183      */
1184     @DoesNotRunOnMainThread
updateMessageInTransaction(final DatabaseWrapper dbWrapper, final MessageData message)1185     public static void updateMessageInTransaction(final DatabaseWrapper dbWrapper,
1186             final MessageData message) {
1187         Assert.isNotMainThread();
1188         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1189         final String messageId = message.getMessageId();
1190         // Check message still exists (sms sync or delete might have purged it)
1191         final MessageData current = BugleDatabaseOperations.readMessage(dbWrapper, messageId);
1192         if (current != null) {
1193             // Delete existing message parts)
1194             deletePartsForMessage(dbWrapper, message.getMessageId());
1195             //  Insert new parts
1196             for (final MessagePartData messagePart : message.getParts()) {
1197                 messagePart.updatePartId(null);
1198                 messagePart.updateMessageId(message.getMessageId());
1199                 insertNewMessagePartInTransaction(dbWrapper, messagePart,
1200                         message.getConversationId());
1201             }
1202             //  Update message row
1203             final ContentValues values = new ContentValues();
1204             message.populate(values);
1205             updateMessageRowIfExists(dbWrapper, message.getMessageId(), values);
1206         }
1207     }
1208 
1209     @DoesNotRunOnMainThread
updateMessageAndPartsInTransaction(final DatabaseWrapper dbWrapper, final MessageData message, final List<MessagePartData> partsToUpdate)1210     public static void updateMessageAndPartsInTransaction(final DatabaseWrapper dbWrapper,
1211             final MessageData message, final List<MessagePartData> partsToUpdate) {
1212         Assert.isNotMainThread();
1213         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1214         final ContentValues values = new ContentValues();
1215         for (final MessagePartData messagePart : partsToUpdate) {
1216             values.clear();
1217             messagePart.populate(values);
1218             updatePartRowIfExists(dbWrapper, messagePart.getPartId(), values);
1219         }
1220         values.clear();
1221         message.populate(values);
1222         updateMessageRowIfExists(dbWrapper, message.getMessageId(), values);
1223     }
1224 
1225     /**
1226      * Delete all parts for a message
1227      */
deletePartsForMessage(final DatabaseWrapper dbWrapper, final String messageId)1228     static void deletePartsForMessage(final DatabaseWrapper dbWrapper,
1229             final String messageId) {
1230         final int cnt = dbWrapper.delete(DatabaseHelper.PARTS_TABLE,
1231                 PartColumns.MESSAGE_ID + " =?",
1232                 new String[] { messageId });
1233         Assert.inRange(cnt, 0, Integer.MAX_VALUE);
1234     }
1235 
1236     /**
1237      * Delete one message and update the conversation (if necessary).
1238      *
1239      * @return number of rows deleted (should be 1 or 0).
1240      */
1241     @DoesNotRunOnMainThread
deleteMessage(final DatabaseWrapper dbWrapper, final String messageId)1242     public static int deleteMessage(final DatabaseWrapper dbWrapper, final String messageId) {
1243         Assert.isNotMainThread();
1244         dbWrapper.beginTransaction();
1245         try {
1246             // Read message to find out which conversation it is in
1247             final MessageData message = BugleDatabaseOperations.readMessage(dbWrapper, messageId);
1248 
1249             int count = 0;
1250             if (message != null) {
1251                 final String conversationId = message.getConversationId();
1252                 // Delete message
1253                 count = dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
1254                         MessageColumns._ID + "=?", new String[] { messageId });
1255 
1256                 if (!deleteConversationIfEmptyInTransaction(dbWrapper, conversationId)) {
1257                     // TODO: Should we leave the conversation sort timestamp alone?
1258                     refreshConversationMetadataInTransaction(dbWrapper, conversationId,
1259                             false/* shouldAutoSwitchSelfId */, false/*archived*/);
1260                 }
1261             }
1262             dbWrapper.setTransactionSuccessful();
1263             return count;
1264         } finally {
1265             dbWrapper.endTransaction();
1266         }
1267     }
1268 
1269     /**
1270      * Deletes the conversation if there are zero non-draft messages left.
1271      * <p>
1272      * This is necessary because the telephony database has a trigger that deletes threads after
1273      * their last message is deleted. We need to ensure that if a thread goes away, we also delete
1274      * the conversation in Bugle. We don't store draft messages in telephony, so we ignore those
1275      * when querying for the # of messages in the conversation.
1276      *
1277      * @return true if the conversation was deleted
1278      */
1279     @DoesNotRunOnMainThread
deleteConversationIfEmptyInTransaction(final DatabaseWrapper dbWrapper, final String conversationId)1280     public static boolean deleteConversationIfEmptyInTransaction(final DatabaseWrapper dbWrapper,
1281             final String conversationId) {
1282         Assert.isNotMainThread();
1283         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1284         Cursor cursor = null;
1285         try {
1286             // TODO: The refreshConversationMetadataInTransaction method below uses this
1287             // same query; maybe they should share this logic?
1288 
1289             // Check to see if there are any (non-draft) messages in the conversation
1290             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
1291                     REFRESH_CONVERSATION_MESSAGE_PROJECTION,
1292                     MessageColumns.CONVERSATION_ID + "=? AND " +
1293                     MessageColumns.STATUS + "!=" + MessageData.BUGLE_STATUS_OUTGOING_DRAFT,
1294                     new String[] { conversationId }, null, null,
1295                     MessageColumns.RECEIVED_TIMESTAMP + " DESC", "1" /* limit */);
1296             if (cursor.getCount() == 0) {
1297                 dbWrapper.delete(DatabaseHelper.CONVERSATIONS_TABLE,
1298                         ConversationColumns._ID + "=?", new String[] { conversationId });
1299                 LogUtil.i(TAG,
1300                         "BugleDatabaseOperations: Deleted empty conversation " + conversationId);
1301                 return true;
1302             } else {
1303                 return false;
1304             }
1305         } finally {
1306             if (cursor != null) {
1307                 cursor.close();
1308             }
1309         }
1310     }
1311 
1312     private static final String[] REFRESH_CONVERSATION_MESSAGE_PROJECTION = new String[] {
1313         MessageColumns._ID,
1314         MessageColumns.RECEIVED_TIMESTAMP,
1315         MessageColumns.SENDER_PARTICIPANT_ID
1316     };
1317 
1318     /**
1319      * Update conversation snippet, timestamp and optionally self id to match latest message in
1320      * conversation.
1321      */
1322     @DoesNotRunOnMainThread
refreshConversationMetadataInTransaction(final DatabaseWrapper dbWrapper, final String conversationId, final boolean shouldAutoSwitchSelfId, boolean keepArchived)1323     public static void refreshConversationMetadataInTransaction(final DatabaseWrapper dbWrapper,
1324             final String conversationId, final boolean shouldAutoSwitchSelfId,
1325             boolean keepArchived) {
1326         Assert.isNotMainThread();
1327         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1328         Cursor cursor = null;
1329         try {
1330             // Check to see if there are any (non-draft) messages in the conversation
1331             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
1332                     REFRESH_CONVERSATION_MESSAGE_PROJECTION,
1333                     MessageColumns.CONVERSATION_ID + "=? AND " +
1334                     MessageColumns.STATUS + "!=" + MessageData.BUGLE_STATUS_OUTGOING_DRAFT,
1335                     new String[] { conversationId }, null, null,
1336                     MessageColumns.RECEIVED_TIMESTAMP + " DESC", "1" /* limit */);
1337 
1338             if (cursor.moveToFirst()) {
1339                 // Refresh latest message in conversation
1340                 final String latestMessageId = cursor.getString(0);
1341                 final long latestMessageTimestamp = cursor.getLong(1);
1342                 final String senderParticipantId = cursor.getString(2);
1343                 final boolean senderBlocked = isBlockedParticipant(dbWrapper, senderParticipantId);
1344                 updateConversationMetadataInTransaction(dbWrapper, conversationId,
1345                         latestMessageId, latestMessageTimestamp, senderBlocked || keepArchived,
1346                         shouldAutoSwitchSelfId);
1347             }
1348         } finally {
1349             if (cursor != null) {
1350                 cursor.close();
1351             }
1352         }
1353     }
1354 
1355     /**
1356      * When moving/removing an existing message update conversation metadata if necessary
1357      * @param dbWrapper      db wrapper
1358      * @param conversationId conversation to modify
1359      * @param messageId      message that is leaving the conversation
1360      * @param shouldAutoSwitchSelfId should we try to auto-switch the conversation's self-id as a
1361      *        result of this call when we see a new latest message?
1362      * @param keepArchived   should we keep the conversation archived despite refresh
1363      */
1364     @DoesNotRunOnMainThread
maybeRefreshConversationMetadataInTransaction( final DatabaseWrapper dbWrapper, final String conversationId, final String messageId, final boolean shouldAutoSwitchSelfId, final boolean keepArchived)1365     public static void maybeRefreshConversationMetadataInTransaction(
1366             final DatabaseWrapper dbWrapper, final String conversationId, final String messageId,
1367             final boolean shouldAutoSwitchSelfId, final boolean keepArchived) {
1368         Assert.isNotMainThread();
1369         boolean refresh = true;
1370         if (!TextUtils.isEmpty(messageId)) {
1371             refresh = false;
1372             // Look for an existing conversation in the db with this conversation id
1373             Cursor cursor = null;
1374             try {
1375                 cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
1376                         new String[] { ConversationColumns.LATEST_MESSAGE_ID },
1377                         ConversationColumns._ID + "=?",
1378                         new String[] { conversationId },
1379                         null, null, null);
1380                 Assert.inRange(cursor.getCount(), 0, 1);
1381                 if (cursor.moveToFirst()) {
1382                     refresh = TextUtils.equals(cursor.getString(0), messageId);
1383                 }
1384             } finally {
1385                 if (cursor != null) {
1386                     cursor.close();
1387                 }
1388             }
1389         }
1390         if (refresh) {
1391             // TODO: I think it is okay to delete the conversation if it is empty...
1392             refreshConversationMetadataInTransaction(dbWrapper, conversationId,
1393                     shouldAutoSwitchSelfId, keepArchived);
1394         }
1395     }
1396 
1397 
1398 
1399     // SQL statement to query latest message if for particular conversation
1400     private static final String QUERY_CONVERSATIONS_LATEST_MESSAGE_SQL = "SELECT "
1401             + ConversationColumns.LATEST_MESSAGE_ID + " FROM " + DatabaseHelper.CONVERSATIONS_TABLE
1402             + " WHERE " + ConversationColumns._ID + "=? LIMIT 1";
1403 
1404     /**
1405      * Note this is not thread safe so callers need to make sure they own the wrapper + statements
1406      * while they call this and use the returned value.
1407      */
1408     @DoesNotRunOnMainThread
getQueryConversationsLatestMessageStatement( final DatabaseWrapper db, final String conversationId)1409     public static SQLiteStatement getQueryConversationsLatestMessageStatement(
1410             final DatabaseWrapper db, final String conversationId) {
1411         Assert.isNotMainThread();
1412         final SQLiteStatement query = db.getStatementInTransaction(
1413                 DatabaseWrapper.INDEX_QUERY_CONVERSATIONS_LATEST_MESSAGE,
1414                 QUERY_CONVERSATIONS_LATEST_MESSAGE_SQL);
1415         query.clearBindings();
1416         query.bindString(1, conversationId);
1417         return query;
1418     }
1419 
1420     // SQL statement to query latest message if for particular conversation
1421     private static final String QUERY_MESSAGES_LATEST_MESSAGE_SQL = "SELECT "
1422             + MessageColumns._ID + " FROM " + DatabaseHelper.MESSAGES_TABLE
1423             + " WHERE " + MessageColumns.CONVERSATION_ID + "=? ORDER BY "
1424             + MessageColumns.RECEIVED_TIMESTAMP + " DESC LIMIT 1";
1425 
1426     /**
1427      * Note this is not thread safe so callers need to make sure they own the wrapper + statements
1428      * while they call this and use the returned value.
1429      */
1430     @DoesNotRunOnMainThread
getQueryMessagesLatestMessageStatement( final DatabaseWrapper db, final String conversationId)1431     public static SQLiteStatement getQueryMessagesLatestMessageStatement(
1432             final DatabaseWrapper db, final String conversationId) {
1433         Assert.isNotMainThread();
1434         final SQLiteStatement query = db.getStatementInTransaction(
1435                 DatabaseWrapper.INDEX_QUERY_MESSAGES_LATEST_MESSAGE,
1436                 QUERY_MESSAGES_LATEST_MESSAGE_SQL);
1437         query.clearBindings();
1438         query.bindString(1, conversationId);
1439         return query;
1440     }
1441 
1442     /**
1443      * Update conversation metadata if necessary
1444      * @param dbWrapper      db wrapper
1445      * @param conversationId conversation to modify
1446      * @param shouldAutoSwitchSelfId should we try to auto-switch the conversation's self-id as a
1447      *                               result of this call when we see a new latest message?
1448      * @param keepArchived if the conversation should be kept archived
1449      */
1450     @DoesNotRunOnMainThread
maybeRefreshConversationMetadataInTransaction( final DatabaseWrapper dbWrapper, final String conversationId, final boolean shouldAutoSwitchSelfId, boolean keepArchived)1451     public static void maybeRefreshConversationMetadataInTransaction(
1452             final DatabaseWrapper dbWrapper, final String conversationId,
1453             final boolean shouldAutoSwitchSelfId, boolean keepArchived) {
1454         Assert.isNotMainThread();
1455         String currentLatestMessageId = null;
1456         String latestMessageId = null;
1457         try {
1458             final SQLiteStatement currentLatestMessageIdSql =
1459                     getQueryConversationsLatestMessageStatement(dbWrapper, conversationId);
1460             currentLatestMessageId = currentLatestMessageIdSql.simpleQueryForString();
1461 
1462             final SQLiteStatement latestMessageIdSql =
1463                     getQueryMessagesLatestMessageStatement(dbWrapper, conversationId);
1464             latestMessageId = latestMessageIdSql.simpleQueryForString();
1465         } catch (final SQLiteDoneException e) {
1466             LogUtil.e(TAG, "BugleDatabaseOperations: Query for latest message failed", e);
1467         }
1468 
1469         if (TextUtils.isEmpty(currentLatestMessageId) ||
1470                 !TextUtils.equals(currentLatestMessageId, latestMessageId)) {
1471             refreshConversationMetadataInTransaction(dbWrapper, conversationId,
1472                     shouldAutoSwitchSelfId, keepArchived);
1473         }
1474     }
1475 
getConversationExists(final DatabaseWrapper dbWrapper, final String conversationId)1476     static boolean getConversationExists(final DatabaseWrapper dbWrapper,
1477             final String conversationId) {
1478         // Look for an existing conversation in the db with this conversation id
1479         Cursor cursor = null;
1480         try {
1481             cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
1482                     new String[] { /* No projection */},
1483                     ConversationColumns._ID + "=?",
1484                     new String[] { conversationId },
1485                     null, null, null);
1486             return cursor.getCount() == 1;
1487         } finally {
1488             if (cursor != null) {
1489                 cursor.close();
1490             }
1491         }
1492     }
1493 
1494     /** Preserve parts in message but clear the stored draft */
1495     public static final int UPDATE_MODE_CLEAR_DRAFT = 1;
1496     /** Add the message as a draft */
1497     public static final int UPDATE_MODE_ADD_DRAFT = 2;
1498 
1499     /**
1500      * Update draft message for specified conversation
1501      * @param dbWrapper       local database (wrapped)
1502      * @param conversationId  conversation to update
1503      * @param message         Optional message to preserve attachments for (either as draft or for
1504      *                        sending)
1505      * @param updateMode      either {@link #UPDATE_MODE_CLEAR_DRAFT} or
1506      *                        {@link #UPDATE_MODE_ADD_DRAFT}
1507      * @return message id of newly written draft (else null)
1508      */
1509     @DoesNotRunOnMainThread
updateDraftMessageData(final DatabaseWrapper dbWrapper, final String conversationId, @Nullable final MessageData message, final int updateMode)1510     public static String updateDraftMessageData(final DatabaseWrapper dbWrapper,
1511             final String conversationId, @Nullable final MessageData message,
1512             final int updateMode) {
1513         Assert.isNotMainThread();
1514         Assert.notNull(conversationId);
1515         Assert.inRange(updateMode, UPDATE_MODE_CLEAR_DRAFT, UPDATE_MODE_ADD_DRAFT);
1516         String messageId = null;
1517         Cursor cursor = null;
1518         dbWrapper.beginTransaction();
1519         try {
1520             // Find all draft parts for the current conversation
1521             final SimpleArrayMap<Uri, MessagePartData> currentDraftParts = new SimpleArrayMap<>();
1522             cursor = dbWrapper.query(DatabaseHelper.DRAFT_PARTS_VIEW,
1523                     MessagePartData.getProjection(),
1524                     MessageColumns.CONVERSATION_ID + " =?",
1525                     new String[] { conversationId }, null, null, null);
1526             while (cursor.moveToNext()) {
1527                 final MessagePartData part = MessagePartData.createFromCursor(cursor);
1528                 if (part.isAttachment()) {
1529                     currentDraftParts.put(part.getContentUri(), part);
1530                 }
1531             }
1532             // Optionally, preserve attachments for "message"
1533             final boolean conversationExists = getConversationExists(dbWrapper, conversationId);
1534             if (message != null && conversationExists) {
1535                 for (final MessagePartData part : message.getParts()) {
1536                     if (part.isAttachment()) {
1537                         currentDraftParts.remove(part.getContentUri());
1538                     }
1539                 }
1540             }
1541 
1542             // Delete orphan content
1543             for (int index = 0; index < currentDraftParts.size(); index++) {
1544                 final MessagePartData part = currentDraftParts.valueAt(index);
1545                 part.destroySync();
1546             }
1547 
1548             // Delete existing draft (cascade deletes parts)
1549             dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
1550                     MessageColumns.STATUS + "=? AND " + MessageColumns.CONVERSATION_ID + "=?",
1551                     new String[] {
1552                         Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_DRAFT),
1553                         conversationId
1554                     });
1555 
1556             // Write new draft
1557             if (updateMode == UPDATE_MODE_ADD_DRAFT && message != null
1558                     && message.hasContent() && conversationExists) {
1559                 Assert.equals(MessageData.BUGLE_STATUS_OUTGOING_DRAFT,
1560                         message.getStatus());
1561 
1562                 // Now add draft to message table
1563                 insertNewMessageInTransaction(dbWrapper, message);
1564                 messageId = message.getMessageId();
1565             }
1566 
1567             if (conversationExists) {
1568                 updateConversationDraftSnippetAndPreviewInTransaction(
1569                         dbWrapper, conversationId, message);
1570 
1571                 if (message != null && message.getSelfId() != null) {
1572                     updateConversationSelfIdInTransaction(dbWrapper, conversationId,
1573                             message.getSelfId());
1574                 }
1575             }
1576 
1577             dbWrapper.setTransactionSuccessful();
1578         } finally {
1579             dbWrapper.endTransaction();
1580             if (cursor != null) {
1581                 cursor.close();
1582             }
1583         }
1584         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
1585             LogUtil.v(TAG,
1586                     "Updated draft message " + messageId + " for conversation " + conversationId);
1587         }
1588         return messageId;
1589     }
1590 
1591     /**
1592      * Read the first draft message associated with this conversation.
1593      * If none present create an empty (sms) draft message.
1594      */
1595     @DoesNotRunOnMainThread
readDraftMessageData(final DatabaseWrapper dbWrapper, final String conversationId, final String conversationSelfId)1596     public static MessageData readDraftMessageData(final DatabaseWrapper dbWrapper,
1597             final String conversationId, final String conversationSelfId) {
1598         Assert.isNotMainThread();
1599         MessageData message = null;
1600         Cursor cursor = null;
1601         dbWrapper.beginTransaction();
1602         try {
1603             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
1604                     MessageData.getProjection(),
1605                     MessageColumns.STATUS + "=? AND " + MessageColumns.CONVERSATION_ID + "=?",
1606                     new String[] {
1607                         Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_DRAFT),
1608                         conversationId
1609                     }, null, null, null);
1610             Assert.inRange(cursor.getCount(), 0, 1);
1611             if (cursor.moveToFirst()) {
1612                 message = new MessageData();
1613                 message.bindDraft(cursor, conversationSelfId);
1614                 readMessagePartsData(dbWrapper, message, true);
1615                 // Disconnect draft parts from DB
1616                 for (final MessagePartData part : message.getParts()) {
1617                     part.updatePartId(null);
1618                     part.updateMessageId(null);
1619                 }
1620                 message.updateMessageId(null);
1621             }
1622             dbWrapper.setTransactionSuccessful();
1623         } finally {
1624             dbWrapper.endTransaction();
1625             if (cursor != null) {
1626                 cursor.close();
1627             }
1628         }
1629         return message;
1630     }
1631 
1632     // Internal
addParticipantToConversation(final DatabaseWrapper dbWrapper, final ParticipantData participant, final String conversationId)1633     private static void addParticipantToConversation(final DatabaseWrapper dbWrapper,
1634             final ParticipantData participant, final String conversationId) {
1635         final String participantId = getOrCreateParticipantInTransaction(dbWrapper, participant);
1636         Assert.notNull(participantId);
1637 
1638         // Add the participant to the conversation participants table
1639         final ContentValues values = new ContentValues();
1640         values.put(ConversationParticipantsColumns.CONVERSATION_ID, conversationId);
1641         values.put(ConversationParticipantsColumns.PARTICIPANT_ID, participantId);
1642         dbWrapper.insert(DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE, null, values);
1643     }
1644 
1645     /**
1646      * Get string used as canonical recipient for participant cache for sub id
1647      */
getCanonicalRecipientFromSubId(final int subId)1648     private static String getCanonicalRecipientFromSubId(final int subId) {
1649         return "SELF(" + subId + ")";
1650     }
1651 
1652     /**
1653      * Maps from a sub id or phone number to a participant id if there is one.
1654      *
1655      * @return If the participant is available in our cache, or the DB, this returns the
1656      * participant id for the given subid/phone number.  Otherwise it returns null.
1657      */
1658     @VisibleForTesting
getParticipantId(final DatabaseWrapper dbWrapper, final int subId, final String canonicalRecipient)1659     private static String getParticipantId(final DatabaseWrapper dbWrapper,
1660             final int subId, final String canonicalRecipient) {
1661         // First check our memory cache for the participant Id
1662         String participantId;
1663         synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
1664             participantId = sNormalizedPhoneNumberToParticipantIdCache.get(canonicalRecipient);
1665         }
1666 
1667         if (participantId != null) {
1668             return participantId;
1669         }
1670 
1671         // This code will only be executed for incremental additions.
1672         Cursor cursor = null;
1673         try {
1674             if (subId != ParticipantData.OTHER_THAN_SELF_SUB_ID) {
1675                 // Now look for an existing participant in the db with this sub id.
1676                 cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
1677                         new String[] {ParticipantColumns._ID},
1678                         ParticipantColumns.SUB_ID + "=?",
1679                         new String[] { Integer.toString(subId) }, null, null, null);
1680             } else {
1681                 // Look for existing participant with this normalized phone number and no subId.
1682                 cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
1683                         new String[] {ParticipantColumns._ID},
1684                         ParticipantColumns.NORMALIZED_DESTINATION + "=? AND "
1685                                 + ParticipantColumns.SUB_ID + "=?",
1686                                 new String[] {canonicalRecipient, Integer.toString(subId)},
1687                                 null, null, null);
1688             }
1689 
1690             if (cursor.moveToFirst()) {
1691                 // TODO Is this assert correct for multi-sim where a new sim was put in?
1692                 Assert.isTrue(cursor.getCount() == 1);
1693 
1694                 // We found an existing participant in the database
1695                 participantId = cursor.getString(0);
1696 
1697                 synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
1698                     // Add it to the cache for next time
1699                     sNormalizedPhoneNumberToParticipantIdCache.put(canonicalRecipient,
1700                             participantId);
1701                 }
1702             }
1703         } finally {
1704             if (cursor != null) {
1705                 cursor.close();
1706             }
1707         }
1708         return participantId;
1709     }
1710 
1711     @DoesNotRunOnMainThread
getOrCreateSelf(final DatabaseWrapper dbWrapper, final int subId)1712     public static ParticipantData getOrCreateSelf(final DatabaseWrapper dbWrapper,
1713             final int subId) {
1714         Assert.isNotMainThread();
1715         ParticipantData participant = null;
1716         dbWrapper.beginTransaction();
1717         try {
1718             final ParticipantData shell = ParticipantData.getSelfParticipant(subId);
1719             final String participantId = getOrCreateParticipantInTransaction(dbWrapper, shell);
1720             participant = getExistingParticipant(dbWrapper, participantId);
1721             dbWrapper.setTransactionSuccessful();
1722         } finally {
1723             dbWrapper.endTransaction();
1724         }
1725         return participant;
1726     }
1727 
1728     /**
1729      * Lookup and if necessary create a new participant
1730      * @param dbWrapper      Database wrapper
1731      * @param participant    Participant to find/create
1732      * @return participantId ParticipantId for existing or newly created participant
1733      */
1734     @DoesNotRunOnMainThread
getOrCreateParticipantInTransaction(final DatabaseWrapper dbWrapper, final ParticipantData participant)1735     public static String getOrCreateParticipantInTransaction(final DatabaseWrapper dbWrapper,
1736             final ParticipantData participant) {
1737         Assert.isNotMainThread();
1738         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1739         int subId = ParticipantData.OTHER_THAN_SELF_SUB_ID;
1740         String participantId = null;
1741         String canonicalRecipient = null;
1742         if (participant.isSelf()) {
1743             subId = participant.getSubId();
1744             canonicalRecipient = getCanonicalRecipientFromSubId(subId);
1745         } else {
1746             canonicalRecipient = participant.getNormalizedDestination();
1747         }
1748         Assert.notNull(canonicalRecipient);
1749         participantId = getParticipantId(dbWrapper, subId, canonicalRecipient);
1750 
1751         if (participantId != null) {
1752             return participantId;
1753         }
1754 
1755         if (!participant.isContactIdResolved()) {
1756             // Refresh participant's name and avatar with matching contact in CP2.
1757             ParticipantRefresh.refreshParticipant(dbWrapper, participant);
1758         }
1759 
1760         // Insert the participant into the participants table
1761         final ContentValues values = participant.toContentValues();
1762         final long participantRow = dbWrapper.insert(DatabaseHelper.PARTICIPANTS_TABLE, null,
1763                 values);
1764         participantId = Long.toString(participantRow);
1765         Assert.notNull(canonicalRecipient);
1766 
1767         synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
1768             // Now that we've inserted it, add it to our cache
1769             sNormalizedPhoneNumberToParticipantIdCache.put(canonicalRecipient, participantId);
1770         }
1771 
1772         return participantId;
1773     }
1774 
1775     @DoesNotRunOnMainThread
updateDestination(final DatabaseWrapper dbWrapper, final String destination, final boolean blocked)1776     public static void updateDestination(final DatabaseWrapper dbWrapper,
1777             final String destination, final boolean blocked) {
1778         Assert.isNotMainThread();
1779         final ContentValues values = new ContentValues();
1780         values.put(ParticipantColumns.BLOCKED, blocked ? 1 : 0);
1781         dbWrapper.update(DatabaseHelper.PARTICIPANTS_TABLE, values,
1782                 ParticipantColumns.NORMALIZED_DESTINATION + "=? AND " +
1783                         ParticipantColumns.SUB_ID + "=?",
1784                 new String[] { destination, Integer.toString(
1785                         ParticipantData.OTHER_THAN_SELF_SUB_ID) });
1786     }
1787 
1788     @DoesNotRunOnMainThread
getConversationFromOtherParticipantDestination( final DatabaseWrapper db, final String otherDestination)1789     public static String getConversationFromOtherParticipantDestination(
1790             final DatabaseWrapper db, final String otherDestination) {
1791         Assert.isNotMainThread();
1792         Cursor cursor = null;
1793         try {
1794             cursor = db.query(DatabaseHelper.CONVERSATIONS_TABLE,
1795                     new String[] { ConversationColumns._ID },
1796                     ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION + "=?",
1797                     new String[] { otherDestination }, null, null, null);
1798             Assert.inRange(cursor.getCount(), 0, 1);
1799             if (cursor.moveToFirst()) {
1800                 return cursor.getString(0);
1801             }
1802         } finally {
1803             if (cursor != null) {
1804                 cursor.close();
1805             }
1806         }
1807         return null;
1808     }
1809 
1810 
1811     /**
1812      * Get a list of conversations that contain any of participants specified.
1813      */
getConversationsForParticipants( final ArrayList<String> participantIds)1814     private static HashSet<String> getConversationsForParticipants(
1815             final ArrayList<String> participantIds) {
1816         final DatabaseWrapper db = DataModel.get().getDatabase();
1817         final HashSet<String> conversationIds = new HashSet<String>();
1818 
1819         final String selection = ConversationParticipantsColumns.PARTICIPANT_ID + "=?";
1820         for (final String participantId : participantIds) {
1821             final String[] selectionArgs = new String[] { participantId };
1822             final Cursor cursor = db.query(DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE,
1823                     ConversationParticipantsQuery.PROJECTION,
1824                     selection, selectionArgs, null, null, null);
1825 
1826             if (cursor != null) {
1827                 try {
1828                     while (cursor.moveToNext()) {
1829                         final String conversationId = cursor.getString(
1830                                 ConversationParticipantsQuery.INDEX_CONVERSATION_ID);
1831                         conversationIds.add(conversationId);
1832                     }
1833                 } finally {
1834                     cursor.close();
1835                 }
1836             }
1837         }
1838 
1839         return conversationIds;
1840     }
1841 
1842     /**
1843      * Refresh conversation names/avatars based on a list of participants that are changed.
1844      */
1845     @DoesNotRunOnMainThread
refreshConversationsForParticipants(final ArrayList<String> participants)1846     public static void refreshConversationsForParticipants(final ArrayList<String> participants) {
1847         Assert.isNotMainThread();
1848         final HashSet<String> conversationIds = getConversationsForParticipants(participants);
1849         if (conversationIds.size() > 0) {
1850             for (final String conversationId : conversationIds) {
1851                 refreshConversation(conversationId);
1852             }
1853 
1854             MessagingContentProvider.notifyConversationListChanged();
1855             if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
1856                 LogUtil.v(TAG, "Number of conversations refreshed:" + conversationIds.size());
1857             }
1858         }
1859     }
1860 
1861     /**
1862      * Refresh conversation names/avatars based on a changed participant.
1863      */
1864     @DoesNotRunOnMainThread
refreshConversationsForParticipant(final String participantId)1865     public static void refreshConversationsForParticipant(final String participantId) {
1866         Assert.isNotMainThread();
1867         final ArrayList<String> participantList = new ArrayList<String>(1);
1868         participantList.add(participantId);
1869         refreshConversationsForParticipants(participantList);
1870     }
1871 
1872     /**
1873      * Refresh one conversation.
1874      */
refreshConversation(final String conversationId)1875     private static void refreshConversation(final String conversationId) {
1876         final DatabaseWrapper db = DataModel.get().getDatabase();
1877 
1878         db.beginTransaction();
1879         try {
1880             BugleDatabaseOperations.updateConversationNameAndAvatarInTransaction(db,
1881                     conversationId);
1882             db.setTransactionSuccessful();
1883         } finally {
1884             db.endTransaction();
1885         }
1886 
1887         MessagingContentProvider.notifyParticipantsChanged(conversationId);
1888         MessagingContentProvider.notifyMessagesChanged(conversationId);
1889         MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
1890     }
1891 
1892     @DoesNotRunOnMainThread
updateRowIfExists(final DatabaseWrapper db, final String table, final String rowKey, final String rowId, final ContentValues values)1893     public static boolean updateRowIfExists(final DatabaseWrapper db, final String table,
1894             final String rowKey, final String rowId, final ContentValues values) {
1895         Assert.isNotMainThread();
1896         final StringBuilder sb = new StringBuilder();
1897         final ArrayList<String> whereValues = new ArrayList<String>(values.size() + 1);
1898         whereValues.add(rowId);
1899 
1900         for (final String key : values.keySet()) {
1901             if (sb.length() > 0) {
1902                 sb.append(" OR ");
1903             }
1904             final Object value = values.get(key);
1905             sb.append(key);
1906             if (value != null) {
1907                 sb.append(" IS NOT ?");
1908                 whereValues.add(value.toString());
1909             } else {
1910                 sb.append(" IS NOT NULL");
1911             }
1912         }
1913 
1914         final String whereClause = rowKey + "=?" + " AND (" + sb.toString() + ")";
1915         final String [] whereValuesArray = whereValues.toArray(new String[whereValues.size()]);
1916         final int count = db.update(table, values, whereClause, whereValuesArray);
1917         if (count > 1) {
1918             LogUtil.w(LogUtil.BUGLE_TAG, "Updated more than 1 row " + count + "; " + table +
1919                     " for " + rowKey + " = " + rowId + " (deleted?)");
1920         }
1921         Assert.inRange(count, 0, 1);
1922         return (count >= 0);
1923     }
1924 }
1925