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.ContentValues; 20 import android.database.ContentObserver; 21 import android.database.Cursor; 22 import android.database.DatabaseUtils; 23 import android.graphics.Color; 24 import android.provider.ContactsContract.CommonDataKinds.Phone; 25 import androidx.collection.ArrayMap; 26 import android.telephony.SubscriptionInfo; 27 import android.text.TextUtils; 28 29 import com.android.messaging.Factory; 30 import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns; 31 import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns; 32 import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns; 33 import com.android.messaging.datamodel.data.ParticipantData; 34 import com.android.messaging.datamodel.data.ParticipantData.ParticipantsQuery; 35 import com.android.messaging.ui.UIIntents; 36 import com.android.messaging.util.Assert; 37 import com.android.messaging.util.ContactUtil; 38 import com.android.messaging.util.LogUtil; 39 import com.android.messaging.util.OsUtil; 40 import com.android.messaging.util.PhoneUtils; 41 import com.android.messaging.util.SafeAsyncTask; 42 import com.google.common.annotations.VisibleForTesting; 43 import com.google.common.base.Joiner; 44 45 import java.util.ArrayList; 46 import java.util.HashSet; 47 import java.util.List; 48 import java.util.Locale; 49 import java.util.Set; 50 import java.util.concurrent.atomic.AtomicBoolean; 51 52 /** 53 * Utility class for refreshing participant information based on matching contact. This updates 54 * 1. name, photo_uri, matching contact_id of participants. 55 * 2. generated_name of conversations. 56 * 57 * There are two kinds of participant refreshes, 58 * 1. Full refresh, this is triggered at application start or activity resumes after contact 59 * change is detected. 60 * 2. Partial refresh, this is triggered when a participant is added to a conversation. This 61 * normally happens during SMS sync. 62 */ 63 @VisibleForTesting 64 public class ParticipantRefresh { 65 private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG; 66 67 /** 68 * Refresh all participants including ones that were resolved before. 69 */ 70 public static final int REFRESH_MODE_FULL = 0; 71 72 /** 73 * Refresh all unresolved participants. 74 */ 75 public static final int REFRESH_MODE_INCREMENTAL = 1; 76 77 /** 78 * Force refresh all self participants. 79 */ 80 public static final int REFRESH_MODE_SELF_ONLY = 2; 81 82 public static class ConversationParticipantsQuery { 83 public static final String[] PROJECTION = new String[] { 84 ConversationParticipantsColumns._ID, 85 ConversationParticipantsColumns.CONVERSATION_ID, 86 ConversationParticipantsColumns.PARTICIPANT_ID 87 }; 88 89 public static final int INDEX_ID = 0; 90 public static final int INDEX_CONVERSATION_ID = 1; 91 public static final int INDEX_PARTICIPANT_ID = 2; 92 } 93 94 // Track whether observer is initialized or not. 95 private static volatile boolean sObserverInitialized = false; 96 private static final Object sLock = new Object(); 97 private static final AtomicBoolean sFullRefreshScheduled = new AtomicBoolean(false); 98 private static final Runnable sFullRefreshRunnable = new Runnable() { 99 @Override 100 public void run() { 101 final boolean oldScheduled = sFullRefreshScheduled.getAndSet(false); 102 Assert.isTrue(oldScheduled); 103 refreshParticipants(REFRESH_MODE_FULL); 104 } 105 }; 106 private static final Runnable sSelfOnlyRefreshRunnable = new Runnable() { 107 @Override 108 public void run() { 109 refreshParticipants(REFRESH_MODE_SELF_ONLY); 110 } 111 }; 112 113 /** 114 * A customized content resolver to track contact changes. 115 */ 116 public static class ContactContentObserver extends ContentObserver { 117 private volatile boolean mContactChanged = false; 118 ContactContentObserver()119 public ContactContentObserver() { 120 super(null); 121 } 122 123 @Override onChange(final boolean selfChange)124 public void onChange(final boolean selfChange) { 125 super.onChange(selfChange); 126 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 127 LogUtil.v(TAG, "Contacts changed"); 128 } 129 mContactChanged = true; 130 } 131 getContactChanged()132 public boolean getContactChanged() { 133 return mContactChanged; 134 } 135 resetContactChanged()136 public void resetContactChanged() { 137 mContactChanged = false; 138 } 139 initialize()140 public void initialize() { 141 // TODO: Handle enterprise contacts post M once contacts provider supports it 142 Factory.get().getApplicationContext().getContentResolver().registerContentObserver( 143 Phone.CONTENT_URI, true, this); 144 mContactChanged = true; // Force a full refresh on initialization. 145 } 146 } 147 148 /** 149 * Refresh participants only if needed, i.e., application start or contact changed. 150 */ refreshParticipantsIfNeeded()151 public static void refreshParticipantsIfNeeded() { 152 if (ParticipantRefresh.getNeedFullRefresh() && 153 sFullRefreshScheduled.compareAndSet(false, true)) { 154 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 155 LogUtil.v(TAG, "Started full participant refresh"); 156 } 157 SafeAsyncTask.executeOnThreadPool(sFullRefreshRunnable); 158 } else if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 159 LogUtil.v(TAG, "Skipped full participant refresh"); 160 } 161 } 162 163 /** 164 * Refresh self participants on subscription or settings change. 165 */ refreshSelfParticipants()166 public static void refreshSelfParticipants() { 167 SafeAsyncTask.executeOnThreadPool(sSelfOnlyRefreshRunnable); 168 } 169 getNeedFullRefresh()170 private static boolean getNeedFullRefresh() { 171 final ContactContentObserver observer = Factory.get().getContactContentObserver(); 172 if (observer == null) { 173 // If there is no observer (for unittest cases), we don't need to refresh participants. 174 return false; 175 } 176 177 if (!sObserverInitialized) { 178 synchronized (sLock) { 179 if (!sObserverInitialized) { 180 observer.initialize(); 181 sObserverInitialized = true; 182 } 183 } 184 } 185 186 return observer.getContactChanged(); 187 } 188 resetNeedFullRefresh()189 private static void resetNeedFullRefresh() { 190 final ContactContentObserver observer = Factory.get().getContactContentObserver(); 191 if (observer != null) { 192 observer.resetContactChanged(); 193 } 194 } 195 196 /** 197 * This class is totally static. Make constructor to be private so that an instance 198 * of this class would not be created by by mistake. 199 */ ParticipantRefresh()200 private ParticipantRefresh() { 201 } 202 203 /** 204 * Refresh participants in Bugle. 205 * 206 * @param refreshMode the refresh mode desired. See {@link #REFRESH_MODE_FULL}, 207 * {@link #REFRESH_MODE_INCREMENTAL}, and {@link #REFRESH_MODE_SELF_ONLY} 208 */ 209 @VisibleForTesting refreshParticipants(final int refreshMode)210 static void refreshParticipants(final int refreshMode) { 211 Assert.inRange(refreshMode, REFRESH_MODE_FULL, REFRESH_MODE_SELF_ONLY); 212 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 213 switch (refreshMode) { 214 case REFRESH_MODE_FULL: 215 LogUtil.v(TAG, "Start full participant refresh"); 216 break; 217 case REFRESH_MODE_INCREMENTAL: 218 LogUtil.v(TAG, "Start partial participant refresh"); 219 break; 220 case REFRESH_MODE_SELF_ONLY: 221 LogUtil.v(TAG, "Start self participant refresh"); 222 break; 223 } 224 } 225 226 if (!ContactUtil.hasReadContactsPermission() || !OsUtil.hasPhonePermission()) { 227 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 228 LogUtil.v(TAG, "Skipping participant referesh because of permissions"); 229 } 230 return; 231 } 232 233 if (refreshMode == REFRESH_MODE_FULL) { 234 // resetNeedFullRefresh right away so that we will skip duplicated full refresh 235 // requests. 236 resetNeedFullRefresh(); 237 } 238 239 if (refreshMode == REFRESH_MODE_FULL || refreshMode == REFRESH_MODE_SELF_ONLY) { 240 refreshSelfParticipantList(); 241 } 242 243 final ArrayList<String> changedParticipants = new ArrayList<String>(); 244 245 String selection = null; 246 String[] selectionArgs = null; 247 248 if (refreshMode == REFRESH_MODE_INCREMENTAL) { 249 // In case of incremental refresh, filter out participants that are already resolved. 250 selection = ParticipantColumns.CONTACT_ID + "=?"; 251 selectionArgs = new String[] { 252 String.valueOf(ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED) }; 253 } else if (refreshMode == REFRESH_MODE_SELF_ONLY) { 254 // In case of self-only refresh, filter out non-self participants. 255 selection = SELF_PARTICIPANTS_CLAUSE; 256 selectionArgs = null; 257 } 258 259 final DatabaseWrapper db = DataModel.get().getDatabase(); 260 Cursor cursor = null; 261 boolean selfUpdated = false; 262 try { 263 cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE, 264 ParticipantsQuery.PROJECTION, selection, selectionArgs, null, null, null); 265 266 if (cursor != null) { 267 while (cursor.moveToNext()) { 268 try { 269 final ParticipantData participantData = 270 ParticipantData.getFromCursor(cursor); 271 if (refreshParticipant(db, participantData)) { 272 if (participantData.isSelf()) { 273 selfUpdated = true; 274 } 275 updateParticipant(db, participantData); 276 final String id = participantData.getId(); 277 changedParticipants.add(id); 278 } 279 } catch (final Exception exception) { 280 // Failure to update one participant shouldn't cancel the entire refresh. 281 // Log the failure so we know what's going on and resume the loop. 282 LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "ParticipantRefresh: Failed to " + 283 "update participant", exception); 284 } 285 } 286 } 287 } finally { 288 if (cursor != null) { 289 cursor.close(); 290 } 291 } 292 293 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 294 LogUtil.v(TAG, "Number of participants refreshed:" + changedParticipants.size()); 295 } 296 297 // Refresh conversations for participants that are changed. 298 if (changedParticipants.size() > 0) { 299 BugleDatabaseOperations.refreshConversationsForParticipants(changedParticipants); 300 } 301 if (selfUpdated) { 302 // Boom 303 MessagingContentProvider.notifyAllParticipantsChanged(); 304 MessagingContentProvider.notifyAllMessagesChanged(); 305 } 306 } 307 308 private static final String SELF_PARTICIPANTS_CLAUSE = ParticipantColumns.SUB_ID 309 + " NOT IN ( " 310 + ParticipantData.OTHER_THAN_SELF_SUB_ID 311 + " )"; 312 getExistingSubIds()313 private static final Set<Integer> getExistingSubIds() { 314 final DatabaseWrapper db = DataModel.get().getDatabase(); 315 final HashSet<Integer> existingSubIds = new HashSet<Integer>(); 316 317 Cursor cursor = null; 318 try { 319 cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE, 320 ParticipantsQuery.PROJECTION, 321 SELF_PARTICIPANTS_CLAUSE, null, null, null, null); 322 323 if (cursor != null) { 324 while (cursor.moveToNext()) { 325 final int subId = cursor.getInt(ParticipantsQuery.INDEX_SUB_ID); 326 existingSubIds.add(subId); 327 } 328 } 329 } finally { 330 if (cursor != null) { 331 cursor.close(); 332 } 333 } 334 return existingSubIds; 335 } 336 337 private static final String UPDATE_SELF_PARTICIPANT_SUBSCRIPTION_SQL = 338 "UPDATE " + DatabaseHelper.PARTICIPANTS_TABLE + " SET " 339 + ParticipantColumns.SIM_SLOT_ID + " = %d, " 340 + ParticipantColumns.SUBSCRIPTION_COLOR + " = %d, " 341 + ParticipantColumns.SUBSCRIPTION_NAME + " = %s " 342 + " WHERE %s"; 343 getUpdateSelfParticipantSubscriptionInfoSql(final int slotId, final int subscriptionColor, final String subscriptionName, final String where)344 static String getUpdateSelfParticipantSubscriptionInfoSql(final int slotId, 345 final int subscriptionColor, final String subscriptionName, final String where) { 346 return String.format((Locale) null /* construct SQL string without localization */, 347 UPDATE_SELF_PARTICIPANT_SUBSCRIPTION_SQL, 348 slotId, subscriptionColor, subscriptionName, where); 349 } 350 351 /** 352 * Ensure that there is a self participant corresponding to every active SIM. Also, ensure 353 * that any other older SIM self participants are marked as inactive. 354 */ refreshSelfParticipantList()355 private static void refreshSelfParticipantList() { 356 if (!OsUtil.isAtLeastL_MR1()) { 357 return; 358 } 359 360 final DatabaseWrapper db = DataModel.get().getDatabase(); 361 362 final List<SubscriptionInfo> subInfoRecords = 363 PhoneUtils.getDefault().toLMr1().getActiveSubscriptionInfoList(); 364 final ArrayMap<Integer, SubscriptionInfo> activeSubscriptionIdToRecordMap = 365 new ArrayMap<Integer, SubscriptionInfo>(); 366 db.beginTransaction(); 367 final Set<Integer> existingSubIds = getExistingSubIds(); 368 369 try { 370 if (subInfoRecords != null) { 371 for (final SubscriptionInfo subInfoRecord : subInfoRecords) { 372 final int subId = subInfoRecord.getSubscriptionId(); 373 // If its a new subscription, add it to the database. 374 if (!existingSubIds.contains(subId)) { 375 db.execSQL(DatabaseHelper.getCreateSelfParticipantSql(subId)); 376 // Add it to the local set to guard against duplicated entries returned 377 // by subscription manager. 378 existingSubIds.add(subId); 379 } 380 activeSubscriptionIdToRecordMap.put(subId, subInfoRecord); 381 382 if (subId == PhoneUtils.getDefault().getDefaultSmsSubscriptionId()) { 383 // This is the system default subscription, so update the default self. 384 activeSubscriptionIdToRecordMap.put(ParticipantData.DEFAULT_SELF_SUB_ID, 385 subInfoRecord); 386 } 387 } 388 } 389 390 // For subscriptions already in the database, refresh ParticipantColumns.SIM_SLOT_ID. 391 for (final Integer subId : activeSubscriptionIdToRecordMap.keySet()) { 392 final SubscriptionInfo record = activeSubscriptionIdToRecordMap.get(subId); 393 final String displayName = 394 DatabaseUtils.sqlEscapeString(record.getDisplayName().toString()); 395 db.execSQL(getUpdateSelfParticipantSubscriptionInfoSql(record.getSimSlotIndex(), 396 record.getIconTint(), displayName, 397 ParticipantColumns.SUB_ID + " = " + subId)); 398 } 399 db.execSQL(getUpdateSelfParticipantSubscriptionInfoSql( 400 ParticipantData.INVALID_SLOT_ID, Color.TRANSPARENT, "''", 401 ParticipantColumns.SUB_ID + " NOT IN (" + 402 Joiner.on(", ").join(activeSubscriptionIdToRecordMap.keySet()) + ")")); 403 db.setTransactionSuccessful(); 404 } finally { 405 db.endTransaction(); 406 } 407 // Fix up conversation self ids by reverting to default self for conversations whose self 408 // ids are no longer active. 409 refreshConversationSelfIds(); 410 } 411 412 /** 413 * Refresh one participant. 414 * @return true if the ParticipantData was changed 415 */ refreshParticipant(final DatabaseWrapper db, final ParticipantData participantData)416 public static boolean refreshParticipant(final DatabaseWrapper db, 417 final ParticipantData participantData) { 418 boolean updated = false; 419 420 if (participantData.isSelf()) { 421 final int selfChange = refreshFromSelfProfile(db, participantData); 422 423 if (selfChange == SELF_PROFILE_EXISTS) { 424 // If a self-profile exists, it takes precedence over Contacts data. So we are done. 425 return true; 426 } 427 428 updated = (selfChange == SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED); 429 430 // Fall-through and try to update based on Contacts data 431 } 432 433 updated |= refreshFromContacts(db, participantData); 434 return updated; 435 } 436 437 private static final int SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED = 1; 438 private static final int SELF_PROFILE_EXISTS = 2; 439 refreshFromSelfProfile(final DatabaseWrapper db, final ParticipantData participantData)440 private static int refreshFromSelfProfile(final DatabaseWrapper db, 441 final ParticipantData participantData) { 442 int changed = 0; 443 // Refresh the phone number based on information from telephony 444 if (participantData.updatePhoneNumberForSelfIfChanged()) { 445 changed = SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED; 446 } 447 448 if (OsUtil.isAtLeastL_MR1()) { 449 // Refresh the subscription info based on information from SubscriptionManager. 450 final SubscriptionInfo subscriptionInfo = 451 PhoneUtils.get(participantData.getSubId()).toLMr1().getActiveSubscriptionInfo(); 452 if (participantData.updateSubscriptionInfoForSelfIfChanged(subscriptionInfo)) { 453 changed = SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED; 454 } 455 } 456 457 // For self participant, try getting name/avatar from self profile in CP2 first. 458 // TODO: in case of multi-sim, profile would not be able to be used for 459 // different numbers. Need to figure out that. 460 Cursor selfCursor = null; 461 try { 462 selfCursor = ContactUtil.getSelf(db.getContext()).performSynchronousQuery(); 463 if (selfCursor != null && selfCursor.getCount() > 0) { 464 selfCursor.moveToNext(); 465 final long selfContactId = selfCursor.getLong(ContactUtil.INDEX_CONTACT_ID); 466 participantData.setContactId(selfContactId); 467 participantData.setFullName(selfCursor.getString( 468 ContactUtil.INDEX_DISPLAY_NAME)); 469 participantData.setFirstName( 470 ContactUtil.lookupFirstName(db.getContext(), selfContactId)); 471 participantData.setProfilePhotoUri(selfCursor.getString( 472 ContactUtil.INDEX_PHOTO_URI)); 473 participantData.setLookupKey(selfCursor.getString( 474 ContactUtil.INDEX_SELF_QUERY_LOOKUP_KEY)); 475 return SELF_PROFILE_EXISTS; 476 } 477 } catch (final Exception exception) { 478 // It's possible for contact query to fail and we don't want that to crash our app. 479 // However, we need to at least log the exception so we know something was wrong. 480 LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "Participant refresh: failed to refresh " + 481 "participant. exception=" + exception); 482 } finally { 483 if (selfCursor != null) { 484 selfCursor.close(); 485 } 486 } 487 return changed; 488 } 489 refreshFromContacts(final DatabaseWrapper db, final ParticipantData participantData)490 private static boolean refreshFromContacts(final DatabaseWrapper db, 491 final ParticipantData participantData) { 492 final String normalizedDestination = participantData.getNormalizedDestination(); 493 final long currentContactId = participantData.getContactId(); 494 final String currentDisplayName = participantData.getFullName(); 495 final String currentFirstName = participantData.getFirstName(); 496 final String currentPhotoUri = participantData.getProfilePhotoUri(); 497 final String currentContactDestination = participantData.getContactDestination(); 498 499 Cursor matchingContactCursor = null; 500 long matchingContactId = -1; 501 String matchingDisplayName = null; 502 String matchingFirstName = null; 503 String matchingPhotoUri = null; 504 String matchingLookupKey = null; 505 String matchingDestination = null; 506 boolean updated = false; 507 508 if (TextUtils.isEmpty(normalizedDestination)) { 509 // The normalized destination can be "" for the self id if we can't get it from the 510 // SIM. Some contact providers throw an IllegalArgumentException if you lookup "", 511 // so we early out. 512 return false; 513 } 514 515 try { 516 matchingContactCursor = ContactUtil.lookupDestination(db.getContext(), 517 normalizedDestination).performSynchronousQuery(); 518 if (matchingContactCursor == null || matchingContactCursor.getCount() == 0) { 519 // If there is no match, mark the participant as contact not found. 520 if (currentContactId != ParticipantData.PARTICIPANT_CONTACT_ID_NOT_FOUND) { 521 participantData.setContactId(ParticipantData.PARTICIPANT_CONTACT_ID_NOT_FOUND); 522 participantData.setFullName(null); 523 participantData.setFirstName(null); 524 participantData.setProfilePhotoUri(null); 525 participantData.setLookupKey(null); 526 updated = true; 527 } 528 return updated; 529 } 530 531 while (matchingContactCursor.moveToNext()) { 532 final long contactId = matchingContactCursor.getLong(ContactUtil.INDEX_CONTACT_ID); 533 // Pick either the first contact or the contact with same id as previous matched 534 // contact id. 535 if (matchingContactId == -1 || currentContactId == contactId) { 536 matchingContactId = contactId; 537 matchingDisplayName = matchingContactCursor.getString( 538 ContactUtil.INDEX_DISPLAY_NAME); 539 matchingFirstName = ContactUtil.lookupFirstName(db.getContext(), contactId); 540 matchingPhotoUri = matchingContactCursor.getString( 541 ContactUtil.INDEX_PHOTO_URI); 542 matchingLookupKey = matchingContactCursor.getString( 543 ContactUtil.INDEX_LOOKUP_KEY); 544 matchingDestination = matchingContactCursor.getString( 545 ContactUtil.INDEX_PHONE_EMAIL); 546 } 547 548 // There is no need to try other contacts if the current contactId was not filled... 549 if (currentContactId < 0 550 // or we found the matching contact id 551 || currentContactId == contactId) { 552 break; 553 } 554 } 555 } catch (final Exception exception) { 556 // It's possible for contact query to fail and we don't want that to crash our app. 557 // However, we need to at least log the exception so we know something was wrong. 558 LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "Participant refresh: failed to refresh " + 559 "participant. exception=" + exception); 560 return false; 561 } finally { 562 if (matchingContactCursor != null) { 563 matchingContactCursor.close(); 564 } 565 } 566 567 // Update participant only if something changed. 568 final boolean isContactIdChanged = (matchingContactId != currentContactId); 569 final boolean isDisplayNameChanged = 570 !TextUtils.equals(matchingDisplayName, currentDisplayName); 571 final boolean isFirstNameChanged = !TextUtils.equals(matchingFirstName, currentFirstName); 572 final boolean isPhotoUrlChanged = !TextUtils.equals(matchingPhotoUri, currentPhotoUri); 573 final boolean isDestinationChanged = !TextUtils.equals(matchingDestination, 574 currentContactDestination); 575 576 if (isContactIdChanged || isDisplayNameChanged || isFirstNameChanged || isPhotoUrlChanged 577 || isDestinationChanged) { 578 participantData.setContactId(matchingContactId); 579 participantData.setFullName(matchingDisplayName); 580 participantData.setFirstName(matchingFirstName); 581 participantData.setProfilePhotoUri(matchingPhotoUri); 582 participantData.setLookupKey(matchingLookupKey); 583 participantData.setContactDestination(matchingDestination); 584 if (isDestinationChanged) { 585 // Update the send destination to the new one entered by user in Contacts. 586 participantData.setSendDestination(matchingDestination); 587 } 588 updated = true; 589 } 590 591 return updated; 592 } 593 594 /** 595 * Update participant with matching contact's contactId, displayName and photoUri. 596 */ updateParticipant(final DatabaseWrapper db, final ParticipantData participantData)597 private static void updateParticipant(final DatabaseWrapper db, 598 final ParticipantData participantData) { 599 final ContentValues values = new ContentValues(); 600 if (participantData.isSelf()) { 601 // Self participants can refresh their normalized phone numbers 602 values.put(ParticipantColumns.NORMALIZED_DESTINATION, 603 participantData.getNormalizedDestination()); 604 values.put(ParticipantColumns.DISPLAY_DESTINATION, 605 participantData.getDisplayDestination()); 606 } 607 values.put(ParticipantColumns.CONTACT_ID, participantData.getContactId()); 608 values.put(ParticipantColumns.LOOKUP_KEY, participantData.getLookupKey()); 609 values.put(ParticipantColumns.FULL_NAME, participantData.getFullName()); 610 values.put(ParticipantColumns.FIRST_NAME, participantData.getFirstName()); 611 values.put(ParticipantColumns.PROFILE_PHOTO_URI, participantData.getProfilePhotoUri()); 612 values.put(ParticipantColumns.CONTACT_DESTINATION, participantData.getContactDestination()); 613 values.put(ParticipantColumns.SEND_DESTINATION, participantData.getSendDestination()); 614 615 db.beginTransaction(); 616 try { 617 db.update(DatabaseHelper.PARTICIPANTS_TABLE, values, ParticipantColumns._ID + "=?", 618 new String[] { participantData.getId() }); 619 db.setTransactionSuccessful(); 620 } finally { 621 db.endTransaction(); 622 } 623 } 624 625 /** 626 * Get a list of inactive self ids in the participants table. 627 */ getInactiveSelfParticipantIds()628 private static List<String> getInactiveSelfParticipantIds() { 629 final DatabaseWrapper db = DataModel.get().getDatabase(); 630 final List<String> inactiveSelf = new ArrayList<String>(); 631 632 final String selection = ParticipantColumns.SIM_SLOT_ID + "=? AND " + 633 SELF_PARTICIPANTS_CLAUSE; 634 Cursor cursor = null; 635 try { 636 cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE, 637 new String[] { ParticipantColumns._ID }, 638 selection, new String[] { String.valueOf(ParticipantData.INVALID_SLOT_ID) }, 639 null, null, null); 640 641 if (cursor != null) { 642 while (cursor.moveToNext()) { 643 final String participantId = cursor.getString(0); 644 inactiveSelf.add(participantId); 645 } 646 } 647 } finally { 648 if (cursor != null) { 649 cursor.close(); 650 } 651 } 652 653 return inactiveSelf; 654 } 655 656 /** 657 * Gets a list of conversations with the given self ids. 658 */ getConversationsWithSelfParticipantIds(final List<String> selfIds)659 private static List<String> getConversationsWithSelfParticipantIds(final List<String> selfIds) { 660 final DatabaseWrapper db = DataModel.get().getDatabase(); 661 final List<String> conversationIds = new ArrayList<String>(); 662 663 Cursor cursor = null; 664 try { 665 final StringBuilder selectionList = new StringBuilder(); 666 for (int i = 0; i < selfIds.size(); i++) { 667 selectionList.append('?'); 668 if (i < selfIds.size() - 1) { 669 selectionList.append(','); 670 } 671 } 672 final String selection = 673 ConversationColumns.CURRENT_SELF_ID + " IN (" + selectionList + ")"; 674 cursor = db.query(DatabaseHelper.CONVERSATIONS_TABLE, 675 new String[] { ConversationColumns._ID }, 676 selection, selfIds.toArray(new String[0]), 677 null, null, null); 678 679 if (cursor != null) { 680 while (cursor.moveToNext()) { 681 final String conversationId = cursor.getString(0); 682 conversationIds.add(conversationId); 683 } 684 } 685 } finally { 686 if (cursor != null) { 687 cursor.close(); 688 } 689 } 690 return conversationIds; 691 } 692 693 /** 694 * Refresh one conversation's self id. 695 */ updateConversationSelfId(final String conversationId, final String selfId)696 private static void updateConversationSelfId(final String conversationId, 697 final String selfId) { 698 final DatabaseWrapper db = DataModel.get().getDatabase(); 699 700 db.beginTransaction(); 701 try { 702 BugleDatabaseOperations.updateConversationSelfIdInTransaction(db, conversationId, 703 selfId); 704 db.setTransactionSuccessful(); 705 } finally { 706 db.endTransaction(); 707 } 708 709 MessagingContentProvider.notifyMessagesChanged(conversationId); 710 MessagingContentProvider.notifyConversationMetadataChanged(conversationId); 711 UIIntents.get().broadcastConversationSelfIdChange(db.getContext(), conversationId, selfId); 712 } 713 714 /** 715 * After refreshing the self participant list, find all conversations with inactive self ids, 716 * and switch them back to system default. 717 */ refreshConversationSelfIds()718 private static void refreshConversationSelfIds() { 719 final List<String> inactiveSelfs = getInactiveSelfParticipantIds(); 720 if (inactiveSelfs.size() == 0) { 721 return; 722 } 723 final List<String> conversationsToRefresh = 724 getConversationsWithSelfParticipantIds(inactiveSelfs); 725 if (conversationsToRefresh.size() == 0) { 726 return; 727 } 728 final DatabaseWrapper db = DataModel.get().getDatabase(); 729 final ParticipantData defaultSelf = 730 BugleDatabaseOperations.getOrCreateSelf(db, ParticipantData.DEFAULT_SELF_SUB_ID); 731 732 if (defaultSelf != null) { 733 for (final String conversationId : conversationsToRefresh) { 734 updateConversationSelfId(conversationId, defaultSelf.getId()); 735 } 736 } 737 } 738 } 739