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.ContentResolver;
20import android.content.ContentValues;
21import android.database.Cursor;
22import android.database.sqlite.SQLiteDoneException;
23import android.database.sqlite.SQLiteStatement;
24import android.net.Uri;
25import android.os.ParcelFileDescriptor;
26import android.support.v4.util.ArrayMap;
27import android.support.v4.util.SimpleArrayMap;
28import android.text.TextUtils;
29
30import com.android.messaging.Factory;
31import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
32import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns;
33import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
34import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
35import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
36import com.android.messaging.datamodel.ParticipantRefresh.ConversationParticipantsQuery;
37import com.android.messaging.datamodel.data.ConversationListItemData;
38import com.android.messaging.datamodel.data.MessageData;
39import com.android.messaging.datamodel.data.MessagePartData;
40import com.android.messaging.datamodel.data.ParticipantData;
41import com.android.messaging.sms.MmsUtils;
42import com.android.messaging.ui.UIIntents;
43import com.android.messaging.util.Assert;
44import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
45import com.android.messaging.util.AvatarUriUtil;
46import com.android.messaging.util.ContentType;
47import com.android.messaging.util.LogUtil;
48import com.android.messaging.util.OsUtil;
49import com.android.messaging.util.PhoneUtils;
50import com.android.messaging.util.UriUtil;
51import com.android.messaging.widget.WidgetConversationProvider;
52import com.google.common.annotations.VisibleForTesting;
53
54import java.io.IOException;
55import java.util.ArrayList;
56import java.util.HashSet;
57import java.util.List;
58import javax.annotation.Nullable;
59
60
61/**
62 * This class manages updating our local database
63 */
64public class BugleDatabaseOperations {
65
66    private static final String TAG = LogUtil.BUGLE_DATABASE_TAG;
67
68    // Global cache of phone numbers -> participant id mapping since this call is expensive.
69    private static final ArrayMap<String, String> sNormalizedPhoneNumberToParticipantIdCache =
70            new ArrayMap<String, String>();
71
72    /**
73     * Convert list of recipient strings (email/phone number) into list of ConversationParticipants
74     *
75     * @param recipients The recipient list
76     * @param refSubId The subId used to normalize phone numbers in the recipients
77     */
78    static ArrayList<ParticipantData> getConversationParticipantsFromRecipients(
79            final List<String> recipients, final int refSubId) {
80        // Generate a list of partially formed participants
81        final ArrayList<ParticipantData> participants = new
82                ArrayList<ParticipantData>();
83
84        if (recipients != null) {
85            for (final String recipient : recipients) {
86                participants.add(ParticipantData.getFromRawPhoneBySimLocale(recipient, refSubId));
87            }
88        }
89        return participants;
90    }
91
92    /**
93     * Sanitize a given list of conversation participants by de-duping and stripping out self
94     * phone number in group conversation.
95     */
96    @DoesNotRunOnMainThread
97    public static void sanitizeConversationParticipants(final List<ParticipantData> participants) {
98        Assert.isNotMainThread();
99        if (participants.size() > 0) {
100            // First remove redundant phone numbers
101            final HashSet<String> recipients = new HashSet<String>();
102            for (int i = participants.size() - 1; i >= 0; i--) {
103                final String recipient = participants.get(i).getNormalizedDestination();
104                if (!recipients.contains(recipient)) {
105                    recipients.add(recipient);
106                } else {
107                    participants.remove(i);
108                }
109            }
110            if (participants.size() > 1) {
111                // Remove self phone number from group conversation.
112                final HashSet<String> selfNumbers =
113                        PhoneUtils.getDefault().getNormalizedSelfNumbers();
114                int removed = 0;
115                // Do this two-pass scan to avoid unnecessary memory allocation.
116                // Prescan to count the self numbers in the list
117                for (final ParticipantData p : participants) {
118                    if (selfNumbers.contains(p.getNormalizedDestination())) {
119                        removed++;
120                    }
121                }
122                // If all are self numbers, maybe that's what the user wants, just leave
123                // the participants as is. Otherwise, do another scan to remove self numbers.
124                if (removed < participants.size()) {
125                    for (int i = participants.size() - 1; i >= 0; i--) {
126                        final String recipient = participants.get(i).getNormalizedDestination();
127                        if (selfNumbers.contains(recipient)) {
128                            participants.remove(i);
129                        }
130                    }
131                }
132            }
133        }
134    }
135
136    /**
137     * Convert list of ConversationParticipants into recipient strings (email/phone number)
138     */
139    @DoesNotRunOnMainThread
140    public static ArrayList<String> getRecipientsFromConversationParticipants(
141            final List<ParticipantData> participants) {
142        Assert.isNotMainThread();
143        // First find the thread id for this list of participants.
144        final ArrayList<String> recipients = new ArrayList<String>();
145
146        for (final ParticipantData participant : participants) {
147            recipients.add(participant.getSendDestination());
148        }
149        return recipients;
150    }
151
152    /**
153     * Get or create a conversation based on the message's thread id
154     *
155     * NOTE: There are phones on which you can't get the recipients from the thread id for SMS
156     * until you have a message, so use getOrCreateConversationFromRecipient instead.
157     *
158     * TODO: Should this be in MMS/SMS code?
159     *
160     * @param db the database
161     * @param threadId The message's thread
162     * @param senderBlocked Flag whether sender of message is in blocked people list
163     * @param refSubId The reference subId for canonicalize phone numbers
164     * @return conversationId
165     */
166    @DoesNotRunOnMainThread
167    public static String getOrCreateConversationFromThreadId(final DatabaseWrapper db,
168            final long threadId, final boolean senderBlocked, final int refSubId) {
169        Assert.isNotMainThread();
170        final List<String> recipients = MmsUtils.getRecipientsByThread(threadId);
171        final ArrayList<ParticipantData> participants =
172                getConversationParticipantsFromRecipients(recipients, refSubId);
173
174        return getOrCreateConversation(db, threadId, senderBlocked, participants, false, false,
175                null);
176    }
177
178    /**
179     * Get or create a conversation based on provided recipient
180     *
181     * @param db the database
182     * @param threadId The message's thread
183     * @param senderBlocked Flag whether sender of message is in blocked people list
184     * @param recipient recipient for thread
185     * @return conversationId
186     */
187    @DoesNotRunOnMainThread
188    public static String getOrCreateConversationFromRecipient(final DatabaseWrapper db,
189            final long threadId, final boolean senderBlocked, final ParticipantData recipient) {
190        Assert.isNotMainThread();
191        final ArrayList<ParticipantData> recipients = new ArrayList<>(1);
192        recipients.add(recipient);
193        return getOrCreateConversation(db, threadId, senderBlocked, recipients, false, false, null);
194    }
195
196    /**
197     * Get or create a conversation based on provided participants
198     *
199     * @param db the database
200     * @param threadId The message's thread
201     * @param archived Flag whether the conversation should be created archived
202     * @param participants list of conversation participants
203     * @param noNotification If notification should be disabled
204     * @param noVibrate If vibrate on notification should be disabled
205     * @param soundUri If there is custom sound URI
206     * @return a conversation id
207     */
208    @DoesNotRunOnMainThread
209    public static String getOrCreateConversation(final DatabaseWrapper db, final long threadId,
210            final boolean archived, final ArrayList<ParticipantData> participants,
211            boolean noNotification, boolean noVibrate, String soundUri) {
212        Assert.isNotMainThread();
213
214        // Check to see if this conversation is already in out local db cache
215        String conversationId = BugleDatabaseOperations.getExistingConversation(db, threadId,
216                false);
217
218        if (conversationId == null) {
219            final String conversationName = ConversationListItemData.generateConversationName(
220                    participants);
221
222            // Create the conversation with the default self participant which always maps to
223            // the system default subscription.
224            final ParticipantData self = ParticipantData.getSelfParticipant(
225                    ParticipantData.DEFAULT_SELF_SUB_ID);
226
227            db.beginTransaction();
228            try {
229                // Look up the "self" participantId (creating if necessary)
230                final String selfId =
231                        BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
232                // Create a new conversation
233                conversationId = BugleDatabaseOperations.createConversationInTransaction(
234                        db, threadId, conversationName, selfId, participants, archived,
235                        noNotification, noVibrate, soundUri);
236                db.setTransactionSuccessful();
237            } finally {
238                db.endTransaction();
239            }
240        }
241
242        return conversationId;
243    }
244
245    /**
246     * Get a conversation from the local DB based on the message's thread id.
247     *
248     * @param dbWrapper     The database
249     * @param threadId      The message's thread in the SMS database
250     * @param senderBlocked Flag whether sender of message is in blocked people list
251     * @return The existing conversation id or null
252     */
253    @VisibleForTesting
254    @DoesNotRunOnMainThread
255    public static String getExistingConversation(final DatabaseWrapper dbWrapper,
256            final long threadId, final boolean senderBlocked) {
257        Assert.isNotMainThread();
258        String conversationId = null;
259
260        Cursor cursor = null;
261        try {
262            // Look for an existing conversation in the db with this thread id
263            cursor = dbWrapper.rawQuery("SELECT " + ConversationColumns._ID
264                            + " FROM " + DatabaseHelper.CONVERSATIONS_TABLE
265                            + " WHERE " + ConversationColumns.SMS_THREAD_ID + "=" + threadId,
266                    null);
267
268            if (cursor.moveToFirst()) {
269                Assert.isTrue(cursor.getCount() == 1);
270                conversationId = cursor.getString(0);
271            }
272        } finally {
273            if (cursor != null) {
274                cursor.close();
275            }
276        }
277
278        return conversationId;
279    }
280
281    /**
282     * Get the thread id for an existing conversation from the local DB.
283     *
284     * @param dbWrapper The database
285     * @param conversationId The conversation to look up thread for
286     * @return The thread id. Returns -1 if the conversation was not found or if it was found
287     * but the thread column was NULL.
288     */
289    @DoesNotRunOnMainThread
290    public static long getThreadId(final DatabaseWrapper dbWrapper, final String conversationId) {
291        Assert.isNotMainThread();
292        long threadId = -1;
293
294        Cursor cursor = null;
295        try {
296            cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
297                    new String[] { ConversationColumns.SMS_THREAD_ID },
298                    ConversationColumns._ID + " =?",
299                    new String[] { conversationId },
300                    null, null, null);
301
302            if (cursor.moveToFirst()) {
303                Assert.isTrue(cursor.getCount() == 1);
304                if (!cursor.isNull(0)) {
305                    threadId = cursor.getLong(0);
306                }
307            }
308        } finally {
309            if (cursor != null) {
310                cursor.close();
311            }
312        }
313
314        return threadId;
315    }
316
317    @DoesNotRunOnMainThread
318    public static boolean isBlockedDestination(final DatabaseWrapper db, final String destination) {
319        Assert.isNotMainThread();
320        return isBlockedParticipant(db, destination, ParticipantColumns.NORMALIZED_DESTINATION);
321    }
322
323    static boolean isBlockedParticipant(final DatabaseWrapper db, final String participantId) {
324        return isBlockedParticipant(db, participantId, ParticipantColumns._ID);
325    }
326
327    static boolean isBlockedParticipant(final DatabaseWrapper db, final String value,
328            final String column) {
329        Cursor cursor = null;
330        try {
331            cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE,
332                    new String[] { ParticipantColumns.BLOCKED },
333                    column + "=? AND " + ParticipantColumns.SUB_ID + "=?",
334                    new String[] { value,
335                    Integer.toString(ParticipantData.OTHER_THAN_SELF_SUB_ID) },
336                    null, null, null);
337
338            Assert.inRange(cursor.getCount(), 0, 1);
339            if (cursor.moveToFirst()) {
340                return cursor.getInt(0) == 1;
341            }
342        } finally {
343            if (cursor != null) {
344                cursor.close();
345            }
346        }
347        return false;  // if there's no row, it's not blocked :-)
348    }
349
350    /**
351     * Create a conversation in the local DB based on the message's thread id.
352     *
353     * It's up to the caller to make sure that this is all inside a transaction.  It will return
354     * null if it's not in the local DB.
355     *
356     * @param dbWrapper     The database
357     * @param threadId      The message's thread
358     * @param selfId        The selfId to make default for this conversation
359     * @param archived      Flag whether the conversation should be created archived
360     * @param noNotification If notification should be disabled
361     * @param noVibrate     If vibrate on notification should be disabled
362     * @param soundUri      The customized sound
363     * @return The existing conversation id or new conversation id
364     */
365    static String createConversationInTransaction(final DatabaseWrapper dbWrapper,
366            final long threadId, final String conversationName, final String selfId,
367            final List<ParticipantData> participants, final boolean archived,
368            boolean noNotification, boolean noVibrate, String soundUri) {
369        // We want conversation and participant creation to be atomic
370        Assert.isTrue(dbWrapper.getDatabase().inTransaction());
371        boolean hasEmailAddress = false;
372        for (final ParticipantData participant : participants) {
373            Assert.isTrue(!participant.isSelf());
374            if (participant.isEmail()) {
375                hasEmailAddress = true;
376            }
377        }
378
379        // TODO : Conversations state - normal vs. archived
380
381        // Insert a new local conversation for this thread id
382        final ContentValues values = new ContentValues();
383        values.put(ConversationColumns.SMS_THREAD_ID, threadId);
384        // Start with conversation hidden - sending a message or saving a draft will change that
385        values.put(ConversationColumns.SORT_TIMESTAMP, 0L);
386        values.put(ConversationColumns.CURRENT_SELF_ID, selfId);
387        values.put(ConversationColumns.PARTICIPANT_COUNT, participants.size());
388        values.put(ConversationColumns.INCLUDE_EMAIL_ADDRESS, (hasEmailAddress ? 1 : 0));
389        if (archived) {
390            values.put(ConversationColumns.ARCHIVE_STATUS, 1);
391        }
392        if (noNotification) {
393            values.put(ConversationColumns.NOTIFICATION_ENABLED, 0);
394        }
395        if (noVibrate) {
396            values.put(ConversationColumns.NOTIFICATION_VIBRATION, 0);
397        }
398        if (!TextUtils.isEmpty(soundUri)) {
399            values.put(ConversationColumns.NOTIFICATION_SOUND_URI, soundUri);
400        }
401
402        fillParticipantData(values, participants);
403
404        final long conversationRowId = dbWrapper.insert(DatabaseHelper.CONVERSATIONS_TABLE, null,
405                values);
406
407        Assert.isTrue(conversationRowId != -1);
408        if (conversationRowId == -1) {
409            LogUtil.e(TAG, "BugleDatabaseOperations : failed to insert conversation into table");
410            return null;
411        }
412
413        final String conversationId = Long.toString(conversationRowId);
414
415        // Make sure that participants are added for this conversation
416        for (final ParticipantData participant : participants) {
417            // TODO: Use blocking information
418            addParticipantToConversation(dbWrapper, participant, conversationId);
419        }
420
421        // Now fully resolved participants available can update conversation name / avatar.
422        // b/16437575: We cannot use the participants directly, but instead have to call
423        // getParticipantsForConversation() to retrieve the actual participants. This is needed
424        // because the call to addParticipantToConversation() won't fill up the ParticipantData
425        // if the participant already exists in the participant table. For example, say you have
426        // an existing conversation with John. Now if you create a new group conversation with
427        // Jeff & John with only their phone numbers, then when we try to add John's number to the
428        // group conversation, we see that he's already in the participant table, therefore we
429        // short-circuit any steps to actually fill out the ParticipantData for John other than
430        // just returning his participant id. Eventually, the ParticipantData we have is still the
431        // raw data with just the phone number. getParticipantsForConversation(), on the other
432        // hand, will fill out all the info for each participant from the participants table.
433        updateConversationNameAndAvatarInTransaction(dbWrapper, conversationId,
434                getParticipantsForConversation(dbWrapper, conversationId));
435
436        return conversationId;
437    }
438
439    private static void fillParticipantData(final ContentValues values,
440            final List<ParticipantData> participants) {
441        if (participants != null && !participants.isEmpty()) {
442            final Uri avatarUri = AvatarUriUtil.createAvatarUri(participants);
443            values.put(ConversationColumns.ICON, avatarUri.toString());
444
445            long contactId;
446            String lookupKey;
447            String destination;
448            if (participants.size() == 1) {
449                final ParticipantData firstParticipant = participants.get(0);
450                contactId = firstParticipant.getContactId();
451                lookupKey = firstParticipant.getLookupKey();
452                destination = firstParticipant.getNormalizedDestination();
453            } else {
454                contactId = 0;
455                lookupKey = null;
456                destination = null;
457            }
458
459            values.put(ConversationColumns.PARTICIPANT_CONTACT_ID, contactId);
460            values.put(ConversationColumns.PARTICIPANT_LOOKUP_KEY, lookupKey);
461            values.put(ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION, destination);
462        }
463    }
464
465    /**
466     * Delete conversation and associated messages/parts
467     */
468    @DoesNotRunOnMainThread
469    public static boolean deleteConversation(final DatabaseWrapper dbWrapper,
470            final String conversationId, final long cutoffTimestamp) {
471        Assert.isNotMainThread();
472        dbWrapper.beginTransaction();
473        boolean conversationDeleted = false;
474        boolean conversationMessagesDeleted = false;
475        try {
476            // Delete existing messages
477            if (cutoffTimestamp == Long.MAX_VALUE) {
478                // Delete parts and messages
479                dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
480                        MessageColumns.CONVERSATION_ID + "=?", new String[] { conversationId });
481                conversationMessagesDeleted = true;
482            } else {
483                // Delete all messages prior to the cutoff
484                dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
485                        MessageColumns.CONVERSATION_ID + "=? AND "
486                                + MessageColumns.RECEIVED_TIMESTAMP + "<=?",
487                                new String[] { conversationId, Long.toString(cutoffTimestamp) });
488
489                // Delete any draft message. The delete above may not always include the draft,
490                // because under certain scenarios (e.g. sending messages in progress), the draft
491                // timestamp can be larger than the cutoff time, which is generally the conversation
492                // sort timestamp. Because of how the sms/mms provider works on some newer
493                // devices, it's important that we never delete all the messages in a conversation
494                // without also deleting the conversation itself (see b/20262204 for details).
495                dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
496                        MessageColumns.STATUS + "=? AND " + MessageColumns.CONVERSATION_ID + "=?",
497                        new String[] {
498                            Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_DRAFT),
499                            conversationId
500                        });
501
502                // Check to see if there are any messages left in the conversation
503                final long count = dbWrapper.queryNumEntries(DatabaseHelper.MESSAGES_TABLE,
504                        MessageColumns.CONVERSATION_ID + "=?", new String[] { conversationId });
505                conversationMessagesDeleted = (count == 0);
506
507                // Log detail information if there are still messages left in the conversation
508                if (!conversationMessagesDeleted) {
509                    final long maxTimestamp =
510                            getConversationMaxTimestamp(dbWrapper, conversationId);
511                    LogUtil.w(TAG, "BugleDatabaseOperations:"
512                            + " cannot delete all messages in a conversation"
513                            + ", after deletion: count=" + count
514                            + ", max timestamp=" + maxTimestamp
515                            + ", cutoff timestamp=" + cutoffTimestamp);
516                }
517            }
518
519            if (conversationMessagesDeleted) {
520                // Delete conversation row
521                final int count = dbWrapper.delete(DatabaseHelper.CONVERSATIONS_TABLE,
522                        ConversationColumns._ID + "=?", new String[] { conversationId });
523                conversationDeleted = (count > 0);
524            }
525            dbWrapper.setTransactionSuccessful();
526        } finally {
527            dbWrapper.endTransaction();
528        }
529        return conversationDeleted;
530    }
531
532    private static final String MAX_RECEIVED_TIMESTAMP =
533            "MAX(" + MessageColumns.RECEIVED_TIMESTAMP + ")";
534    /**
535     * Get the max received timestamp of a conversation's messages
536     */
537    private static long getConversationMaxTimestamp(final DatabaseWrapper dbWrapper,
538            final String conversationId) {
539        final Cursor cursor = dbWrapper.query(
540                DatabaseHelper.MESSAGES_TABLE,
541                new String[]{ MAX_RECEIVED_TIMESTAMP },
542                MessageColumns.CONVERSATION_ID + "=?",
543                new String[]{ conversationId },
544                null, null, null);
545        if (cursor != null) {
546            try {
547                if (cursor.moveToFirst()) {
548                    return cursor.getLong(0);
549                }
550            } finally {
551                cursor.close();
552            }
553        }
554        return 0;
555    }
556
557    @DoesNotRunOnMainThread
558    public static void updateConversationMetadataInTransaction(final DatabaseWrapper dbWrapper,
559            final String conversationId, final String messageId, final long latestTimestamp,
560            final boolean keepArchived, final String smsServiceCenter,
561            final boolean shouldAutoSwitchSelfId) {
562        Assert.isNotMainThread();
563        Assert.isTrue(dbWrapper.getDatabase().inTransaction());
564
565        final ContentValues values = new ContentValues();
566        values.put(ConversationColumns.LATEST_MESSAGE_ID, messageId);
567        values.put(ConversationColumns.SORT_TIMESTAMP, latestTimestamp);
568        if (!TextUtils.isEmpty(smsServiceCenter)) {
569            values.put(ConversationColumns.SMS_SERVICE_CENTER, smsServiceCenter);
570        }
571
572        // When the conversation gets updated with new messages, unarchive the conversation unless
573        // the sender is blocked, or we have been told to keep it archived.
574        if (!keepArchived) {
575            values.put(ConversationColumns.ARCHIVE_STATUS, 0);
576        }
577
578        final MessageData message = readMessage(dbWrapper, messageId);
579        addSnippetTextAndPreviewToContentValues(message, false /* showDraft */, values);
580
581        if (shouldAutoSwitchSelfId) {
582            addSelfIdAutoSwitchInfoToContentValues(dbWrapper, message, conversationId, values);
583        }
584
585        // Conversation always exists as this method is called from ActionService only after
586        // reading and if necessary creating the conversation.
587        updateConversationRow(dbWrapper, conversationId, values);
588
589        if (shouldAutoSwitchSelfId && OsUtil.isAtLeastL_MR1()) {
590            // Normally, the draft message compose UI trusts its UI state for providing up-to-date
591            // conversation self id. Therefore, notify UI through local broadcast receiver about
592            // this external change so the change can be properly reflected.
593            UIIntents.get().broadcastConversationSelfIdChange(dbWrapper.getContext(),
594                    conversationId, getConversationSelfId(dbWrapper, conversationId));
595        }
596    }
597
598    @DoesNotRunOnMainThread
599    public static void updateConversationMetadataInTransaction(final DatabaseWrapper db,
600            final String conversationId, final String messageId, final long latestTimestamp,
601            final boolean keepArchived, final boolean shouldAutoSwitchSelfId) {
602        Assert.isNotMainThread();
603        updateConversationMetadataInTransaction(
604                db, conversationId, messageId, latestTimestamp, keepArchived, null,
605                shouldAutoSwitchSelfId);
606    }
607
608    @DoesNotRunOnMainThread
609    public static void updateConversationArchiveStatusInTransaction(final DatabaseWrapper dbWrapper,
610            final String conversationId, final boolean isArchived) {
611        Assert.isNotMainThread();
612        Assert.isTrue(dbWrapper.getDatabase().inTransaction());
613        final ContentValues values = new ContentValues();
614        values.put(ConversationColumns.ARCHIVE_STATUS, isArchived ? 1 : 0);
615        updateConversationRowIfExists(dbWrapper, conversationId, values);
616    }
617
618    static void addSnippetTextAndPreviewToContentValues(final MessageData message,
619            final boolean showDraft, final ContentValues values) {
620        values.put(ConversationColumns.SHOW_DRAFT, showDraft ? 1 : 0);
621        values.put(ConversationColumns.SNIPPET_TEXT, message.getMessageText());
622        values.put(ConversationColumns.SUBJECT_TEXT, message.getMmsSubject());
623
624        String type = null;
625        String uriString = null;
626        for (final MessagePartData part : message.getParts()) {
627            if (part.isAttachment() &&
628                    ContentType.isConversationListPreviewableType(part.getContentType())) {
629                uriString = part.getContentUri().toString();
630                type = part.getContentType();
631                break;
632            }
633        }
634        values.put(ConversationColumns.PREVIEW_CONTENT_TYPE, type);
635        values.put(ConversationColumns.PREVIEW_URI, uriString);
636    }
637
638    /**
639     * Adds self-id auto switch info for a conversation if the last message has a different
640     * subscription than the conversation's.
641     * @return true if self id will need to be changed, false otherwise.
642     */
643    static boolean addSelfIdAutoSwitchInfoToContentValues(final DatabaseWrapper dbWrapper,
644            final MessageData message, final String conversationId, final ContentValues values) {
645        // Only auto switch conversation self for incoming messages.
646        if (!OsUtil.isAtLeastL_MR1() || !message.getIsIncoming()) {
647            return false;
648        }
649
650        final String conversationSelfId = getConversationSelfId(dbWrapper, conversationId);
651        final String messageSelfId = message.getSelfId();
652
653        if (conversationSelfId == null || messageSelfId == null) {
654            return false;
655        }
656
657        // Get the sub IDs in effect for both the message and the conversation and compare them:
658        // 1. If message is unbound (using default sub id), then the message was sent with
659        //    pre-MSIM support. Don't auto-switch because we don't know the subscription for the
660        //    message.
661        // 2. If message is bound,
662        //    i. If conversation is unbound, use the system default sub id as its effective sub.
663        //    ii. If conversation is bound, use its subscription directly.
664        //    Compare the message sub id with the conversation's effective sub id. If they are
665        //    different, auto-switch the conversation to the message's sub.
666        final ParticipantData conversationSelf = getExistingParticipant(dbWrapper,
667                conversationSelfId);
668        final ParticipantData messageSelf = getExistingParticipant(dbWrapper, messageSelfId);
669        if (!messageSelf.isActiveSubscription()) {
670            // Don't switch if the message subscription is no longer active.
671            return false;
672        }
673        final int messageSubId = messageSelf.getSubId();
674        if (messageSubId == ParticipantData.DEFAULT_SELF_SUB_ID) {
675            return false;
676        }
677
678        final int conversationEffectiveSubId =
679                PhoneUtils.getDefault().getEffectiveSubId(conversationSelf.getSubId());
680
681        if (conversationEffectiveSubId != messageSubId) {
682            return addConversationSelfIdToContentValues(dbWrapper, messageSelf.getId(), values);
683        }
684        return false;
685    }
686
687    /**
688     * Adds conversation self id updates to ContentValues given. This performs check on the selfId
689     * to ensure it's valid and active.
690     * @return true if self id will need to be changed, false otherwise.
691     */
692    static boolean addConversationSelfIdToContentValues(final DatabaseWrapper dbWrapper,
693            final String selfId, final ContentValues values) {
694        // Make sure the selfId passed in is valid and active.
695        final String selection = ParticipantColumns._ID + "=? AND " +
696                ParticipantColumns.SIM_SLOT_ID + "<>?";
697        Cursor cursor = null;
698        try {
699            cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
700                    new String[] { ParticipantColumns._ID }, selection,
701                    new String[] { selfId, String.valueOf(ParticipantData.INVALID_SLOT_ID) },
702                    null, null, null);
703
704            if (cursor != null && cursor.getCount() > 0) {
705                values.put(ConversationColumns.CURRENT_SELF_ID, selfId);
706                return true;
707            }
708        } finally {
709            if (cursor != null) {
710                cursor.close();
711            }
712        }
713        return false;
714    }
715
716    private static void updateConversationDraftSnippetAndPreviewInTransaction(
717            final DatabaseWrapper dbWrapper, final String conversationId,
718            final MessageData draftMessage) {
719        Assert.isTrue(dbWrapper.getDatabase().inTransaction());
720
721        long sortTimestamp = 0L;
722        Cursor cursor = null;
723        try {
724            // Check to find the latest message in the conversation
725            cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
726                    REFRESH_CONVERSATION_MESSAGE_PROJECTION,
727                    MessageColumns.CONVERSATION_ID + "=?",
728                    new String[]{conversationId}, null, null,
729                    MessageColumns.RECEIVED_TIMESTAMP + " DESC", "1" /* limit */);
730
731            if (cursor.moveToFirst()) {
732                sortTimestamp = cursor.getLong(1);
733            }
734        } finally {
735            if (cursor != null) {
736                cursor.close();
737            }
738        }
739
740
741        final ContentValues values = new ContentValues();
742        if (draftMessage == null || !draftMessage.hasContent()) {
743            values.put(ConversationColumns.SHOW_DRAFT, 0);
744            values.put(ConversationColumns.DRAFT_SNIPPET_TEXT, "");
745            values.put(ConversationColumns.DRAFT_SUBJECT_TEXT, "");
746            values.put(ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE, "");
747            values.put(ConversationColumns.DRAFT_PREVIEW_URI, "");
748        } else {
749            sortTimestamp = Math.max(sortTimestamp, draftMessage.getReceivedTimeStamp());
750            values.put(ConversationColumns.SHOW_DRAFT, 1);
751            values.put(ConversationColumns.DRAFT_SNIPPET_TEXT, draftMessage.getMessageText());
752            values.put(ConversationColumns.DRAFT_SUBJECT_TEXT, draftMessage.getMmsSubject());
753            String type = null;
754            String uriString = null;
755            for (final MessagePartData part : draftMessage.getParts()) {
756                if (part.isAttachment() &&
757                        ContentType.isConversationListPreviewableType(part.getContentType())) {
758                    uriString = part.getContentUri().toString();
759                    type = part.getContentType();
760                    break;
761                }
762            }
763            values.put(ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE, type);
764            values.put(ConversationColumns.DRAFT_PREVIEW_URI, uriString);
765        }
766        values.put(ConversationColumns.SORT_TIMESTAMP, sortTimestamp);
767        // Called in transaction after reading conversation row
768        updateConversationRow(dbWrapper, conversationId, values);
769    }
770
771    @DoesNotRunOnMainThread
772    public static boolean updateConversationRowIfExists(final DatabaseWrapper dbWrapper,
773            final String conversationId, final ContentValues values) {
774        Assert.isNotMainThread();
775        return updateRowIfExists(dbWrapper, DatabaseHelper.CONVERSATIONS_TABLE,
776                ConversationColumns._ID, conversationId, values);
777    }
778
779    @DoesNotRunOnMainThread
780    public static void updateConversationRow(final DatabaseWrapper dbWrapper,
781            final String conversationId, final ContentValues values) {
782        Assert.isNotMainThread();
783        final boolean exists = updateConversationRowIfExists(dbWrapper, conversationId, values);
784        Assert.isTrue(exists);
785    }
786
787    @DoesNotRunOnMainThread
788    public static boolean updateMessageRowIfExists(final DatabaseWrapper dbWrapper,
789            final String messageId, final ContentValues values) {
790        Assert.isNotMainThread();
791        return updateRowIfExists(dbWrapper, DatabaseHelper.MESSAGES_TABLE, MessageColumns._ID,
792                messageId, values);
793    }
794
795    @DoesNotRunOnMainThread
796    public static void updateMessageRow(final DatabaseWrapper dbWrapper,
797            final String messageId, final ContentValues values) {
798        Assert.isNotMainThread();
799        final boolean exists = updateMessageRowIfExists(dbWrapper, messageId, values);
800        Assert.isTrue(exists);
801    }
802
803    @DoesNotRunOnMainThread
804    public static boolean updatePartRowIfExists(final DatabaseWrapper dbWrapper,
805            final String partId, final ContentValues values) {
806        Assert.isNotMainThread();
807        return updateRowIfExists(dbWrapper, DatabaseHelper.PARTS_TABLE, PartColumns._ID,
808                partId, values);
809    }
810
811    /**
812     * Returns the default conversation name based on its participants.
813     */
814    private static String getDefaultConversationName(final List<ParticipantData> participants) {
815        return ConversationListItemData.generateConversationName(participants);
816    }
817
818    /**
819     * Updates a given conversation's name based on its participants.
820     */
821    @DoesNotRunOnMainThread
822    public static void updateConversationNameAndAvatarInTransaction(
823            final DatabaseWrapper dbWrapper, final String conversationId) {
824        Assert.isNotMainThread();
825        Assert.isTrue(dbWrapper.getDatabase().inTransaction());
826
827        final ArrayList<ParticipantData> participants =
828                getParticipantsForConversation(dbWrapper, conversationId);
829        updateConversationNameAndAvatarInTransaction(dbWrapper, conversationId, participants);
830    }
831
832    /**
833     * Updates a given conversation's name based on its participants.
834     */
835    private static void updateConversationNameAndAvatarInTransaction(
836            final DatabaseWrapper dbWrapper, final String conversationId,
837            final List<ParticipantData> participants) {
838        Assert.isTrue(dbWrapper.getDatabase().inTransaction());
839
840        final ContentValues values = new ContentValues();
841        values.put(ConversationColumns.NAME,
842                getDefaultConversationName(participants));
843
844        fillParticipantData(values, participants);
845
846        // Used by background thread when refreshing conversation so conversation could be deleted.
847        updateConversationRowIfExists(dbWrapper, conversationId, values);
848
849        WidgetConversationProvider.notifyConversationRenamed(Factory.get().getApplicationContext(),
850                conversationId);
851    }
852
853    /**
854     * Updates a given conversation's self id.
855     */
856    @DoesNotRunOnMainThread
857    public static void updateConversationSelfIdInTransaction(
858            final DatabaseWrapper dbWrapper, final String conversationId, final String selfId) {
859        Assert.isNotMainThread();
860        Assert.isTrue(dbWrapper.getDatabase().inTransaction());
861        final ContentValues values = new ContentValues();
862        if (addConversationSelfIdToContentValues(dbWrapper, selfId, values)) {
863            updateConversationRowIfExists(dbWrapper, conversationId, values);
864        }
865    }
866
867    @DoesNotRunOnMainThread
868    public static String getConversationSelfId(final DatabaseWrapper dbWrapper,
869            final String conversationId) {
870        Assert.isNotMainThread();
871        Cursor cursor = null;
872        try {
873            cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
874                    new String[] { ConversationColumns.CURRENT_SELF_ID },
875                    ConversationColumns._ID + "=?",
876                    new String[] { conversationId },
877                    null, null, null);
878            Assert.inRange(cursor.getCount(), 0, 1);
879            if (cursor.moveToFirst()) {
880                return cursor.getString(0);
881            }
882        } finally {
883            if (cursor != null) {
884                cursor.close();
885            }
886        }
887        return null;
888    }
889
890    /**
891     * Frees up memory associated with phone number to participant id matching.
892     */
893    @DoesNotRunOnMainThread
894    public static void clearParticipantIdCache() {
895        Assert.isNotMainThread();
896        synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
897            sNormalizedPhoneNumberToParticipantIdCache.clear();
898        }
899    }
900
901    @DoesNotRunOnMainThread
902    public static ArrayList<String> getRecipientsForConversation(final DatabaseWrapper dbWrapper,
903            final String conversationId) {
904        Assert.isNotMainThread();
905        final ArrayList<ParticipantData> participants =
906                getParticipantsForConversation(dbWrapper, conversationId);
907
908        final ArrayList<String> recipients = new ArrayList<String>();
909        for (final ParticipantData participant : participants) {
910            recipients.add(participant.getSendDestination());
911        }
912
913        return recipients;
914    }
915
916    @DoesNotRunOnMainThread
917    public static String getSmsServiceCenterForConversation(final DatabaseWrapper dbWrapper,
918            final String conversationId) {
919        Assert.isNotMainThread();
920        Cursor cursor = null;
921        try {
922            cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
923                    new String[] { ConversationColumns.SMS_SERVICE_CENTER },
924                    ConversationColumns._ID + "=?",
925                    new String[] { conversationId },
926                    null, null, null);
927            Assert.inRange(cursor.getCount(), 0, 1);
928            if (cursor.moveToFirst()) {
929                return cursor.getString(0);
930            }
931        } finally {
932            if (cursor != null) {
933                cursor.close();
934            }
935        }
936        return null;
937    }
938
939    @DoesNotRunOnMainThread
940    public static ParticipantData getExistingParticipant(final DatabaseWrapper dbWrapper,
941            final String participantId) {
942        Assert.isNotMainThread();
943        ParticipantData participant = null;
944        Cursor cursor = null;
945        try {
946            cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
947                    ParticipantData.ParticipantsQuery.PROJECTION,
948                    ParticipantColumns._ID + " =?",
949                    new String[] { participantId }, null, null, null);
950            Assert.inRange(cursor.getCount(), 0, 1);
951            if (cursor.moveToFirst()) {
952                participant = ParticipantData.getFromCursor(cursor);
953            }
954        } finally {
955            if (cursor != null) {
956                cursor.close();
957            }
958        }
959
960        return participant;
961    }
962
963    static int getSelfSubscriptionId(final DatabaseWrapper dbWrapper,
964            final String selfParticipantId) {
965        final ParticipantData selfParticipant = BugleDatabaseOperations.getExistingParticipant(
966                dbWrapper, selfParticipantId);
967        if (selfParticipant != null) {
968            Assert.isTrue(selfParticipant.isSelf());
969            return selfParticipant.getSubId();
970        }
971        return ParticipantData.DEFAULT_SELF_SUB_ID;
972    }
973
974    @VisibleForTesting
975    @DoesNotRunOnMainThread
976    public static ArrayList<ParticipantData> getParticipantsForConversation(
977            final DatabaseWrapper dbWrapper, final String conversationId) {
978        Assert.isNotMainThread();
979        final ArrayList<ParticipantData> participants =
980                new ArrayList<ParticipantData>();
981        Cursor cursor = null;
982        try {
983            cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
984                    ParticipantData.ParticipantsQuery.PROJECTION,
985                    ParticipantColumns._ID + " IN ( " + "SELECT "
986                            + ConversationParticipantsColumns.PARTICIPANT_ID + " AS "
987                            + ParticipantColumns._ID
988                            + " FROM " + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE
989                            + " WHERE " + ConversationParticipantsColumns.CONVERSATION_ID + " =? )",
990                            new String[] { conversationId }, null, null, null);
991
992            while (cursor.moveToNext()) {
993                participants.add(ParticipantData.getFromCursor(cursor));
994            }
995        } finally {
996            if (cursor != null) {
997                cursor.close();
998            }
999        }
1000
1001        return participants;
1002    }
1003
1004    @DoesNotRunOnMainThread
1005    public static MessageData readMessage(final DatabaseWrapper dbWrapper, final String messageId) {
1006        Assert.isNotMainThread();
1007        final MessageData message = readMessageData(dbWrapper, messageId);
1008        if (message != null) {
1009            readMessagePartsData(dbWrapper, message, false);
1010        }
1011        return message;
1012    }
1013
1014    @VisibleForTesting
1015    static MessagePartData readMessagePartData(final DatabaseWrapper dbWrapper,
1016            final String partId) {
1017        MessagePartData messagePartData = null;
1018        Cursor cursor = null;
1019        try {
1020            cursor = dbWrapper.query(DatabaseHelper.PARTS_TABLE,
1021                    MessagePartData.getProjection(), PartColumns._ID + "=?",
1022                    new String[] { partId }, null, null, null);
1023            Assert.inRange(cursor.getCount(), 0, 1);
1024            if (cursor.moveToFirst()) {
1025                messagePartData = MessagePartData.createFromCursor(cursor);
1026            }
1027        } finally {
1028            if (cursor != null) {
1029                cursor.close();
1030            }
1031        }
1032        return messagePartData;
1033    }
1034
1035    @DoesNotRunOnMainThread
1036    public static MessageData readMessageData(final DatabaseWrapper dbWrapper,
1037            final Uri smsMessageUri) {
1038        Assert.isNotMainThread();
1039        MessageData message = null;
1040        Cursor cursor = null;
1041        try {
1042            cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
1043                    MessageData.getProjection(), MessageColumns.SMS_MESSAGE_URI + "=?",
1044                    new String[] { smsMessageUri.toString() }, null, null, null);
1045            Assert.inRange(cursor.getCount(), 0, 1);
1046            if (cursor.moveToFirst()) {
1047                message = new MessageData();
1048                message.bind(cursor);
1049            }
1050        } finally {
1051            if (cursor != null) {
1052                cursor.close();
1053            }
1054        }
1055        return message;
1056    }
1057
1058    @DoesNotRunOnMainThread
1059    public static MessageData readMessageData(final DatabaseWrapper dbWrapper,
1060            final String messageId) {
1061        Assert.isNotMainThread();
1062        MessageData message = null;
1063        Cursor cursor = null;
1064        try {
1065            cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
1066                    MessageData.getProjection(), MessageColumns._ID + "=?",
1067                    new String[] { messageId }, null, null, null);
1068            Assert.inRange(cursor.getCount(), 0, 1);
1069            if (cursor.moveToFirst()) {
1070                message = new MessageData();
1071                message.bind(cursor);
1072            }
1073        } finally {
1074            if (cursor != null) {
1075                cursor.close();
1076            }
1077        }
1078        return message;
1079    }
1080
1081    /**
1082     * Read all the parts for a message
1083     * @param dbWrapper database
1084     * @param message read parts for this message
1085     * @param checkAttachmentFilesExist check each attachment file and only include if file exists
1086     */
1087    private static void readMessagePartsData(final DatabaseWrapper dbWrapper,
1088            final MessageData message, final boolean checkAttachmentFilesExist) {
1089        final ContentResolver contentResolver =
1090                Factory.get().getApplicationContext().getContentResolver();
1091        Cursor cursor = null;
1092        try {
1093            cursor = dbWrapper.query(DatabaseHelper.PARTS_TABLE,
1094                    MessagePartData.getProjection(), PartColumns.MESSAGE_ID + "=?",
1095                    new String[] { message.getMessageId() }, null, null, null);
1096            while (cursor.moveToNext()) {
1097                final MessagePartData messagePartData = MessagePartData.createFromCursor(cursor);
1098                if (checkAttachmentFilesExist && messagePartData.isAttachment() &&
1099                        !UriUtil.isBugleAppResource(messagePartData.getContentUri())) {
1100                    try {
1101                        // Test that the file exists before adding the attachment to the draft
1102                        final ParcelFileDescriptor fileDescriptor =
1103                                contentResolver.openFileDescriptor(
1104                                        messagePartData.getContentUri(), "r");
1105                        if (fileDescriptor != null) {
1106                            fileDescriptor.close();
1107                            message.addPart(messagePartData);
1108                        }
1109                    } catch (final IOException e) {
1110                        // The attachment's temp storage no longer exists, just ignore the file
1111                    } catch (final SecurityException e) {
1112                        // Likely thrown by openFileDescriptor due to an expired access grant.
1113                        if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
1114                            LogUtil.d(LogUtil.BUGLE_TAG, "uri: " + messagePartData.getContentUri());
1115                        }
1116                    }
1117                } else {
1118                    message.addPart(messagePartData);
1119                }
1120            }
1121        } finally {
1122            if (cursor != null) {
1123                cursor.close();
1124            }
1125        }
1126    }
1127
1128    /**
1129     * Write a message part to our local database
1130     *
1131     * @param dbWrapper     The database
1132     * @param messagePart   The message part to insert
1133     * @return The row id of the newly inserted part
1134     */
1135    static String insertNewMessagePartInTransaction(final DatabaseWrapper dbWrapper,
1136            final MessagePartData messagePart, final String conversationId) {
1137        Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1138        Assert.isTrue(!TextUtils.isEmpty(messagePart.getMessageId()));
1139
1140        // Insert a new part row
1141        final SQLiteStatement insert = messagePart.getInsertStatement(dbWrapper, conversationId);
1142        final long rowNumber = insert.executeInsert();
1143
1144        Assert.inRange(rowNumber, 0, Long.MAX_VALUE);
1145        final String partId = Long.toString(rowNumber);
1146
1147        // Update the part id
1148        messagePart.updatePartId(partId);
1149
1150        return partId;
1151    }
1152
1153    /**
1154     * Insert a message and its parts into the table
1155     */
1156    @DoesNotRunOnMainThread
1157    public static void insertNewMessageInTransaction(final DatabaseWrapper dbWrapper,
1158            final MessageData message) {
1159        Assert.isNotMainThread();
1160        Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1161
1162        // Insert message row
1163        final SQLiteStatement insert = message.getInsertStatement(dbWrapper);
1164        final long rowNumber = insert.executeInsert();
1165
1166        Assert.inRange(rowNumber, 0, Long.MAX_VALUE);
1167        final String messageId = Long.toString(rowNumber);
1168        message.updateMessageId(messageId);
1169        //  Insert new parts
1170        for (final MessagePartData messagePart : message.getParts()) {
1171            messagePart.updateMessageId(messageId);
1172            insertNewMessagePartInTransaction(dbWrapper, messagePart, message.getConversationId());
1173        }
1174    }
1175
1176    /**
1177     * Update a message and add its parts into the table
1178     */
1179    @DoesNotRunOnMainThread
1180    public static void updateMessageInTransaction(final DatabaseWrapper dbWrapper,
1181            final MessageData message) {
1182        Assert.isNotMainThread();
1183        Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1184        final String messageId = message.getMessageId();
1185        // Check message still exists (sms sync or delete might have purged it)
1186        final MessageData current = BugleDatabaseOperations.readMessage(dbWrapper, messageId);
1187        if (current != null) {
1188            // Delete existing message parts)
1189            deletePartsForMessage(dbWrapper, message.getMessageId());
1190            //  Insert new parts
1191            for (final MessagePartData messagePart : message.getParts()) {
1192                messagePart.updatePartId(null);
1193                messagePart.updateMessageId(message.getMessageId());
1194                insertNewMessagePartInTransaction(dbWrapper, messagePart,
1195                        message.getConversationId());
1196            }
1197            //  Update message row
1198            final ContentValues values = new ContentValues();
1199            message.populate(values);
1200            updateMessageRowIfExists(dbWrapper, message.getMessageId(), values);
1201        }
1202    }
1203
1204    @DoesNotRunOnMainThread
1205    public static void updateMessageAndPartsInTransaction(final DatabaseWrapper dbWrapper,
1206            final MessageData message, final List<MessagePartData> partsToUpdate) {
1207        Assert.isNotMainThread();
1208        Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1209        final ContentValues values = new ContentValues();
1210        for (final MessagePartData messagePart : partsToUpdate) {
1211            values.clear();
1212            messagePart.populate(values);
1213            updatePartRowIfExists(dbWrapper, messagePart.getPartId(), values);
1214        }
1215        values.clear();
1216        message.populate(values);
1217        updateMessageRowIfExists(dbWrapper, message.getMessageId(), values);
1218    }
1219
1220    /**
1221     * Delete all parts for a message
1222     */
1223    static void deletePartsForMessage(final DatabaseWrapper dbWrapper,
1224            final String messageId) {
1225        final int cnt = dbWrapper.delete(DatabaseHelper.PARTS_TABLE,
1226                PartColumns.MESSAGE_ID + " =?",
1227                new String[] { messageId });
1228        Assert.inRange(cnt, 0, Integer.MAX_VALUE);
1229    }
1230
1231    /**
1232     * Delete one message and update the conversation (if necessary).
1233     *
1234     * @return number of rows deleted (should be 1 or 0).
1235     */
1236    @DoesNotRunOnMainThread
1237    public static int deleteMessage(final DatabaseWrapper dbWrapper, final String messageId) {
1238        Assert.isNotMainThread();
1239        dbWrapper.beginTransaction();
1240        try {
1241            // Read message to find out which conversation it is in
1242            final MessageData message = BugleDatabaseOperations.readMessage(dbWrapper, messageId);
1243
1244            int count = 0;
1245            if (message != null) {
1246                final String conversationId = message.getConversationId();
1247                // Delete message
1248                count = dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
1249                        MessageColumns._ID + "=?", new String[] { messageId });
1250
1251                if (!deleteConversationIfEmptyInTransaction(dbWrapper, conversationId)) {
1252                    // TODO: Should we leave the conversation sort timestamp alone?
1253                    refreshConversationMetadataInTransaction(dbWrapper, conversationId,
1254                            false/* shouldAutoSwitchSelfId */, false/*archived*/);
1255                }
1256            }
1257            dbWrapper.setTransactionSuccessful();
1258            return count;
1259        } finally {
1260            dbWrapper.endTransaction();
1261        }
1262    }
1263
1264    /**
1265     * Deletes the conversation if there are zero non-draft messages left.
1266     * <p>
1267     * This is necessary because the telephony database has a trigger that deletes threads after
1268     * their last message is deleted. We need to ensure that if a thread goes away, we also delete
1269     * the conversation in Bugle. We don't store draft messages in telephony, so we ignore those
1270     * when querying for the # of messages in the conversation.
1271     *
1272     * @return true if the conversation was deleted
1273     */
1274    @DoesNotRunOnMainThread
1275    public static boolean deleteConversationIfEmptyInTransaction(final DatabaseWrapper dbWrapper,
1276            final String conversationId) {
1277        Assert.isNotMainThread();
1278        Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1279        Cursor cursor = null;
1280        try {
1281            // TODO: The refreshConversationMetadataInTransaction method below uses this
1282            // same query; maybe they should share this logic?
1283
1284            // Check to see if there are any (non-draft) messages in the conversation
1285            cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
1286                    REFRESH_CONVERSATION_MESSAGE_PROJECTION,
1287                    MessageColumns.CONVERSATION_ID + "=? AND " +
1288                    MessageColumns.STATUS + "!=" + MessageData.BUGLE_STATUS_OUTGOING_DRAFT,
1289                    new String[] { conversationId }, null, null,
1290                    MessageColumns.RECEIVED_TIMESTAMP + " DESC", "1" /* limit */);
1291            if (cursor.getCount() == 0) {
1292                dbWrapper.delete(DatabaseHelper.CONVERSATIONS_TABLE,
1293                        ConversationColumns._ID + "=?", new String[] { conversationId });
1294                LogUtil.i(TAG,
1295                        "BugleDatabaseOperations: Deleted empty conversation " + conversationId);
1296                return true;
1297            } else {
1298                return false;
1299            }
1300        } finally {
1301            if (cursor != null) {
1302                cursor.close();
1303            }
1304        }
1305    }
1306
1307    private static final String[] REFRESH_CONVERSATION_MESSAGE_PROJECTION = new String[] {
1308        MessageColumns._ID,
1309        MessageColumns.RECEIVED_TIMESTAMP,
1310        MessageColumns.SENDER_PARTICIPANT_ID
1311    };
1312
1313    /**
1314     * Update conversation snippet, timestamp and optionally self id to match latest message in
1315     * conversation.
1316     */
1317    @DoesNotRunOnMainThread
1318    public static void refreshConversationMetadataInTransaction(final DatabaseWrapper dbWrapper,
1319            final String conversationId, final boolean shouldAutoSwitchSelfId,
1320            boolean keepArchived) {
1321        Assert.isNotMainThread();
1322        Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1323        Cursor cursor = null;
1324        try {
1325            // Check to see if there are any (non-draft) messages in the conversation
1326            cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
1327                    REFRESH_CONVERSATION_MESSAGE_PROJECTION,
1328                    MessageColumns.CONVERSATION_ID + "=? AND " +
1329                    MessageColumns.STATUS + "!=" + MessageData.BUGLE_STATUS_OUTGOING_DRAFT,
1330                    new String[] { conversationId }, null, null,
1331                    MessageColumns.RECEIVED_TIMESTAMP + " DESC", "1" /* limit */);
1332
1333            if (cursor.moveToFirst()) {
1334                // Refresh latest message in conversation
1335                final String latestMessageId = cursor.getString(0);
1336                final long latestMessageTimestamp = cursor.getLong(1);
1337                final String senderParticipantId = cursor.getString(2);
1338                final boolean senderBlocked = isBlockedParticipant(dbWrapper, senderParticipantId);
1339                updateConversationMetadataInTransaction(dbWrapper, conversationId,
1340                        latestMessageId, latestMessageTimestamp, senderBlocked || keepArchived,
1341                        shouldAutoSwitchSelfId);
1342            }
1343        } finally {
1344            if (cursor != null) {
1345                cursor.close();
1346            }
1347        }
1348    }
1349
1350    /**
1351     * When moving/removing an existing message update conversation metadata if necessary
1352     * @param dbWrapper      db wrapper
1353     * @param conversationId conversation to modify
1354     * @param messageId      message that is leaving the conversation
1355     * @param shouldAutoSwitchSelfId should we try to auto-switch the conversation's self-id as a
1356     *        result of this call when we see a new latest message?
1357     * @param keepArchived   should we keep the conversation archived despite refresh
1358     */
1359    @DoesNotRunOnMainThread
1360    public static void maybeRefreshConversationMetadataInTransaction(
1361            final DatabaseWrapper dbWrapper, final String conversationId, final String messageId,
1362            final boolean shouldAutoSwitchSelfId, final boolean keepArchived) {
1363        Assert.isNotMainThread();
1364        boolean refresh = true;
1365        if (!TextUtils.isEmpty(messageId)) {
1366            refresh = false;
1367            // Look for an existing conversation in the db with this conversation id
1368            Cursor cursor = null;
1369            try {
1370                cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
1371                        new String[] { ConversationColumns.LATEST_MESSAGE_ID },
1372                        ConversationColumns._ID + "=?",
1373                        new String[] { conversationId },
1374                        null, null, null);
1375                Assert.inRange(cursor.getCount(), 0, 1);
1376                if (cursor.moveToFirst()) {
1377                    refresh = TextUtils.equals(cursor.getString(0), messageId);
1378                }
1379            } finally {
1380                if (cursor != null) {
1381                    cursor.close();
1382                }
1383            }
1384        }
1385        if (refresh) {
1386            // TODO: I think it is okay to delete the conversation if it is empty...
1387            refreshConversationMetadataInTransaction(dbWrapper, conversationId,
1388                    shouldAutoSwitchSelfId, keepArchived);
1389        }
1390    }
1391
1392
1393
1394    // SQL statement to query latest message if for particular conversation
1395    private static final String QUERY_CONVERSATIONS_LATEST_MESSAGE_SQL = "SELECT "
1396            + ConversationColumns.LATEST_MESSAGE_ID + " FROM " + DatabaseHelper.CONVERSATIONS_TABLE
1397            + " WHERE " + ConversationColumns._ID + "=? LIMIT 1";
1398
1399    /**
1400     * Note this is not thread safe so callers need to make sure they own the wrapper + statements
1401     * while they call this and use the returned value.
1402     */
1403    @DoesNotRunOnMainThread
1404    public static SQLiteStatement getQueryConversationsLatestMessageStatement(
1405            final DatabaseWrapper db, final String conversationId) {
1406        Assert.isNotMainThread();
1407        final SQLiteStatement query = db.getStatementInTransaction(
1408                DatabaseWrapper.INDEX_QUERY_CONVERSATIONS_LATEST_MESSAGE,
1409                QUERY_CONVERSATIONS_LATEST_MESSAGE_SQL);
1410        query.clearBindings();
1411        query.bindString(1, conversationId);
1412        return query;
1413    }
1414
1415    // SQL statement to query latest message if for particular conversation
1416    private static final String QUERY_MESSAGES_LATEST_MESSAGE_SQL = "SELECT "
1417            + MessageColumns._ID + " FROM " + DatabaseHelper.MESSAGES_TABLE
1418            + " WHERE " + MessageColumns.CONVERSATION_ID + "=? ORDER BY "
1419            + MessageColumns.RECEIVED_TIMESTAMP + " DESC LIMIT 1";
1420
1421    /**
1422     * Note this is not thread safe so callers need to make sure they own the wrapper + statements
1423     * while they call this and use the returned value.
1424     */
1425    @DoesNotRunOnMainThread
1426    public static SQLiteStatement getQueryMessagesLatestMessageStatement(
1427            final DatabaseWrapper db, final String conversationId) {
1428        Assert.isNotMainThread();
1429        final SQLiteStatement query = db.getStatementInTransaction(
1430                DatabaseWrapper.INDEX_QUERY_MESSAGES_LATEST_MESSAGE,
1431                QUERY_MESSAGES_LATEST_MESSAGE_SQL);
1432        query.clearBindings();
1433        query.bindString(1, conversationId);
1434        return query;
1435    }
1436
1437    /**
1438     * Update conversation metadata if necessary
1439     * @param dbWrapper      db wrapper
1440     * @param conversationId conversation to modify
1441     * @param shouldAutoSwitchSelfId should we try to auto-switch the conversation's self-id as a
1442     *                               result of this call when we see a new latest message?
1443     * @param keepArchived if the conversation should be kept archived
1444     */
1445    @DoesNotRunOnMainThread
1446    public static void maybeRefreshConversationMetadataInTransaction(
1447            final DatabaseWrapper dbWrapper, final String conversationId,
1448            final boolean shouldAutoSwitchSelfId, boolean keepArchived) {
1449        Assert.isNotMainThread();
1450        String currentLatestMessageId = null;
1451        String latestMessageId = null;
1452        try {
1453            final SQLiteStatement currentLatestMessageIdSql =
1454                    getQueryConversationsLatestMessageStatement(dbWrapper, conversationId);
1455            currentLatestMessageId = currentLatestMessageIdSql.simpleQueryForString();
1456
1457            final SQLiteStatement latestMessageIdSql =
1458                    getQueryMessagesLatestMessageStatement(dbWrapper, conversationId);
1459            latestMessageId = latestMessageIdSql.simpleQueryForString();
1460        } catch (final SQLiteDoneException e) {
1461            LogUtil.e(TAG, "BugleDatabaseOperations: Query for latest message failed", e);
1462        }
1463
1464        if (TextUtils.isEmpty(currentLatestMessageId) ||
1465                !TextUtils.equals(currentLatestMessageId, latestMessageId)) {
1466            refreshConversationMetadataInTransaction(dbWrapper, conversationId,
1467                    shouldAutoSwitchSelfId, keepArchived);
1468        }
1469    }
1470
1471    static boolean getConversationExists(final DatabaseWrapper dbWrapper,
1472            final String conversationId) {
1473        // Look for an existing conversation in the db with this conversation id
1474        Cursor cursor = null;
1475        try {
1476            cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
1477                    new String[] { /* No projection */},
1478                    ConversationColumns._ID + "=?",
1479                    new String[] { conversationId },
1480                    null, null, null);
1481            return cursor.getCount() == 1;
1482        } finally {
1483            if (cursor != null) {
1484                cursor.close();
1485            }
1486        }
1487    }
1488
1489    /** Preserve parts in message but clear the stored draft */
1490    public static final int UPDATE_MODE_CLEAR_DRAFT = 1;
1491    /** Add the message as a draft */
1492    public static final int UPDATE_MODE_ADD_DRAFT = 2;
1493
1494    /**
1495     * Update draft message for specified conversation
1496     * @param dbWrapper       local database (wrapped)
1497     * @param conversationId  conversation to update
1498     * @param message         Optional message to preserve attachments for (either as draft or for
1499     *                        sending)
1500     * @param updateMode      either {@link #UPDATE_MODE_CLEAR_DRAFT} or
1501     *                        {@link #UPDATE_MODE_ADD_DRAFT}
1502     * @return message id of newly written draft (else null)
1503     */
1504    @DoesNotRunOnMainThread
1505    public static String updateDraftMessageData(final DatabaseWrapper dbWrapper,
1506            final String conversationId, @Nullable final MessageData message,
1507            final int updateMode) {
1508        Assert.isNotMainThread();
1509        Assert.notNull(conversationId);
1510        Assert.inRange(updateMode, UPDATE_MODE_CLEAR_DRAFT, UPDATE_MODE_ADD_DRAFT);
1511        String messageId = null;
1512        Cursor cursor = null;
1513        dbWrapper.beginTransaction();
1514        try {
1515            // Find all draft parts for the current conversation
1516            final SimpleArrayMap<Uri, MessagePartData> currentDraftParts = new SimpleArrayMap<>();
1517            cursor = dbWrapper.query(DatabaseHelper.DRAFT_PARTS_VIEW,
1518                    MessagePartData.getProjection(),
1519                    MessageColumns.CONVERSATION_ID + " =?",
1520                    new String[] { conversationId }, null, null, null);
1521            while (cursor.moveToNext()) {
1522                final MessagePartData part = MessagePartData.createFromCursor(cursor);
1523                if (part.isAttachment()) {
1524                    currentDraftParts.put(part.getContentUri(), part);
1525                }
1526            }
1527            // Optionally, preserve attachments for "message"
1528            final boolean conversationExists = getConversationExists(dbWrapper, conversationId);
1529            if (message != null && conversationExists) {
1530                for (final MessagePartData part : message.getParts()) {
1531                    if (part.isAttachment()) {
1532                        currentDraftParts.remove(part.getContentUri());
1533                    }
1534                }
1535            }
1536
1537            // Delete orphan content
1538            for (int index = 0; index < currentDraftParts.size(); index++) {
1539                final MessagePartData part = currentDraftParts.valueAt(index);
1540                part.destroySync();
1541            }
1542
1543            // Delete existing draft (cascade deletes parts)
1544            dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
1545                    MessageColumns.STATUS + "=? AND " + MessageColumns.CONVERSATION_ID + "=?",
1546                    new String[] {
1547                        Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_DRAFT),
1548                        conversationId
1549                    });
1550
1551            // Write new draft
1552            if (updateMode == UPDATE_MODE_ADD_DRAFT && message != null
1553                    && message.hasContent() && conversationExists) {
1554                Assert.equals(MessageData.BUGLE_STATUS_OUTGOING_DRAFT,
1555                        message.getStatus());
1556
1557                // Now add draft to message table
1558                insertNewMessageInTransaction(dbWrapper, message);
1559                messageId = message.getMessageId();
1560            }
1561
1562            if (conversationExists) {
1563                updateConversationDraftSnippetAndPreviewInTransaction(
1564                        dbWrapper, conversationId, message);
1565
1566                if (message != null && message.getSelfId() != null) {
1567                    updateConversationSelfIdInTransaction(dbWrapper, conversationId,
1568                            message.getSelfId());
1569                }
1570            }
1571
1572            dbWrapper.setTransactionSuccessful();
1573        } finally {
1574            dbWrapper.endTransaction();
1575            if (cursor != null) {
1576                cursor.close();
1577            }
1578        }
1579        if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
1580            LogUtil.v(TAG,
1581                    "Updated draft message " + messageId + " for conversation " + conversationId);
1582        }
1583        return messageId;
1584    }
1585
1586    /**
1587     * Read the first draft message associated with this conversation.
1588     * If none present create an empty (sms) draft message.
1589     */
1590    @DoesNotRunOnMainThread
1591    public static MessageData readDraftMessageData(final DatabaseWrapper dbWrapper,
1592            final String conversationId, final String conversationSelfId) {
1593        Assert.isNotMainThread();
1594        MessageData message = null;
1595        Cursor cursor = null;
1596        dbWrapper.beginTransaction();
1597        try {
1598            cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
1599                    MessageData.getProjection(),
1600                    MessageColumns.STATUS + "=? AND " + MessageColumns.CONVERSATION_ID + "=?",
1601                    new String[] {
1602                        Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_DRAFT),
1603                        conversationId
1604                    }, null, null, null);
1605            Assert.inRange(cursor.getCount(), 0, 1);
1606            if (cursor.moveToFirst()) {
1607                message = new MessageData();
1608                message.bindDraft(cursor, conversationSelfId);
1609                readMessagePartsData(dbWrapper, message, true);
1610                // Disconnect draft parts from DB
1611                for (final MessagePartData part : message.getParts()) {
1612                    part.updatePartId(null);
1613                    part.updateMessageId(null);
1614                }
1615                message.updateMessageId(null);
1616            }
1617            dbWrapper.setTransactionSuccessful();
1618        } finally {
1619            dbWrapper.endTransaction();
1620            if (cursor != null) {
1621                cursor.close();
1622            }
1623        }
1624        return message;
1625    }
1626
1627    // Internal
1628    private static void addParticipantToConversation(final DatabaseWrapper dbWrapper,
1629            final ParticipantData participant, final String conversationId) {
1630        final String participantId = getOrCreateParticipantInTransaction(dbWrapper, participant);
1631        Assert.notNull(participantId);
1632
1633        // Add the participant to the conversation participants table
1634        final ContentValues values = new ContentValues();
1635        values.put(ConversationParticipantsColumns.CONVERSATION_ID, conversationId);
1636        values.put(ConversationParticipantsColumns.PARTICIPANT_ID, participantId);
1637        dbWrapper.insert(DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE, null, values);
1638    }
1639
1640    /**
1641     * Get string used as canonical recipient for participant cache for sub id
1642     */
1643    private static String getCanonicalRecipientFromSubId(final int subId) {
1644        return "SELF(" + subId + ")";
1645    }
1646
1647    /**
1648     * Maps from a sub id or phone number to a participant id if there is one.
1649     *
1650     * @return If the participant is available in our cache, or the DB, this returns the
1651     * participant id for the given subid/phone number.  Otherwise it returns null.
1652     */
1653    @VisibleForTesting
1654    private static String getParticipantId(final DatabaseWrapper dbWrapper,
1655            final int subId, final String canonicalRecipient) {
1656        // First check our memory cache for the participant Id
1657        String participantId;
1658        synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
1659            participantId = sNormalizedPhoneNumberToParticipantIdCache.get(canonicalRecipient);
1660        }
1661
1662        if (participantId != null) {
1663            return participantId;
1664        }
1665
1666        // This code will only be executed for incremental additions.
1667        Cursor cursor = null;
1668        try {
1669            if (subId != ParticipantData.OTHER_THAN_SELF_SUB_ID) {
1670                // Now look for an existing participant in the db with this sub id.
1671                cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
1672                        new String[] {ParticipantColumns._ID},
1673                        ParticipantColumns.SUB_ID + "=?",
1674                        new String[] { Integer.toString(subId) }, null, null, null);
1675            } else {
1676                // Look for existing participant with this normalized phone number and no subId.
1677                cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
1678                        new String[] {ParticipantColumns._ID},
1679                        ParticipantColumns.NORMALIZED_DESTINATION + "=? AND "
1680                                + ParticipantColumns.SUB_ID + "=?",
1681                                new String[] {canonicalRecipient, Integer.toString(subId)},
1682                                null, null, null);
1683            }
1684
1685            if (cursor.moveToFirst()) {
1686                // TODO Is this assert correct for multi-sim where a new sim was put in?
1687                Assert.isTrue(cursor.getCount() == 1);
1688
1689                // We found an existing participant in the database
1690                participantId = cursor.getString(0);
1691
1692                synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
1693                    // Add it to the cache for next time
1694                    sNormalizedPhoneNumberToParticipantIdCache.put(canonicalRecipient,
1695                            participantId);
1696                }
1697            }
1698        } finally {
1699            if (cursor != null) {
1700                cursor.close();
1701            }
1702        }
1703        return participantId;
1704    }
1705
1706    @DoesNotRunOnMainThread
1707    public static ParticipantData getOrCreateSelf(final DatabaseWrapper dbWrapper,
1708            final int subId) {
1709        Assert.isNotMainThread();
1710        ParticipantData participant = null;
1711        dbWrapper.beginTransaction();
1712        try {
1713            final ParticipantData shell = ParticipantData.getSelfParticipant(subId);
1714            final String participantId = getOrCreateParticipantInTransaction(dbWrapper, shell);
1715            participant = getExistingParticipant(dbWrapper, participantId);
1716            dbWrapper.setTransactionSuccessful();
1717        } finally {
1718            dbWrapper.endTransaction();
1719        }
1720        return participant;
1721    }
1722
1723    /**
1724     * Lookup and if necessary create a new participant
1725     * @param dbWrapper      Database wrapper
1726     * @param participant    Participant to find/create
1727     * @return participantId ParticipantId for existing or newly created participant
1728     */
1729    @DoesNotRunOnMainThread
1730    public static String getOrCreateParticipantInTransaction(final DatabaseWrapper dbWrapper,
1731            final ParticipantData participant) {
1732        Assert.isNotMainThread();
1733        Assert.isTrue(dbWrapper.getDatabase().inTransaction());
1734        int subId = ParticipantData.OTHER_THAN_SELF_SUB_ID;
1735        String participantId = null;
1736        String canonicalRecipient = null;
1737        if (participant.isSelf()) {
1738            subId = participant.getSubId();
1739            canonicalRecipient = getCanonicalRecipientFromSubId(subId);
1740        } else {
1741            canonicalRecipient = participant.getNormalizedDestination();
1742        }
1743        Assert.notNull(canonicalRecipient);
1744        participantId = getParticipantId(dbWrapper, subId, canonicalRecipient);
1745
1746        if (participantId != null) {
1747            return participantId;
1748        }
1749
1750        if (!participant.isContactIdResolved()) {
1751            // Refresh participant's name and avatar with matching contact in CP2.
1752            ParticipantRefresh.refreshParticipant(dbWrapper, participant);
1753        }
1754
1755        // Insert the participant into the participants table
1756        final ContentValues values = participant.toContentValues();
1757        final long participantRow = dbWrapper.insert(DatabaseHelper.PARTICIPANTS_TABLE, null,
1758                values);
1759        participantId = Long.toString(participantRow);
1760        Assert.notNull(canonicalRecipient);
1761
1762        synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
1763            // Now that we've inserted it, add it to our cache
1764            sNormalizedPhoneNumberToParticipantIdCache.put(canonicalRecipient, participantId);
1765        }
1766
1767        return participantId;
1768    }
1769
1770    @DoesNotRunOnMainThread
1771    public static void updateDestination(final DatabaseWrapper dbWrapper,
1772            final String destination, final boolean blocked) {
1773        Assert.isNotMainThread();
1774        final ContentValues values = new ContentValues();
1775        values.put(ParticipantColumns.BLOCKED, blocked ? 1 : 0);
1776        dbWrapper.update(DatabaseHelper.PARTICIPANTS_TABLE, values,
1777                ParticipantColumns.NORMALIZED_DESTINATION + "=? AND " +
1778                        ParticipantColumns.SUB_ID + "=?",
1779                new String[] { destination, Integer.toString(
1780                        ParticipantData.OTHER_THAN_SELF_SUB_ID) });
1781    }
1782
1783    @DoesNotRunOnMainThread
1784    public static String getConversationFromOtherParticipantDestination(
1785            final DatabaseWrapper db, final String otherDestination) {
1786        Assert.isNotMainThread();
1787        Cursor cursor = null;
1788        try {
1789            cursor = db.query(DatabaseHelper.CONVERSATIONS_TABLE,
1790                    new String[] { ConversationColumns._ID },
1791                    ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION + "=?",
1792                    new String[] { otherDestination }, null, null, null);
1793            Assert.inRange(cursor.getCount(), 0, 1);
1794            if (cursor.moveToFirst()) {
1795                return cursor.getString(0);
1796            }
1797        } finally {
1798            if (cursor != null) {
1799                cursor.close();
1800            }
1801        }
1802        return null;
1803    }
1804
1805
1806    /**
1807     * Get a list of conversations that contain any of participants specified.
1808     */
1809    private static HashSet<String> getConversationsForParticipants(
1810            final ArrayList<String> participantIds) {
1811        final DatabaseWrapper db = DataModel.get().getDatabase();
1812        final HashSet<String> conversationIds = new HashSet<String>();
1813
1814        final String selection = ConversationParticipantsColumns.PARTICIPANT_ID + "=?";
1815        for (final String participantId : participantIds) {
1816            final String[] selectionArgs = new String[] { participantId };
1817            final Cursor cursor = db.query(DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE,
1818                    ConversationParticipantsQuery.PROJECTION,
1819                    selection, selectionArgs, null, null, null);
1820
1821            if (cursor != null) {
1822                try {
1823                    while (cursor.moveToNext()) {
1824                        final String conversationId = cursor.getString(
1825                                ConversationParticipantsQuery.INDEX_CONVERSATION_ID);
1826                        conversationIds.add(conversationId);
1827                    }
1828                } finally {
1829                    cursor.close();
1830                }
1831            }
1832        }
1833
1834        return conversationIds;
1835    }
1836
1837    /**
1838     * Refresh conversation names/avatars based on a list of participants that are changed.
1839     */
1840    @DoesNotRunOnMainThread
1841    public static void refreshConversationsForParticipants(final ArrayList<String> participants) {
1842        Assert.isNotMainThread();
1843        final HashSet<String> conversationIds = getConversationsForParticipants(participants);
1844        if (conversationIds.size() > 0) {
1845            for (final String conversationId : conversationIds) {
1846                refreshConversation(conversationId);
1847            }
1848
1849            MessagingContentProvider.notifyConversationListChanged();
1850            if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
1851                LogUtil.v(TAG, "Number of conversations refreshed:" + conversationIds.size());
1852            }
1853        }
1854    }
1855
1856    /**
1857     * Refresh conversation names/avatars based on a changed participant.
1858     */
1859    @DoesNotRunOnMainThread
1860    public static void refreshConversationsForParticipant(final String participantId) {
1861        Assert.isNotMainThread();
1862        final ArrayList<String> participantList = new ArrayList<String>(1);
1863        participantList.add(participantId);
1864        refreshConversationsForParticipants(participantList);
1865    }
1866
1867    /**
1868     * Refresh one conversation.
1869     */
1870    private static void refreshConversation(final String conversationId) {
1871        final DatabaseWrapper db = DataModel.get().getDatabase();
1872
1873        db.beginTransaction();
1874        try {
1875            BugleDatabaseOperations.updateConversationNameAndAvatarInTransaction(db,
1876                    conversationId);
1877            db.setTransactionSuccessful();
1878        } finally {
1879            db.endTransaction();
1880        }
1881
1882        MessagingContentProvider.notifyParticipantsChanged(conversationId);
1883        MessagingContentProvider.notifyMessagesChanged(conversationId);
1884        MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
1885    }
1886
1887    @DoesNotRunOnMainThread
1888    public static boolean updateRowIfExists(final DatabaseWrapper db, final String table,
1889            final String rowKey, final String rowId, final ContentValues values) {
1890        Assert.isNotMainThread();
1891        final StringBuilder sb = new StringBuilder();
1892        final ArrayList<String> whereValues = new ArrayList<String>(values.size() + 1);
1893        whereValues.add(rowId);
1894
1895        for (final String key : values.keySet()) {
1896            if (sb.length() > 0) {
1897                sb.append(" OR ");
1898            }
1899            final Object value = values.get(key);
1900            sb.append(key);
1901            if (value != null) {
1902                sb.append(" IS NOT ?");
1903                whereValues.add(value.toString());
1904            } else {
1905                sb.append(" IS NOT NULL");
1906            }
1907        }
1908
1909        final String whereClause = rowKey + "=?" + " AND (" + sb.toString() + ")";
1910        final String [] whereValuesArray = whereValues.toArray(new String[whereValues.size()]);
1911        final int count = db.update(table, values, whereClause, whereValuesArray);
1912        if (count > 1) {
1913            LogUtil.w(LogUtil.BUGLE_TAG, "Updated more than 1 row " + count + "; " + table +
1914                    " for " + rowKey + " = " + rowId + " (deleted?)");
1915        }
1916        Assert.inRange(count, 0, 1);
1917        return (count >= 0);
1918    }
1919}
1920