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 */ 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 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 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 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 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 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 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 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 318 public static boolean isBlockedDestination(final DatabaseWrapper db, final String destination) { 319 Assert.isNotMainThread(); 320 return isBlockedParticipant(db, destination, ParticipantColumns.NORMALIZED_DESTINATION); 321 } 322 323 static boolean isBlockedParticipant(final DatabaseWrapper db, final String participantId) { 324 return isBlockedParticipant(db, participantId, ParticipantColumns._ID); 325 } 326 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 */ 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 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 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 */ 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 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 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 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 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 */ 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 */ 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 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 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 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 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 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 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 */ 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 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 */ 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 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 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 899 public static void clearParticipantIdCache() { 900 Assert.isNotMainThread(); 901 synchronized (sNormalizedPhoneNumberToParticipantIdCache) { 902 sNormalizedPhoneNumberToParticipantIdCache.clear(); 903 } 904 } 905 906 @DoesNotRunOnMainThread 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 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 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 968 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 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 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 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 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 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 */ 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 */ 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 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 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 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 */ 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 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 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 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 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 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 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 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 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 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 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 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 */ 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 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 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 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 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 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 */ 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 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 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 */ 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 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