Home | History | Annotate | Download | only in datamodel
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.messaging.datamodel;
     18 
     19 import android.content.ContentResolver;
     20 import android.content.ContentValues;
     21 import android.database.Cursor;
     22 import android.database.sqlite.SQLiteDoneException;
     23 import android.database.sqlite.SQLiteStatement;
     24 import android.net.Uri;
     25 import android.os.ParcelFileDescriptor;
     26 import android.support.v4.util.ArrayMap;
     27 import android.support.v4.util.SimpleArrayMap;
     28 import android.text.TextUtils;
     29 
     30 import com.android.messaging.Factory;
     31 import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
     32 import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns;
     33 import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
     34 import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
     35 import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
     36 import com.android.messaging.datamodel.ParticipantRefresh.ConversationParticipantsQuery;
     37 import com.android.messaging.datamodel.data.ConversationListItemData;
     38 import com.android.messaging.datamodel.data.MessageData;
     39 import com.android.messaging.datamodel.data.MessagePartData;
     40 import com.android.messaging.datamodel.data.ParticipantData;
     41 import com.android.messaging.sms.MmsUtils;
     42 import com.android.messaging.ui.UIIntents;
     43 import com.android.messaging.util.Assert;
     44 import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
     45 import com.android.messaging.util.AvatarUriUtil;
     46 import com.android.messaging.util.ContentType;
     47 import com.android.messaging.util.LogUtil;
     48 import com.android.messaging.util.OsUtil;
     49 import com.android.messaging.util.PhoneUtils;
     50 import com.android.messaging.util.UriUtil;
     51 import com.android.messaging.widget.WidgetConversationProvider;
     52 import com.google.common.annotations.VisibleForTesting;
     53 
     54 import java.io.IOException;
     55 import java.util.ArrayList;
     56 import java.util.HashSet;
     57 import java.util.List;
     58 import javax.annotation.Nullable;
     59 
     60 
     61 /**
     62  * This class manages updating our local database
     63  */
     64 public 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