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