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