/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.messaging.datamodel;

import android.content.ContentValues;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.graphics.Color;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import androidx.collection.ArrayMap;
import android.telephony.SubscriptionInfo;
import android.text.TextUtils;

import com.android.messaging.Factory;
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns;
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.datamodel.data.ParticipantData.ParticipantsQuery;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ContactUtil;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.SafeAsyncTask;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Utility class for refreshing participant information based on matching contact. This updates
 *     1. name, photo_uri, matching contact_id of participants.
 *     2. generated_name of conversations.
 *
 * There are two kinds of participant refreshes,
 *     1. Full refresh, this is triggered at application start or activity resumes after contact
 *        change is detected.
 *     2. Partial refresh, this is triggered when a participant is added to a conversation. This
 *        normally happens during SMS sync.
 */
@VisibleForTesting
public class ParticipantRefresh {
    private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;

    /**
     * Refresh all participants including ones that were resolved before.
     */
    public static final int REFRESH_MODE_FULL = 0;

    /**
     * Refresh all unresolved participants.
     */
    public static final int REFRESH_MODE_INCREMENTAL = 1;

    /**
     * Force refresh all self participants.
     */
    public static final int REFRESH_MODE_SELF_ONLY = 2;

    public static class ConversationParticipantsQuery {
        public static final String[] PROJECTION = new String[] {
            ConversationParticipantsColumns._ID,
            ConversationParticipantsColumns.CONVERSATION_ID,
            ConversationParticipantsColumns.PARTICIPANT_ID
        };

        public static final int INDEX_ID                        = 0;
        public static final int INDEX_CONVERSATION_ID           = 1;
        public static final int INDEX_PARTICIPANT_ID            = 2;
    }

    // Track whether observer is initialized or not.
    private static volatile boolean sObserverInitialized = false;
    private static final Object sLock = new Object();
    private static final AtomicBoolean sFullRefreshScheduled = new AtomicBoolean(false);
    private static final Runnable sFullRefreshRunnable = new Runnable() {
        @Override
        public void run() {
            final boolean oldScheduled = sFullRefreshScheduled.getAndSet(false);
            Assert.isTrue(oldScheduled);
            refreshParticipants(REFRESH_MODE_FULL);
        }
    };
    private static final Runnable sSelfOnlyRefreshRunnable = new Runnable() {
        @Override
        public void run() {
            refreshParticipants(REFRESH_MODE_SELF_ONLY);
        }
    };

    /**
     * A customized content resolver to track contact changes.
     */
    public static class ContactContentObserver extends ContentObserver {
        private volatile boolean mContactChanged = false;

        public ContactContentObserver() {
            super(null);
        }

        @Override
        public void onChange(final boolean selfChange) {
            super.onChange(selfChange);
            if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
                LogUtil.v(TAG, "Contacts changed");
            }
            mContactChanged = true;
        }

        public boolean getContactChanged() {
            return mContactChanged;
        }

        public void resetContactChanged() {
            mContactChanged = false;
        }

        public void initialize() {
            // TODO: Handle enterprise contacts post M once contacts provider supports it
            Factory.get().getApplicationContext().getContentResolver().registerContentObserver(
                    Phone.CONTENT_URI, true, this);
            mContactChanged = true; // Force a full refresh on initialization.
        }
    }

    /**
     * Refresh participants only if needed, i.e., application start or contact changed.
     */
    public static void refreshParticipantsIfNeeded() {
        if (ParticipantRefresh.getNeedFullRefresh() &&
                sFullRefreshScheduled.compareAndSet(false, true)) {
            if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
                LogUtil.v(TAG, "Started full participant refresh");
            }
            SafeAsyncTask.executeOnThreadPool(sFullRefreshRunnable);
        } else if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
            LogUtil.v(TAG, "Skipped full participant refresh");
        }
    }

    /**
     * Refresh self participants on subscription or settings change.
     */
    public static void refreshSelfParticipants() {
        SafeAsyncTask.executeOnThreadPool(sSelfOnlyRefreshRunnable);
    }

    private static boolean getNeedFullRefresh() {
        final ContactContentObserver observer = Factory.get().getContactContentObserver();
        if (observer == null) {
            // If there is no observer (for unittest cases), we don't need to refresh participants.
            return false;
        }

        if (!sObserverInitialized) {
            synchronized (sLock) {
                if (!sObserverInitialized) {
                    observer.initialize();
                    sObserverInitialized = true;
                }
            }
        }

        return observer.getContactChanged();
    }

    private static void resetNeedFullRefresh() {
        final ContactContentObserver observer = Factory.get().getContactContentObserver();
        if (observer != null) {
            observer.resetContactChanged();
        }
    }

    /**
     * This class is totally static. Make constructor to be private so that an instance
     * of this class would not be created by by mistake.
     */
    private ParticipantRefresh() {
    }

    /**
     * Refresh participants in Bugle.
     *
     * @param refreshMode the refresh mode desired. See {@link #REFRESH_MODE_FULL},
     *        {@link #REFRESH_MODE_INCREMENTAL}, and {@link #REFRESH_MODE_SELF_ONLY}
     */
     @VisibleForTesting
     static void refreshParticipants(final int refreshMode) {
        Assert.inRange(refreshMode, REFRESH_MODE_FULL, REFRESH_MODE_SELF_ONLY);
        if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
            switch (refreshMode) {
                case REFRESH_MODE_FULL:
                    LogUtil.v(TAG, "Start full participant refresh");
                    break;
                case REFRESH_MODE_INCREMENTAL:
                    LogUtil.v(TAG, "Start partial participant refresh");
                    break;
                case REFRESH_MODE_SELF_ONLY:
                    LogUtil.v(TAG, "Start self participant refresh");
                    break;
            }
        }

        if (!ContactUtil.hasReadContactsPermission() || !OsUtil.hasPhonePermission()) {
            if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
                LogUtil.v(TAG, "Skipping participant referesh because of permissions");
            }
            return;
        }

        if (refreshMode == REFRESH_MODE_FULL) {
            // resetNeedFullRefresh right away so that we will skip duplicated full refresh
            // requests.
            resetNeedFullRefresh();
        }

        if (refreshMode == REFRESH_MODE_FULL || refreshMode == REFRESH_MODE_SELF_ONLY) {
            refreshSelfParticipantList();
        }

        final ArrayList<String> changedParticipants = new ArrayList<String>();

        String selection = null;
        String[] selectionArgs = null;

        if (refreshMode == REFRESH_MODE_INCREMENTAL) {
            // In case of incremental refresh, filter out participants that are already resolved.
            selection = ParticipantColumns.CONTACT_ID + "=?";
            selectionArgs = new String[] {
                    String.valueOf(ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED) };
        } else if (refreshMode == REFRESH_MODE_SELF_ONLY) {
            // In case of self-only refresh, filter out non-self participants.
            selection = SELF_PARTICIPANTS_CLAUSE;
            selectionArgs = null;
        }

        final DatabaseWrapper db = DataModel.get().getDatabase();
        Cursor cursor = null;
        boolean selfUpdated = false;
        try {
            cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE,
                    ParticipantsQuery.PROJECTION, selection, selectionArgs, null, null, null);

            if (cursor != null) {
                while (cursor.moveToNext()) {
                    try {
                        final ParticipantData participantData =
                                ParticipantData.getFromCursor(cursor);
                        if (refreshParticipant(db, participantData)) {
                            if (participantData.isSelf()) {
                                selfUpdated = true;
                            }
                            updateParticipant(db, participantData);
                            final String id = participantData.getId();
                            changedParticipants.add(id);
                        }
                    } catch (final Exception exception) {
                        // Failure to update one participant shouldn't cancel the entire refresh.
                        // Log the failure so we know what's going on and resume the loop.
                        LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "ParticipantRefresh: Failed to " +
                                "update participant", exception);
                    }
                }
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
            LogUtil.v(TAG, "Number of participants refreshed:" + changedParticipants.size());
        }

        // Refresh conversations for participants that are changed.
        if (changedParticipants.size() > 0) {
            BugleDatabaseOperations.refreshConversationsForParticipants(changedParticipants);
        }
        if (selfUpdated) {
            // Boom
            MessagingContentProvider.notifyAllParticipantsChanged();
            MessagingContentProvider.notifyAllMessagesChanged();
        }
    }

    private static final String SELF_PARTICIPANTS_CLAUSE = ParticipantColumns.SUB_ID
            + " NOT IN ( "
            + ParticipantData.OTHER_THAN_SELF_SUB_ID
            + " )";

    private static final Set<Integer> getExistingSubIds() {
        final DatabaseWrapper db = DataModel.get().getDatabase();
        final HashSet<Integer> existingSubIds = new HashSet<Integer>();

        Cursor cursor = null;
        try {
            cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE,
                    ParticipantsQuery.PROJECTION,
                    SELF_PARTICIPANTS_CLAUSE, null, null, null, null);

            if (cursor != null) {
                while (cursor.moveToNext()) {
                    final int subId = cursor.getInt(ParticipantsQuery.INDEX_SUB_ID);
                    existingSubIds.add(subId);
                }
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return existingSubIds;
    }

    private static final String UPDATE_SELF_PARTICIPANT_SUBSCRIPTION_SQL =
            "UPDATE " + DatabaseHelper.PARTICIPANTS_TABLE + " SET "
            +  ParticipantColumns.SIM_SLOT_ID + " = %d, "
            +  ParticipantColumns.SUBSCRIPTION_COLOR + " = %d, "
            +  ParticipantColumns.SUBSCRIPTION_NAME + " = %s "
            + " WHERE %s";

    static String getUpdateSelfParticipantSubscriptionInfoSql(final int slotId,
            final int subscriptionColor, final String subscriptionName, final String where) {
        return String.format((Locale) null /* construct SQL string without localization */,
                UPDATE_SELF_PARTICIPANT_SUBSCRIPTION_SQL,
                slotId, subscriptionColor, subscriptionName, where);
    }

    /**
     * Ensure that there is a self participant corresponding to every active SIM. Also, ensure
     * that any other older SIM self participants are marked as inactive.
     */
    private static void refreshSelfParticipantList() {
        if (!OsUtil.isAtLeastL_MR1()) {
            return;
        }

        final DatabaseWrapper db = DataModel.get().getDatabase();

        final List<SubscriptionInfo> subInfoRecords =
                PhoneUtils.getDefault().toLMr1().getActiveSubscriptionInfoList();
        final ArrayMap<Integer, SubscriptionInfo> activeSubscriptionIdToRecordMap =
                new ArrayMap<Integer, SubscriptionInfo>();
        db.beginTransaction();
        final Set<Integer> existingSubIds = getExistingSubIds();

        try {
            if (subInfoRecords != null) {
                for (final SubscriptionInfo subInfoRecord : subInfoRecords) {
                    final int subId = subInfoRecord.getSubscriptionId();
                    // If its a new subscription, add it to the database.
                    if (!existingSubIds.contains(subId)) {
                        db.execSQL(DatabaseHelper.getCreateSelfParticipantSql(subId));
                        // Add it to the local set to guard against duplicated entries returned
                        // by subscription manager.
                        existingSubIds.add(subId);
                    }
                    activeSubscriptionIdToRecordMap.put(subId, subInfoRecord);

                    if (subId == PhoneUtils.getDefault().getDefaultSmsSubscriptionId()) {
                        // This is the system default subscription, so update the default self.
                        activeSubscriptionIdToRecordMap.put(ParticipantData.DEFAULT_SELF_SUB_ID,
                                subInfoRecord);
                    }
                }
            }

            // For subscriptions already in the database, refresh ParticipantColumns.SIM_SLOT_ID.
            for (final Integer subId : activeSubscriptionIdToRecordMap.keySet()) {
                final SubscriptionInfo record = activeSubscriptionIdToRecordMap.get(subId);
                final String displayName =
                        DatabaseUtils.sqlEscapeString(record.getDisplayName().toString());
                db.execSQL(getUpdateSelfParticipantSubscriptionInfoSql(record.getSimSlotIndex(),
                        record.getIconTint(), displayName,
                        ParticipantColumns.SUB_ID + " = " + subId));
            }
            db.execSQL(getUpdateSelfParticipantSubscriptionInfoSql(
                    ParticipantData.INVALID_SLOT_ID, Color.TRANSPARENT, "''",
                    ParticipantColumns.SUB_ID + " NOT IN (" +
                    Joiner.on(", ").join(activeSubscriptionIdToRecordMap.keySet()) + ")"));
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
        // Fix up conversation self ids by reverting to default self for conversations whose self
        // ids are no longer active.
        refreshConversationSelfIds();
    }

    /**
     * Refresh one participant.
     * @return true if the ParticipantData was changed
     */
    public static boolean refreshParticipant(final DatabaseWrapper db,
            final ParticipantData participantData) {
        boolean updated = false;

        if (participantData.isSelf()) {
            final int selfChange = refreshFromSelfProfile(db, participantData);

            if (selfChange == SELF_PROFILE_EXISTS) {
                // If a self-profile exists, it takes precedence over Contacts data. So we are done.
                return true;
            }

            updated = (selfChange == SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED);

            // Fall-through and try to update based on Contacts data
        }

        updated |= refreshFromContacts(db, participantData);
        return updated;
    }

    private static final int SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED = 1;
    private static final int SELF_PROFILE_EXISTS = 2;

    private static int refreshFromSelfProfile(final DatabaseWrapper db,
            final ParticipantData participantData) {
        int changed = 0;
        // Refresh the phone number based on information from telephony
        if (participantData.updatePhoneNumberForSelfIfChanged()) {
            changed = SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED;
        }

        if (OsUtil.isAtLeastL_MR1()) {
            // Refresh the subscription info based on information from SubscriptionManager.
            final SubscriptionInfo subscriptionInfo =
                    PhoneUtils.get(participantData.getSubId()).toLMr1().getActiveSubscriptionInfo();
            if (participantData.updateSubscriptionInfoForSelfIfChanged(subscriptionInfo)) {
                changed = SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED;
            }
        }

        // For self participant, try getting name/avatar from self profile in CP2 first.
        // TODO: in case of multi-sim, profile would not be able to be used for
        // different numbers. Need to figure out that.
        Cursor selfCursor = null;
        try {
            selfCursor = ContactUtil.getSelf(db.getContext()).performSynchronousQuery();
            if (selfCursor != null && selfCursor.getCount() > 0) {
                selfCursor.moveToNext();
                final long selfContactId = selfCursor.getLong(ContactUtil.INDEX_CONTACT_ID);
                participantData.setContactId(selfContactId);
                participantData.setFullName(selfCursor.getString(
                        ContactUtil.INDEX_DISPLAY_NAME));
                participantData.setFirstName(
                        ContactUtil.lookupFirstName(db.getContext(), selfContactId));
                participantData.setProfilePhotoUri(selfCursor.getString(
                        ContactUtil.INDEX_PHOTO_URI));
                participantData.setLookupKey(selfCursor.getString(
                        ContactUtil.INDEX_SELF_QUERY_LOOKUP_KEY));
                return SELF_PROFILE_EXISTS;
            }
        } catch (final Exception exception) {
            // It's possible for contact query to fail and we don't want that to crash our app.
            // However, we need to at least log the exception so we know something was wrong.
            LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "Participant refresh: failed to refresh " +
                    "participant. exception=" + exception);
        } finally {
            if (selfCursor != null) {
                selfCursor.close();
            }
        }
        return changed;
    }

    private static boolean refreshFromContacts(final DatabaseWrapper db,
            final ParticipantData participantData) {
        final String normalizedDestination = participantData.getNormalizedDestination();
        final long currentContactId = participantData.getContactId();
        final String currentDisplayName = participantData.getFullName();
        final String currentFirstName = participantData.getFirstName();
        final String currentPhotoUri = participantData.getProfilePhotoUri();
        final String currentContactDestination = participantData.getContactDestination();

        Cursor matchingContactCursor = null;
        long matchingContactId = -1;
        String matchingDisplayName = null;
        String matchingFirstName = null;
        String matchingPhotoUri = null;
        String matchingLookupKey = null;
        String matchingDestination = null;
        boolean updated = false;

        if (TextUtils.isEmpty(normalizedDestination)) {
            // The normalized destination can be "" for the self id if we can't get it from the
            // SIM.  Some contact providers throw an IllegalArgumentException if you lookup "",
            // so we early out.
            return false;
        }

        try {
            matchingContactCursor = ContactUtil.lookupDestination(db.getContext(),
                    normalizedDestination).performSynchronousQuery();
            if (matchingContactCursor == null || matchingContactCursor.getCount() == 0) {
                // If there is no match, mark the participant as contact not found.
                if (currentContactId != ParticipantData.PARTICIPANT_CONTACT_ID_NOT_FOUND) {
                    participantData.setContactId(ParticipantData.PARTICIPANT_CONTACT_ID_NOT_FOUND);
                    participantData.setFullName(null);
                    participantData.setFirstName(null);
                    participantData.setProfilePhotoUri(null);
                    participantData.setLookupKey(null);
                    updated = true;
                }
                return updated;
            }

            while (matchingContactCursor.moveToNext()) {
                final long contactId = matchingContactCursor.getLong(ContactUtil.INDEX_CONTACT_ID);
                // Pick either the first contact or the contact with same id as previous matched
                // contact id.
                if (matchingContactId == -1 || currentContactId == contactId) {
                    matchingContactId = contactId;
                    matchingDisplayName = matchingContactCursor.getString(
                            ContactUtil.INDEX_DISPLAY_NAME);
                    matchingFirstName = ContactUtil.lookupFirstName(db.getContext(), contactId);
                    matchingPhotoUri = matchingContactCursor.getString(
                            ContactUtil.INDEX_PHOTO_URI);
                    matchingLookupKey = matchingContactCursor.getString(
                            ContactUtil.INDEX_LOOKUP_KEY);
                    matchingDestination = matchingContactCursor.getString(
                            ContactUtil.INDEX_PHONE_EMAIL);
                }

                // There is no need to try other contacts if the current contactId was not filled...
                if (currentContactId < 0
                        // or we found the matching contact id
                        || currentContactId == contactId) {
                    break;
                }
            }
        } catch (final Exception exception) {
            // It's possible for contact query to fail and we don't want that to crash our app.
            // However, we need to at least log the exception so we know something was wrong.
            LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "Participant refresh: failed to refresh " +
                    "participant. exception=" + exception);
            return false;
        } finally {
            if (matchingContactCursor != null) {
                matchingContactCursor.close();
            }
        }

        // Update participant only if something changed.
        final boolean isContactIdChanged = (matchingContactId != currentContactId);
        final boolean isDisplayNameChanged =
                !TextUtils.equals(matchingDisplayName, currentDisplayName);
        final boolean isFirstNameChanged = !TextUtils.equals(matchingFirstName, currentFirstName);
        final boolean isPhotoUrlChanged = !TextUtils.equals(matchingPhotoUri, currentPhotoUri);
        final boolean isDestinationChanged = !TextUtils.equals(matchingDestination,
                currentContactDestination);

        if (isContactIdChanged || isDisplayNameChanged || isFirstNameChanged || isPhotoUrlChanged
                || isDestinationChanged) {
            participantData.setContactId(matchingContactId);
            participantData.setFullName(matchingDisplayName);
            participantData.setFirstName(matchingFirstName);
            participantData.setProfilePhotoUri(matchingPhotoUri);
            participantData.setLookupKey(matchingLookupKey);
            participantData.setContactDestination(matchingDestination);
            if (isDestinationChanged) {
                // Update the send destination to the new one entered by user in Contacts.
                participantData.setSendDestination(matchingDestination);
            }
            updated = true;
        }

        return updated;
    }

    /**
     * Update participant with matching contact's contactId, displayName and photoUri.
     */
    private static void updateParticipant(final DatabaseWrapper db,
            final ParticipantData participantData) {
        final ContentValues values = new ContentValues();
        if (participantData.isSelf()) {
            // Self participants can refresh their normalized phone numbers
            values.put(ParticipantColumns.NORMALIZED_DESTINATION,
                    participantData.getNormalizedDestination());
            values.put(ParticipantColumns.DISPLAY_DESTINATION,
                    participantData.getDisplayDestination());
        }
        values.put(ParticipantColumns.CONTACT_ID, participantData.getContactId());
        values.put(ParticipantColumns.LOOKUP_KEY, participantData.getLookupKey());
        values.put(ParticipantColumns.FULL_NAME, participantData.getFullName());
        values.put(ParticipantColumns.FIRST_NAME, participantData.getFirstName());
        values.put(ParticipantColumns.PROFILE_PHOTO_URI, participantData.getProfilePhotoUri());
        values.put(ParticipantColumns.CONTACT_DESTINATION, participantData.getContactDestination());
        values.put(ParticipantColumns.SEND_DESTINATION, participantData.getSendDestination());

        db.beginTransaction();
        try {
            db.update(DatabaseHelper.PARTICIPANTS_TABLE, values, ParticipantColumns._ID + "=?",
                    new String[] { participantData.getId() });
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }

    /**
     * Get a list of inactive self ids in the participants table.
     */
    private static List<String> getInactiveSelfParticipantIds() {
        final DatabaseWrapper db = DataModel.get().getDatabase();
        final List<String> inactiveSelf = new ArrayList<String>();

        final String selection = ParticipantColumns.SIM_SLOT_ID + "=? AND " +
                SELF_PARTICIPANTS_CLAUSE;
        Cursor cursor = null;
        try {
            cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE,
                    new String[] { ParticipantColumns._ID },
                    selection, new String[] { String.valueOf(ParticipantData.INVALID_SLOT_ID) },
                    null, null, null);

            if (cursor != null) {
                while (cursor.moveToNext()) {
                    final String participantId = cursor.getString(0);
                    inactiveSelf.add(participantId);
                }
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        return inactiveSelf;
    }

    /**
     * Gets a list of conversations with the given self ids.
     */
    private static List<String> getConversationsWithSelfParticipantIds(final List<String> selfIds) {
        final DatabaseWrapper db = DataModel.get().getDatabase();
        final List<String> conversationIds = new ArrayList<String>();

        Cursor cursor = null;
        try {
            final StringBuilder selectionList = new StringBuilder();
            for (int i = 0; i < selfIds.size(); i++) {
                selectionList.append('?');
                if (i < selfIds.size() - 1) {
                    selectionList.append(',');
                }
            }
            final String selection =
                    ConversationColumns.CURRENT_SELF_ID + " IN (" + selectionList + ")";
            cursor = db.query(DatabaseHelper.CONVERSATIONS_TABLE,
                    new String[] { ConversationColumns._ID },
                    selection, selfIds.toArray(new String[0]),
                    null, null, null);

            if (cursor != null) {
                while (cursor.moveToNext()) {
                    final String conversationId = cursor.getString(0);
                    conversationIds.add(conversationId);
                }
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return conversationIds;
    }

    /**
     * Refresh one conversation's self id.
     */
    private static void updateConversationSelfId(final String conversationId,
            final String selfId) {
        final DatabaseWrapper db = DataModel.get().getDatabase();

        db.beginTransaction();
        try {
            BugleDatabaseOperations.updateConversationSelfIdInTransaction(db, conversationId,
                    selfId);
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }

        MessagingContentProvider.notifyMessagesChanged(conversationId);
        MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
        UIIntents.get().broadcastConversationSelfIdChange(db.getContext(), conversationId, selfId);
    }

    /**
     * After refreshing the self participant list, find all conversations with inactive self ids,
     * and switch them back to system default.
     */
    private static void refreshConversationSelfIds() {
        final List<String> inactiveSelfs = getInactiveSelfParticipantIds();
        if (inactiveSelfs.size() == 0) {
            return;
        }
        final List<String> conversationsToRefresh =
                getConversationsWithSelfParticipantIds(inactiveSelfs);
        if (conversationsToRefresh.size() == 0) {
            return;
        }
        final DatabaseWrapper db = DataModel.get().getDatabase();
        final ParticipantData defaultSelf =
                BugleDatabaseOperations.getOrCreateSelf(db, ParticipantData.DEFAULT_SELF_SUB_ID);

        if (defaultSelf != null) {
            for (final String conversationId : conversationsToRefresh) {
                updateConversationSelfId(conversationId, defaultSelf.getId());
            }
        }
    }
}
