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
17package com.android.messaging.datamodel;
18
19import android.content.ContentValues;
20import android.database.ContentObserver;
21import android.database.Cursor;
22import android.database.DatabaseUtils;
23import android.graphics.Color;
24import android.provider.ContactsContract.CommonDataKinds.Phone;
25import android.support.v4.util.ArrayMap;
26import android.telephony.SubscriptionInfo;
27import android.text.TextUtils;
28
29import com.android.messaging.Factory;
30import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
31import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns;
32import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
33import com.android.messaging.datamodel.data.ParticipantData;
34import com.android.messaging.datamodel.data.ParticipantData.ParticipantsQuery;
35import com.android.messaging.ui.UIIntents;
36import com.android.messaging.util.Assert;
37import com.android.messaging.util.ContactUtil;
38import com.android.messaging.util.LogUtil;
39import com.android.messaging.util.OsUtil;
40import com.android.messaging.util.PhoneUtils;
41import com.android.messaging.util.SafeAsyncTask;
42import com.google.common.annotations.VisibleForTesting;
43import com.google.common.base.Joiner;
44
45import java.util.ArrayList;
46import java.util.HashSet;
47import java.util.List;
48import java.util.Locale;
49import java.util.Set;
50import 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
64public 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