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