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 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 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 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 894 public static void clearParticipantIdCache() { 895 Assert.isNotMainThread(); 896 synchronized (sNormalizedPhoneNumberToParticipantIdCache) { 897 sNormalizedPhoneNumberToParticipantIdCache.clear(); 898 } 899 } 900 901 @DoesNotRunOnMainThread 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 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 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 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 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 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 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 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 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 */ 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 */ 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 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 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 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 */ 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 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 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 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 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 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 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 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 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 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 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 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 */ 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 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 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 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 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 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 */ 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 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 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 */ 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 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