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 android.support.v4.util.ArrayMap;
27 import android.support.v4.util.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         fillParticipantData(values, participants);
845 
846         // Used by background thread when refreshing conversation so conversation could be deleted.
847         updateConversationRowIfExists(dbWrapper, conversationId, values);
848 
849         WidgetConversationProvider.notifyConversationRenamed(Factory.get().getApplicationContext(),
850                 conversationId);
851     }
852 
853     /**
854      * Updates a given conversation's self id.
855      */
856     @DoesNotRunOnMainThread
updateConversationSelfIdInTransaction( final DatabaseWrapper dbWrapper, final String conversationId, final String selfId)857     public static void updateConversationSelfIdInTransaction(
858             final DatabaseWrapper dbWrapper, final String conversationId, final String selfId) {
859         Assert.isNotMainThread();
860         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
861         final ContentValues values = new ContentValues();
862         if (addConversationSelfIdToContentValues(dbWrapper, selfId, values)) {
863             updateConversationRowIfExists(dbWrapper, conversationId, values);
864         }
865     }
866 
867     @DoesNotRunOnMainThread
getConversationSelfId(final DatabaseWrapper dbWrapper, final String conversationId)868     public static String getConversationSelfId(final DatabaseWrapper dbWrapper,
869             final String conversationId) {
870         Assert.isNotMainThread();
871         Cursor cursor = null;
872         try {
873             cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
874                     new String[] { ConversationColumns.CURRENT_SELF_ID },
875                     ConversationColumns._ID + "=?",
876                     new String[] { conversationId },
877                     null, null, null);
878             Assert.inRange(cursor.getCount(), 0, 1);
879             if (cursor.moveToFirst()) {
880                 return cursor.getString(0);
881             }
882         } finally {
883             if (cursor != null) {
884                 cursor.close();
885             }
886         }
887         return null;
888     }
889 
890     /**
891      * Frees up memory associated with phone number to participant id matching.
892      */
893     @DoesNotRunOnMainThread
clearParticipantIdCache()894     public static void clearParticipantIdCache() {
895         Assert.isNotMainThread();
896         synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
897             sNormalizedPhoneNumberToParticipantIdCache.clear();
898         }
899     }
900 
901     @DoesNotRunOnMainThread
getRecipientsForConversation(final DatabaseWrapper dbWrapper, final String conversationId)902     public static ArrayList<String> getRecipientsForConversation(final DatabaseWrapper dbWrapper,
903             final String conversationId) {
904         Assert.isNotMainThread();
905         final ArrayList<ParticipantData> participants =
906                 getParticipantsForConversation(dbWrapper, conversationId);
907 
908         final ArrayList<String> recipients = new ArrayList<String>();
909         for (final ParticipantData participant : participants) {
910             recipients.add(participant.getSendDestination());
911         }
912 
913         return recipients;
914     }
915 
916     @DoesNotRunOnMainThread
getSmsServiceCenterForConversation(final DatabaseWrapper dbWrapper, final String conversationId)917     public static String getSmsServiceCenterForConversation(final DatabaseWrapper dbWrapper,
918             final String conversationId) {
919         Assert.isNotMainThread();
920         Cursor cursor = null;
921         try {
922             cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
923                     new String[] { ConversationColumns.SMS_SERVICE_CENTER },
924                     ConversationColumns._ID + "=?",
925                     new String[] { conversationId },
926                     null, null, null);
927             Assert.inRange(cursor.getCount(), 0, 1);
928             if (cursor.moveToFirst()) {
929                 return cursor.getString(0);
930             }
931         } finally {
932             if (cursor != null) {
933                 cursor.close();
934             }
935         }
936         return null;
937     }
938 
939     @DoesNotRunOnMainThread
getExistingParticipant(final DatabaseWrapper dbWrapper, final String participantId)940     public static ParticipantData getExistingParticipant(final DatabaseWrapper dbWrapper,
941             final String participantId) {
942         Assert.isNotMainThread();
943         ParticipantData participant = null;
944         Cursor cursor = null;
945         try {
946             cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
947                     ParticipantData.ParticipantsQuery.PROJECTION,
948                     ParticipantColumns._ID + " =?",
949                     new String[] { participantId }, null, null, null);
950             Assert.inRange(cursor.getCount(), 0, 1);
951             if (cursor.moveToFirst()) {
952                 participant = ParticipantData.getFromCursor(cursor);
953             }
954         } finally {
955             if (cursor != null) {
956                 cursor.close();
957             }
958         }
959 
960         return participant;
961     }
962 
getSelfSubscriptionId(final DatabaseWrapper dbWrapper, final String selfParticipantId)963     static int getSelfSubscriptionId(final DatabaseWrapper dbWrapper,
964             final String selfParticipantId) {
965         final ParticipantData selfParticipant = BugleDatabaseOperations.getExistingParticipant(
966                 dbWrapper, selfParticipantId);
967         if (selfParticipant != null) {
968             Assert.isTrue(selfParticipant.isSelf());
969             return selfParticipant.getSubId();
970         }
971         return ParticipantData.DEFAULT_SELF_SUB_ID;
972     }
973 
974     @VisibleForTesting
975     @DoesNotRunOnMainThread
getParticipantsForConversation( final DatabaseWrapper dbWrapper, final String conversationId)976     public static ArrayList<ParticipantData> getParticipantsForConversation(
977             final DatabaseWrapper dbWrapper, final String conversationId) {
978         Assert.isNotMainThread();
979         final ArrayList<ParticipantData> participants =
980                 new ArrayList<ParticipantData>();
981         Cursor cursor = null;
982         try {
983             cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
984                     ParticipantData.ParticipantsQuery.PROJECTION,
985                     ParticipantColumns._ID + " IN ( " + "SELECT "
986                             + ConversationParticipantsColumns.PARTICIPANT_ID + " AS "
987                             + ParticipantColumns._ID
988                             + " FROM " + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE
989                             + " WHERE " + ConversationParticipantsColumns.CONVERSATION_ID + " =? )",
990                             new String[] { conversationId }, null, null, null);
991 
992             while (cursor.moveToNext()) {
993                 participants.add(ParticipantData.getFromCursor(cursor));
994             }
995         } finally {
996             if (cursor != null) {
997                 cursor.close();
998             }
999         }
1000 
1001         return participants;
1002     }
1003 
1004     @DoesNotRunOnMainThread
readMessage(final DatabaseWrapper dbWrapper, final String messageId)1005     public static MessageData readMessage(final DatabaseWrapper dbWrapper, final String messageId) {
1006         Assert.isNotMainThread();
1007         final MessageData message = readMessageData(dbWrapper, messageId);
1008         if (message != null) {
1009             readMessagePartsData(dbWrapper, message, false);
1010         }
1011         return message;
1012     }
1013 
1014     @VisibleForTesting
readMessagePartData(final DatabaseWrapper dbWrapper, final String partId)1015     static MessagePartData readMessagePartData(final DatabaseWrapper dbWrapper,
1016             final String partId) {
1017         MessagePartData messagePartData = null;
1018         Cursor cursor = null;
1019         try {
1020             cursor = dbWrapper.query(DatabaseHelper.PARTS_TABLE,
1021                     MessagePartData.getProjection(), PartColumns._ID + "=?",
1022                     new String[] { partId }, null, null, null);
1023             Assert.inRange(cursor.getCount(), 0, 1);
1024             if (cursor.moveToFirst()) {
1025                 messagePartData = MessagePartData.createFromCursor(cursor);
1026             }
1027         } finally {
1028             if (cursor != null) {
1029                 cursor.close();
1030             }
1031         }
1032         return messagePartData;
1033     }
1034 
1035     @DoesNotRunOnMainThread
readMessageData(final DatabaseWrapper dbWrapper, final Uri smsMessageUri)1036     public static MessageData readMessageData(final DatabaseWrapper dbWrapper,
1037             final Uri smsMessageUri) {
1038         Assert.isNotMainThread();
1039         MessageData message = null;
1040         Cursor cursor = null;
1041         try {
1042             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
1043                     MessageData.getProjection(), MessageColumns.SMS_MESSAGE_URI + "=?",
1044                     new String[] { smsMessageUri.toString() }, null, null, null);
1045             Assert.inRange(cursor.getCount(), 0, 1);
1046             if (cursor.moveToFirst()) {
1047                 message = new MessageData();
1048                 message.bind(cursor);
1049             }
1050         } finally {
1051             if (cursor != null) {
1052                 cursor.close();
1053             }
1054         }
1055         return message;
1056     }
1057 
1058     @DoesNotRunOnMainThread
readMessageData(final DatabaseWrapper dbWrapper, final String messageId)1059     public static MessageData readMessageData(final DatabaseWrapper dbWrapper,
1060             final String messageId) {
1061         Assert.isNotMainThread();
1062         MessageData message = null;
1063         Cursor cursor = null;
1064         try {
1065             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
1066                     MessageData.getProjection(), MessageColumns._ID + "=?",
1067                     new String[] { messageId }, null, null, null);
1068             Assert.inRange(cursor.getCount(), 0, 1);
1069             if (cursor.moveToFirst()) {
1070                 message = new MessageData();
1071                 message.bind(cursor);
1072             }
1073         } finally {
1074             if (cursor != null) {
1075                 cursor.close();
1076             }
1077         }
1078         return message;
1079     }
1080 
1081     /**
1082      * Read all the parts for a message
1083      * @param dbWrapper database
1084      * @param message read parts for this message
1085      * @param checkAttachmentFilesExist check each attachment file and only include if file exists
1086      */
readMessagePartsData(final DatabaseWrapper dbWrapper, final MessageData message, final boolean checkAttachmentFilesExist)1087     private static void readMessagePartsData(final DatabaseWrapper dbWrapper,
1088             final MessageData message, final boolean checkAttachmentFilesExist) {
1089         final ContentResolver contentResolver =
1090                 Factory.get().getApplicationContext().getContentResolver();
1091         Cursor cursor = null;
1092         try {
1093             cursor = dbWrapper.query(DatabaseHelper.PARTS_TABLE,
1094                     MessagePartData.getProjection(), PartColumns.MESSAGE_ID + "=?",
1095                     new String[] { message.getMessageId() }, null, null, null);
1096             while (cursor.moveToNext()) {
1097                 final MessagePartData messagePartData = MessagePartData.createFromCursor(cursor);
1098                 if (checkAttachmentFilesExist && messagePartData.isAttachment() &&
1099                         !UriUtil.isBugleAppResource(messagePartData.getContentUri())) {
1100                     try {
1101                         // Test that the file exists before adding the attachment to the draft
1102                         final ParcelFileDescriptor fileDescriptor =
1103                                 contentResolver.openFileDescriptor(
1104                                         messagePartData.getContentUri(), "r");
1105                         if (fileDescriptor != null) {
1106                             fileDescriptor.close();
1107                             message.addPart(messagePartData);
1108                         }
1109                     } catch (final IOException e) {
1110                         // The attachment's temp storage no longer exists, just ignore the file
1111                     } catch (final SecurityException e) {
1112                         // Likely thrown by openFileDescriptor due to an expired access grant.
1113                         if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
1114                             LogUtil.d(LogUtil.BUGLE_TAG, "uri: " + messagePartData.getContentUri());
1115                         }
1116                     }
1117                 } else {
1118                     message.addPart(messagePartData);
1119                 }
1120             }
1121         } finally {
1122             if (cursor != null) {
1123                 cursor.close();
1124             }
1125         }
1126     }
1127 
1128     /**
1129      * Write a message part to our local database
1130      *
1131      * @param dbWrapper     The database
1132      * @param messagePart   The message part to insert
1133      * @return The row id of the newly inserted part
1134      */
insertNewMessagePartInTransaction(final DatabaseWrapper dbWrapper, final MessagePartData messagePart, final String conversationId)1135     static String insertNewMessagePartInTransaction(final DatabaseWrapper dbWrapper,
1136             final MessagePartData messagePart, final String conversationId) {
1137         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1138         Assert.isTrue(!TextUtils.isEmpty(messagePart.getMessageId()));
1139 
1140         // Insert a new part row
1141         final SQLiteStatement insert = messagePart.getInsertStatement(dbWrapper, conversationId);
1142         final long rowNumber = insert.executeInsert();
1143 
1144         Assert.inRange(rowNumber, 0, Long.MAX_VALUE);
1145         final String partId = Long.toString(rowNumber);
1146 
1147         // Update the part id
1148         messagePart.updatePartId(partId);
1149 
1150         return partId;
1151     }
1152 
1153     /**
1154      * Insert a message and its parts into the table
1155      */
1156     @DoesNotRunOnMainThread
insertNewMessageInTransaction(final DatabaseWrapper dbWrapper, final MessageData message)1157     public static void insertNewMessageInTransaction(final DatabaseWrapper dbWrapper,
1158             final MessageData message) {
1159         Assert.isNotMainThread();
1160         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1161 
1162         // Insert message row
1163         final SQLiteStatement insert = message.getInsertStatement(dbWrapper);
1164         final long rowNumber = insert.executeInsert();
1165 
1166         Assert.inRange(rowNumber, 0, Long.MAX_VALUE);
1167         final String messageId = Long.toString(rowNumber);
1168         message.updateMessageId(messageId);
1169         //  Insert new parts
1170         for (final MessagePartData messagePart : message.getParts()) {
1171             messagePart.updateMessageId(messageId);
1172             insertNewMessagePartInTransaction(dbWrapper, messagePart, message.getConversationId());
1173         }
1174     }
1175 
1176     /**
1177      * Update a message and add its parts into the table
1178      */
1179     @DoesNotRunOnMainThread
updateMessageInTransaction(final DatabaseWrapper dbWrapper, final MessageData message)1180     public static void updateMessageInTransaction(final DatabaseWrapper dbWrapper,
1181             final MessageData message) {
1182         Assert.isNotMainThread();
1183         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1184         final String messageId = message.getMessageId();
1185         // Check message still exists (sms sync or delete might have purged it)
1186         final MessageData current = BugleDatabaseOperations.readMessage(dbWrapper, messageId);
1187         if (current != null) {
1188             // Delete existing message parts)
1189             deletePartsForMessage(dbWrapper, message.getMessageId());
1190             //  Insert new parts
1191             for (final MessagePartData messagePart : message.getParts()) {
1192                 messagePart.updatePartId(null);
1193                 messagePart.updateMessageId(message.getMessageId());
1194                 insertNewMessagePartInTransaction(dbWrapper, messagePart,
1195                         message.getConversationId());
1196             }
1197             //  Update message row
1198             final ContentValues values = new ContentValues();
1199             message.populate(values);
1200             updateMessageRowIfExists(dbWrapper, message.getMessageId(), values);
1201         }
1202     }
1203 
1204     @DoesNotRunOnMainThread
updateMessageAndPartsInTransaction(final DatabaseWrapper dbWrapper, final MessageData message, final List<MessagePartData> partsToUpdate)1205     public static void updateMessageAndPartsInTransaction(final DatabaseWrapper dbWrapper,
1206             final MessageData message, final List<MessagePartData> partsToUpdate) {
1207         Assert.isNotMainThread();
1208         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1209         final ContentValues values = new ContentValues();
1210         for (final MessagePartData messagePart : partsToUpdate) {
1211             values.clear();
1212             messagePart.populate(values);
1213             updatePartRowIfExists(dbWrapper, messagePart.getPartId(), values);
1214         }
1215         values.clear();
1216         message.populate(values);
1217         updateMessageRowIfExists(dbWrapper, message.getMessageId(), values);
1218     }
1219 
1220     /**
1221      * Delete all parts for a message
1222      */
deletePartsForMessage(final DatabaseWrapper dbWrapper, final String messageId)1223     static void deletePartsForMessage(final DatabaseWrapper dbWrapper,
1224             final String messageId) {
1225         final int cnt = dbWrapper.delete(DatabaseHelper.PARTS_TABLE,
1226                 PartColumns.MESSAGE_ID + " =?",
1227                 new String[] { messageId });
1228         Assert.inRange(cnt, 0, Integer.MAX_VALUE);
1229     }
1230 
1231     /**
1232      * Delete one message and update the conversation (if necessary).
1233      *
1234      * @return number of rows deleted (should be 1 or 0).
1235      */
1236     @DoesNotRunOnMainThread
deleteMessage(final DatabaseWrapper dbWrapper, final String messageId)1237     public static int deleteMessage(final DatabaseWrapper dbWrapper, final String messageId) {
1238         Assert.isNotMainThread();
1239         dbWrapper.beginTransaction();
1240         try {
1241             // Read message to find out which conversation it is in
1242             final MessageData message = BugleDatabaseOperations.readMessage(dbWrapper, messageId);
1243 
1244             int count = 0;
1245             if (message != null) {
1246                 final String conversationId = message.getConversationId();
1247                 // Delete message
1248                 count = dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
1249                         MessageColumns._ID + "=?", new String[] { messageId });
1250 
1251                 if (!deleteConversationIfEmptyInTransaction(dbWrapper, conversationId)) {
1252                     // TODO: Should we leave the conversation sort timestamp alone?
1253                     refreshConversationMetadataInTransaction(dbWrapper, conversationId,
1254                             false/* shouldAutoSwitchSelfId */, false/*archived*/);
1255                 }
1256             }
1257             dbWrapper.setTransactionSuccessful();
1258             return count;
1259         } finally {
1260             dbWrapper.endTransaction();
1261         }
1262     }
1263 
1264     /**
1265      * Deletes the conversation if there are zero non-draft messages left.
1266      * <p>
1267      * This is necessary because the telephony database has a trigger that deletes threads after
1268      * their last message is deleted. We need to ensure that if a thread goes away, we also delete
1269      * the conversation in Bugle. We don't store draft messages in telephony, so we ignore those
1270      * when querying for the # of messages in the conversation.
1271      *
1272      * @return true if the conversation was deleted
1273      */
1274     @DoesNotRunOnMainThread
deleteConversationIfEmptyInTransaction(final DatabaseWrapper dbWrapper, final String conversationId)1275     public static boolean deleteConversationIfEmptyInTransaction(final DatabaseWrapper dbWrapper,
1276             final String conversationId) {
1277         Assert.isNotMainThread();
1278         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1279         Cursor cursor = null;
1280         try {
1281             // TODO: The refreshConversationMetadataInTransaction method below uses this
1282             // same query; maybe they should share this logic?
1283 
1284             // Check to see if there are any (non-draft) messages in the conversation
1285             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
1286                     REFRESH_CONVERSATION_MESSAGE_PROJECTION,
1287                     MessageColumns.CONVERSATION_ID + "=? AND " +
1288                     MessageColumns.STATUS + "!=" + MessageData.BUGLE_STATUS_OUTGOING_DRAFT,
1289                     new String[] { conversationId }, null, null,
1290                     MessageColumns.RECEIVED_TIMESTAMP + " DESC", "1" /* limit */);
1291             if (cursor.getCount() == 0) {
1292                 dbWrapper.delete(DatabaseHelper.CONVERSATIONS_TABLE,
1293                         ConversationColumns._ID + "=?", new String[] { conversationId });
1294                 LogUtil.i(TAG,
1295                         "BugleDatabaseOperations: Deleted empty conversation " + conversationId);
1296                 return true;
1297             } else {
1298                 return false;
1299             }
1300         } finally {
1301             if (cursor != null) {
1302                 cursor.close();
1303             }
1304         }
1305     }
1306 
1307     private static final String[] REFRESH_CONVERSATION_MESSAGE_PROJECTION = new String[] {
1308         MessageColumns._ID,
1309         MessageColumns.RECEIVED_TIMESTAMP,
1310         MessageColumns.SENDER_PARTICIPANT_ID
1311     };
1312 
1313     /**
1314      * Update conversation snippet, timestamp and optionally self id to match latest message in
1315      * conversation.
1316      */
1317     @DoesNotRunOnMainThread
refreshConversationMetadataInTransaction(final DatabaseWrapper dbWrapper, final String conversationId, final boolean shouldAutoSwitchSelfId, boolean keepArchived)1318     public static void refreshConversationMetadataInTransaction(final DatabaseWrapper dbWrapper,
1319             final String conversationId, final boolean shouldAutoSwitchSelfId,
1320             boolean keepArchived) {
1321         Assert.isNotMainThread();
1322         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1323         Cursor cursor = null;
1324         try {
1325             // Check to see if there are any (non-draft) messages in the conversation
1326             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
1327                     REFRESH_CONVERSATION_MESSAGE_PROJECTION,
1328                     MessageColumns.CONVERSATION_ID + "=? AND " +
1329                     MessageColumns.STATUS + "!=" + MessageData.BUGLE_STATUS_OUTGOING_DRAFT,
1330                     new String[] { conversationId }, null, null,
1331                     MessageColumns.RECEIVED_TIMESTAMP + " DESC", "1" /* limit */);
1332 
1333             if (cursor.moveToFirst()) {
1334                 // Refresh latest message in conversation
1335                 final String latestMessageId = cursor.getString(0);
1336                 final long latestMessageTimestamp = cursor.getLong(1);
1337                 final String senderParticipantId = cursor.getString(2);
1338                 final boolean senderBlocked = isBlockedParticipant(dbWrapper, senderParticipantId);
1339                 updateConversationMetadataInTransaction(dbWrapper, conversationId,
1340                         latestMessageId, latestMessageTimestamp, senderBlocked || keepArchived,
1341                         shouldAutoSwitchSelfId);
1342             }
1343         } finally {
1344             if (cursor != null) {
1345                 cursor.close();
1346             }
1347         }
1348     }
1349 
1350     /**
1351      * When moving/removing an existing message update conversation metadata if necessary
1352      * @param dbWrapper      db wrapper
1353      * @param conversationId conversation to modify
1354      * @param messageId      message that is leaving the conversation
1355      * @param shouldAutoSwitchSelfId should we try to auto-switch the conversation's self-id as a
1356      *        result of this call when we see a new latest message?
1357      * @param keepArchived   should we keep the conversation archived despite refresh
1358      */
1359     @DoesNotRunOnMainThread
maybeRefreshConversationMetadataInTransaction( final DatabaseWrapper dbWrapper, final String conversationId, final String messageId, final boolean shouldAutoSwitchSelfId, final boolean keepArchived)1360     public static void maybeRefreshConversationMetadataInTransaction(
1361             final DatabaseWrapper dbWrapper, final String conversationId, final String messageId,
1362             final boolean shouldAutoSwitchSelfId, final boolean keepArchived) {
1363         Assert.isNotMainThread();
1364         boolean refresh = true;
1365         if (!TextUtils.isEmpty(messageId)) {
1366             refresh = false;
1367             // Look for an existing conversation in the db with this conversation id
1368             Cursor cursor = null;
1369             try {
1370                 cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
1371                         new String[] { ConversationColumns.LATEST_MESSAGE_ID },
1372                         ConversationColumns._ID + "=?",
1373                         new String[] { conversationId },
1374                         null, null, null);
1375                 Assert.inRange(cursor.getCount(), 0, 1);
1376                 if (cursor.moveToFirst()) {
1377                     refresh = TextUtils.equals(cursor.getString(0), messageId);
1378                 }
1379             } finally {
1380                 if (cursor != null) {
1381                     cursor.close();
1382                 }
1383             }
1384         }
1385         if (refresh) {
1386             // TODO: I think it is okay to delete the conversation if it is empty...
1387             refreshConversationMetadataInTransaction(dbWrapper, conversationId,
1388                     shouldAutoSwitchSelfId, keepArchived);
1389         }
1390     }
1391 
1392 
1393 
1394     // SQL statement to query latest message if for particular conversation
1395     private static final String QUERY_CONVERSATIONS_LATEST_MESSAGE_SQL = "SELECT "
1396             + ConversationColumns.LATEST_MESSAGE_ID + " FROM " + DatabaseHelper.CONVERSATIONS_TABLE
1397             + " WHERE " + ConversationColumns._ID + "=? LIMIT 1";
1398 
1399     /**
1400      * Note this is not thread safe so callers need to make sure they own the wrapper + statements
1401      * while they call this and use the returned value.
1402      */
1403     @DoesNotRunOnMainThread
getQueryConversationsLatestMessageStatement( final DatabaseWrapper db, final String conversationId)1404     public static SQLiteStatement getQueryConversationsLatestMessageStatement(
1405             final DatabaseWrapper db, final String conversationId) {
1406         Assert.isNotMainThread();
1407         final SQLiteStatement query = db.getStatementInTransaction(
1408                 DatabaseWrapper.INDEX_QUERY_CONVERSATIONS_LATEST_MESSAGE,
1409                 QUERY_CONVERSATIONS_LATEST_MESSAGE_SQL);
1410         query.clearBindings();
1411         query.bindString(1, conversationId);
1412         return query;
1413     }
1414 
1415     // SQL statement to query latest message if for particular conversation
1416     private static final String QUERY_MESSAGES_LATEST_MESSAGE_SQL = "SELECT "
1417             + MessageColumns._ID + " FROM " + DatabaseHelper.MESSAGES_TABLE
1418             + " WHERE " + MessageColumns.CONVERSATION_ID + "=? ORDER BY "
1419             + MessageColumns.RECEIVED_TIMESTAMP + " DESC LIMIT 1";
1420 
1421     /**
1422      * Note this is not thread safe so callers need to make sure they own the wrapper + statements
1423      * while they call this and use the returned value.
1424      */
1425     @DoesNotRunOnMainThread
getQueryMessagesLatestMessageStatement( final DatabaseWrapper db, final String conversationId)1426     public static SQLiteStatement getQueryMessagesLatestMessageStatement(
1427             final DatabaseWrapper db, final String conversationId) {
1428         Assert.isNotMainThread();
1429         final SQLiteStatement query = db.getStatementInTransaction(
1430                 DatabaseWrapper.INDEX_QUERY_MESSAGES_LATEST_MESSAGE,
1431                 QUERY_MESSAGES_LATEST_MESSAGE_SQL);
1432         query.clearBindings();
1433         query.bindString(1, conversationId);
1434         return query;
1435     }
1436 
1437     /**
1438      * Update conversation metadata if necessary
1439      * @param dbWrapper      db wrapper
1440      * @param conversationId conversation to modify
1441      * @param shouldAutoSwitchSelfId should we try to auto-switch the conversation's self-id as a
1442      *                               result of this call when we see a new latest message?
1443      * @param keepArchived if the conversation should be kept archived
1444      */
1445     @DoesNotRunOnMainThread
maybeRefreshConversationMetadataInTransaction( final DatabaseWrapper dbWrapper, final String conversationId, final boolean shouldAutoSwitchSelfId, boolean keepArchived)1446     public static void maybeRefreshConversationMetadataInTransaction(
1447             final DatabaseWrapper dbWrapper, final String conversationId,
1448             final boolean shouldAutoSwitchSelfId, boolean keepArchived) {
1449         Assert.isNotMainThread();
1450         String currentLatestMessageId = null;
1451         String latestMessageId = null;
1452         try {
1453             final SQLiteStatement currentLatestMessageIdSql =
1454                     getQueryConversationsLatestMessageStatement(dbWrapper, conversationId);
1455             currentLatestMessageId = currentLatestMessageIdSql.simpleQueryForString();
1456 
1457             final SQLiteStatement latestMessageIdSql =
1458                     getQueryMessagesLatestMessageStatement(dbWrapper, conversationId);
1459             latestMessageId = latestMessageIdSql.simpleQueryForString();
1460         } catch (final SQLiteDoneException e) {
1461             LogUtil.e(TAG, "BugleDatabaseOperations: Query for latest message failed", e);
1462         }
1463 
1464         if (TextUtils.isEmpty(currentLatestMessageId) ||
1465                 !TextUtils.equals(currentLatestMessageId, latestMessageId)) {
1466             refreshConversationMetadataInTransaction(dbWrapper, conversationId,
1467                     shouldAutoSwitchSelfId, keepArchived);
1468         }
1469     }
1470 
getConversationExists(final DatabaseWrapper dbWrapper, final String conversationId)1471     static boolean getConversationExists(final DatabaseWrapper dbWrapper,
1472             final String conversationId) {
1473         // Look for an existing conversation in the db with this conversation id
1474         Cursor cursor = null;
1475         try {
1476             cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
1477                     new String[] { /* No projection */},
1478                     ConversationColumns._ID + "=?",
1479                     new String[] { conversationId },
1480                     null, null, null);
1481             return cursor.getCount() == 1;
1482         } finally {
1483             if (cursor != null) {
1484                 cursor.close();
1485             }
1486         }
1487     }
1488 
1489     /** Preserve parts in message but clear the stored draft */
1490     public static final int UPDATE_MODE_CLEAR_DRAFT = 1;
1491     /** Add the message as a draft */
1492     public static final int UPDATE_MODE_ADD_DRAFT = 2;
1493 
1494     /**
1495      * Update draft message for specified conversation
1496      * @param dbWrapper       local database (wrapped)
1497      * @param conversationId  conversation to update
1498      * @param message         Optional message to preserve attachments for (either as draft or for
1499      *                        sending)
1500      * @param updateMode      either {@link #UPDATE_MODE_CLEAR_DRAFT} or
1501      *                        {@link #UPDATE_MODE_ADD_DRAFT}
1502      * @return message id of newly written draft (else null)
1503      */
1504     @DoesNotRunOnMainThread
updateDraftMessageData(final DatabaseWrapper dbWrapper, final String conversationId, @Nullable final MessageData message, final int updateMode)1505     public static String updateDraftMessageData(final DatabaseWrapper dbWrapper,
1506             final String conversationId, @Nullable final MessageData message,
1507             final int updateMode) {
1508         Assert.isNotMainThread();
1509         Assert.notNull(conversationId);
1510         Assert.inRange(updateMode, UPDATE_MODE_CLEAR_DRAFT, UPDATE_MODE_ADD_DRAFT);
1511         String messageId = null;
1512         Cursor cursor = null;
1513         dbWrapper.beginTransaction();
1514         try {
1515             // Find all draft parts for the current conversation
1516             final SimpleArrayMap<Uri, MessagePartData> currentDraftParts = new SimpleArrayMap<>();
1517             cursor = dbWrapper.query(DatabaseHelper.DRAFT_PARTS_VIEW,
1518                     MessagePartData.getProjection(),
1519                     MessageColumns.CONVERSATION_ID + " =?",
1520                     new String[] { conversationId }, null, null, null);
1521             while (cursor.moveToNext()) {
1522                 final MessagePartData part = MessagePartData.createFromCursor(cursor);
1523                 if (part.isAttachment()) {
1524                     currentDraftParts.put(part.getContentUri(), part);
1525                 }
1526             }
1527             // Optionally, preserve attachments for "message"
1528             final boolean conversationExists = getConversationExists(dbWrapper, conversationId);
1529             if (message != null && conversationExists) {
1530                 for (final MessagePartData part : message.getParts()) {
1531                     if (part.isAttachment()) {
1532                         currentDraftParts.remove(part.getContentUri());
1533                     }
1534                 }
1535             }
1536 
1537             // Delete orphan content
1538             for (int index = 0; index < currentDraftParts.size(); index++) {
1539                 final MessagePartData part = currentDraftParts.valueAt(index);
1540                 part.destroySync();
1541             }
1542 
1543             // Delete existing draft (cascade deletes parts)
1544             dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
1545                     MessageColumns.STATUS + "=? AND " + MessageColumns.CONVERSATION_ID + "=?",
1546                     new String[] {
1547                         Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_DRAFT),
1548                         conversationId
1549                     });
1550 
1551             // Write new draft
1552             if (updateMode == UPDATE_MODE_ADD_DRAFT && message != null
1553                     && message.hasContent() && conversationExists) {
1554                 Assert.equals(MessageData.BUGLE_STATUS_OUTGOING_DRAFT,
1555                         message.getStatus());
1556 
1557                 // Now add draft to message table
1558                 insertNewMessageInTransaction(dbWrapper, message);
1559                 messageId = message.getMessageId();
1560             }
1561 
1562             if (conversationExists) {
1563                 updateConversationDraftSnippetAndPreviewInTransaction(
1564                         dbWrapper, conversationId, message);
1565 
1566                 if (message != null && message.getSelfId() != null) {
1567                     updateConversationSelfIdInTransaction(dbWrapper, conversationId,
1568                             message.getSelfId());
1569                 }
1570             }
1571 
1572             dbWrapper.setTransactionSuccessful();
1573         } finally {
1574             dbWrapper.endTransaction();
1575             if (cursor != null) {
1576                 cursor.close();
1577             }
1578         }
1579         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
1580             LogUtil.v(TAG,
1581                     "Updated draft message " + messageId + " for conversation " + conversationId);
1582         }
1583         return messageId;
1584     }
1585 
1586     /**
1587      * Read the first draft message associated with this conversation.
1588      * If none present create an empty (sms) draft message.
1589      */
1590     @DoesNotRunOnMainThread
readDraftMessageData(final DatabaseWrapper dbWrapper, final String conversationId, final String conversationSelfId)1591     public static MessageData readDraftMessageData(final DatabaseWrapper dbWrapper,
1592             final String conversationId, final String conversationSelfId) {
1593         Assert.isNotMainThread();
1594         MessageData message = null;
1595         Cursor cursor = null;
1596         dbWrapper.beginTransaction();
1597         try {
1598             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
1599                     MessageData.getProjection(),
1600                     MessageColumns.STATUS + "=? AND " + MessageColumns.CONVERSATION_ID + "=?",
1601                     new String[] {
1602                         Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_DRAFT),
1603                         conversationId
1604                     }, null, null, null);
1605             Assert.inRange(cursor.getCount(), 0, 1);
1606             if (cursor.moveToFirst()) {
1607                 message = new MessageData();
1608                 message.bindDraft(cursor, conversationSelfId);
1609                 readMessagePartsData(dbWrapper, message, true);
1610                 // Disconnect draft parts from DB
1611                 for (final MessagePartData part : message.getParts()) {
1612                     part.updatePartId(null);
1613                     part.updateMessageId(null);
1614                 }
1615                 message.updateMessageId(null);
1616             }
1617             dbWrapper.setTransactionSuccessful();
1618         } finally {
1619             dbWrapper.endTransaction();
1620             if (cursor != null) {
1621                 cursor.close();
1622             }
1623         }
1624         return message;
1625     }
1626 
1627     // Internal
addParticipantToConversation(final DatabaseWrapper dbWrapper, final ParticipantData participant, final String conversationId)1628     private static void addParticipantToConversation(final DatabaseWrapper dbWrapper,
1629             final ParticipantData participant, final String conversationId) {
1630         final String participantId = getOrCreateParticipantInTransaction(dbWrapper, participant);
1631         Assert.notNull(participantId);
1632 
1633         // Add the participant to the conversation participants table
1634         final ContentValues values = new ContentValues();
1635         values.put(ConversationParticipantsColumns.CONVERSATION_ID, conversationId);
1636         values.put(ConversationParticipantsColumns.PARTICIPANT_ID, participantId);
1637         dbWrapper.insert(DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE, null, values);
1638     }
1639 
1640     /**
1641      * Get string used as canonical recipient for participant cache for sub id
1642      */
getCanonicalRecipientFromSubId(final int subId)1643     private static String getCanonicalRecipientFromSubId(final int subId) {
1644         return "SELF(" + subId + ")";
1645     }
1646 
1647     /**
1648      * Maps from a sub id or phone number to a participant id if there is one.
1649      *
1650      * @return If the participant is available in our cache, or the DB, this returns the
1651      * participant id for the given subid/phone number.  Otherwise it returns null.
1652      */
1653     @VisibleForTesting
getParticipantId(final DatabaseWrapper dbWrapper, final int subId, final String canonicalRecipient)1654     private static String getParticipantId(final DatabaseWrapper dbWrapper,
1655             final int subId, final String canonicalRecipient) {
1656         // First check our memory cache for the participant Id
1657         String participantId;
1658         synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
1659             participantId = sNormalizedPhoneNumberToParticipantIdCache.get(canonicalRecipient);
1660         }
1661 
1662         if (participantId != null) {
1663             return participantId;
1664         }
1665 
1666         // This code will only be executed for incremental additions.
1667         Cursor cursor = null;
1668         try {
1669             if (subId != ParticipantData.OTHER_THAN_SELF_SUB_ID) {
1670                 // Now look for an existing participant in the db with this sub id.
1671                 cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
1672                         new String[] {ParticipantColumns._ID},
1673                         ParticipantColumns.SUB_ID + "=?",
1674                         new String[] { Integer.toString(subId) }, null, null, null);
1675             } else {
1676                 // Look for existing participant with this normalized phone number and no subId.
1677                 cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
1678                         new String[] {ParticipantColumns._ID},
1679                         ParticipantColumns.NORMALIZED_DESTINATION + "=? AND "
1680                                 + ParticipantColumns.SUB_ID + "=?",
1681                                 new String[] {canonicalRecipient, Integer.toString(subId)},
1682                                 null, null, null);
1683             }
1684 
1685             if (cursor.moveToFirst()) {
1686                 // TODO Is this assert correct for multi-sim where a new sim was put in?
1687                 Assert.isTrue(cursor.getCount() == 1);
1688 
1689                 // We found an existing participant in the database
1690                 participantId = cursor.getString(0);
1691 
1692                 synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
1693                     // Add it to the cache for next time
1694                     sNormalizedPhoneNumberToParticipantIdCache.put(canonicalRecipient,
1695                             participantId);
1696                 }
1697             }
1698         } finally {
1699             if (cursor != null) {
1700                 cursor.close();
1701             }
1702         }
1703         return participantId;
1704     }
1705 
1706     @DoesNotRunOnMainThread
getOrCreateSelf(final DatabaseWrapper dbWrapper, final int subId)1707     public static ParticipantData getOrCreateSelf(final DatabaseWrapper dbWrapper,
1708             final int subId) {
1709         Assert.isNotMainThread();
1710         ParticipantData participant = null;
1711         dbWrapper.beginTransaction();
1712         try {
1713             final ParticipantData shell = ParticipantData.getSelfParticipant(subId);
1714             final String participantId = getOrCreateParticipantInTransaction(dbWrapper, shell);
1715             participant = getExistingParticipant(dbWrapper, participantId);
1716             dbWrapper.setTransactionSuccessful();
1717         } finally {
1718             dbWrapper.endTransaction();
1719         }
1720         return participant;
1721     }
1722 
1723     /**
1724      * Lookup and if necessary create a new participant
1725      * @param dbWrapper      Database wrapper
1726      * @param participant    Participant to find/create
1727      * @return participantId ParticipantId for existing or newly created participant
1728      */
1729     @DoesNotRunOnMainThread
getOrCreateParticipantInTransaction(final DatabaseWrapper dbWrapper, final ParticipantData participant)1730     public static String getOrCreateParticipantInTransaction(final DatabaseWrapper dbWrapper,
1731             final ParticipantData participant) {
1732         Assert.isNotMainThread();
1733         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1734         int subId = ParticipantData.OTHER_THAN_SELF_SUB_ID;
1735         String participantId = null;
1736         String canonicalRecipient = null;
1737         if (participant.isSelf()) {
1738             subId = participant.getSubId();
1739             canonicalRecipient = getCanonicalRecipientFromSubId(subId);
1740         } else {
1741             canonicalRecipient = participant.getNormalizedDestination();
1742         }
1743         Assert.notNull(canonicalRecipient);
1744         participantId = getParticipantId(dbWrapper, subId, canonicalRecipient);
1745 
1746         if (participantId != null) {
1747             return participantId;
1748         }
1749 
1750         if (!participant.isContactIdResolved()) {
1751             // Refresh participant's name and avatar with matching contact in CP2.
1752             ParticipantRefresh.refreshParticipant(dbWrapper, participant);
1753         }
1754 
1755         // Insert the participant into the participants table
1756         final ContentValues values = participant.toContentValues();
1757         final long participantRow = dbWrapper.insert(DatabaseHelper.PARTICIPANTS_TABLE, null,
1758                 values);
1759         participantId = Long.toString(participantRow);
1760         Assert.notNull(canonicalRecipient);
1761 
1762         synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
1763             // Now that we've inserted it, add it to our cache
1764             sNormalizedPhoneNumberToParticipantIdCache.put(canonicalRecipient, participantId);
1765         }
1766 
1767         return participantId;
1768     }
1769 
1770     @DoesNotRunOnMainThread
updateDestination(final DatabaseWrapper dbWrapper, final String destination, final boolean blocked)1771     public static void updateDestination(final DatabaseWrapper dbWrapper,
1772             final String destination, final boolean blocked) {
1773         Assert.isNotMainThread();
1774         final ContentValues values = new ContentValues();
1775         values.put(ParticipantColumns.BLOCKED, blocked ? 1 : 0);
1776         dbWrapper.update(DatabaseHelper.PARTICIPANTS_TABLE, values,
1777                 ParticipantColumns.NORMALIZED_DESTINATION + "=? AND " +
1778                         ParticipantColumns.SUB_ID + "=?",
1779                 new String[] { destination, Integer.toString(
1780                         ParticipantData.OTHER_THAN_SELF_SUB_ID) });
1781     }
1782 
1783     @DoesNotRunOnMainThread
getConversationFromOtherParticipantDestination( final DatabaseWrapper db, final String otherDestination)1784     public static String getConversationFromOtherParticipantDestination(
1785             final DatabaseWrapper db, final String otherDestination) {
1786         Assert.isNotMainThread();
1787         Cursor cursor = null;
1788         try {
1789             cursor = db.query(DatabaseHelper.CONVERSATIONS_TABLE,
1790                     new String[] { ConversationColumns._ID },
1791                     ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION + "=?",
1792                     new String[] { otherDestination }, null, null, null);
1793             Assert.inRange(cursor.getCount(), 0, 1);
1794             if (cursor.moveToFirst()) {
1795                 return cursor.getString(0);
1796             }
1797         } finally {
1798             if (cursor != null) {
1799                 cursor.close();
1800             }
1801         }
1802         return null;
1803     }
1804 
1805 
1806     /**
1807      * Get a list of conversations that contain any of participants specified.
1808      */
getConversationsForParticipants( final ArrayList<String> participantIds)1809     private static HashSet<String> getConversationsForParticipants(
1810             final ArrayList<String> participantIds) {
1811         final DatabaseWrapper db = DataModel.get().getDatabase();
1812         final HashSet<String> conversationIds = new HashSet<String>();
1813 
1814         final String selection = ConversationParticipantsColumns.PARTICIPANT_ID + "=?";
1815         for (final String participantId : participantIds) {
1816             final String[] selectionArgs = new String[] { participantId };
1817             final Cursor cursor = db.query(DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE,
1818                     ConversationParticipantsQuery.PROJECTION,
1819                     selection, selectionArgs, null, null, null);
1820 
1821             if (cursor != null) {
1822                 try {
1823                     while (cursor.moveToNext()) {
1824                         final String conversationId = cursor.getString(
1825                                 ConversationParticipantsQuery.INDEX_CONVERSATION_ID);
1826                         conversationIds.add(conversationId);
1827                     }
1828                 } finally {
1829                     cursor.close();
1830                 }
1831             }
1832         }
1833 
1834         return conversationIds;
1835     }
1836 
1837     /**
1838      * Refresh conversation names/avatars based on a list of participants that are changed.
1839      */
1840     @DoesNotRunOnMainThread
refreshConversationsForParticipants(final ArrayList<String> participants)1841     public static void refreshConversationsForParticipants(final ArrayList<String> participants) {
1842         Assert.isNotMainThread();
1843         final HashSet<String> conversationIds = getConversationsForParticipants(participants);
1844         if (conversationIds.size() > 0) {
1845             for (final String conversationId : conversationIds) {
1846                 refreshConversation(conversationId);
1847             }
1848 
1849             MessagingContentProvider.notifyConversationListChanged();
1850             if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
1851                 LogUtil.v(TAG, "Number of conversations refreshed:" + conversationIds.size());
1852             }
1853         }
1854     }
1855 
1856     /**
1857      * Refresh conversation names/avatars based on a changed participant.
1858      */
1859     @DoesNotRunOnMainThread
refreshConversationsForParticipant(final String participantId)1860     public static void refreshConversationsForParticipant(final String participantId) {
1861         Assert.isNotMainThread();
1862         final ArrayList<String> participantList = new ArrayList<String>(1);
1863         participantList.add(participantId);
1864         refreshConversationsForParticipants(participantList);
1865     }
1866 
1867     /**
1868      * Refresh one conversation.
1869      */
refreshConversation(final String conversationId)1870     private static void refreshConversation(final String conversationId) {
1871         final DatabaseWrapper db = DataModel.get().getDatabase();
1872 
1873         db.beginTransaction();
1874         try {
1875             BugleDatabaseOperations.updateConversationNameAndAvatarInTransaction(db,
1876                     conversationId);
1877             db.setTransactionSuccessful();
1878         } finally {
1879             db.endTransaction();
1880         }
1881 
1882         MessagingContentProvider.notifyParticipantsChanged(conversationId);
1883         MessagingContentProvider.notifyMessagesChanged(conversationId);
1884         MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
1885     }
1886 
1887     @DoesNotRunOnMainThread
updateRowIfExists(final DatabaseWrapper db, final String table, final String rowKey, final String rowId, final ContentValues values)1888     public static boolean updateRowIfExists(final DatabaseWrapper db, final String table,
1889             final String rowKey, final String rowId, final ContentValues values) {
1890         Assert.isNotMainThread();
1891         final StringBuilder sb = new StringBuilder();
1892         final ArrayList<String> whereValues = new ArrayList<String>(values.size() + 1);
1893         whereValues.add(rowId);
1894 
1895         for (final String key : values.keySet()) {
1896             if (sb.length() > 0) {
1897                 sb.append(" OR ");
1898             }
1899             final Object value = values.get(key);
1900             sb.append(key);
1901             if (value != null) {
1902                 sb.append(" IS NOT ?");
1903                 whereValues.add(value.toString());
1904             } else {
1905                 sb.append(" IS NOT NULL");
1906             }
1907         }
1908 
1909         final String whereClause = rowKey + "=?" + " AND (" + sb.toString() + ")";
1910         final String [] whereValuesArray = whereValues.toArray(new String[whereValues.size()]);
1911         final int count = db.update(table, values, whereClause, whereValuesArray);
1912         if (count > 1) {
1913             LogUtil.w(LogUtil.BUGLE_TAG, "Updated more than 1 row " + count + "; " + table +
1914                     " for " + rowKey + " = " + rowId + " (deleted?)");
1915         }
1916         Assert.inRange(count, 0, 1);
1917         return (count >= 0);
1918     }
1919 }
1920