Home | History | Annotate | Download | only in datamodel
      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 android.support.v4.util.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 
    119         public ContactContentObserver() {
    120             super(null);
    121         }
    122 
    123         @Override
    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 
    132         public boolean getContactChanged() {
    133             return mContactChanged;
    134         }
    135 
    136         public void resetContactChanged() {
    137             mContactChanged = false;
    138         }
    139 
    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      */
    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      */
    166     public static void refreshSelfParticipants() {
    167         SafeAsyncTask.executeOnThreadPool(sSelfOnlyRefreshRunnable);
    168     }
    169 
    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 
    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      */
    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
    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 
    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 
    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      */
    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      */
    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 
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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