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         // Fill in IS_ENTERPRISE.
    845         final boolean hasAnyEnterpriseContact =
    846                 ConversationListItemData.hasAnyEnterpriseContact(participants);
    847         values.put(ConversationColumns.IS_ENTERPRISE, hasAnyEnterpriseContact);
    848 
    849         fillParticipantData(values, participants);
    850 
    851         // Used by background thread when refreshing conversation so conversation could be deleted.
    852         updateConversationRowIfExists(dbWrapper, conversationId, values);
    853 
    854         WidgetConversationProvider.notifyConversationRenamed(Factory.get().getApplicationContext(),
    855                 conversationId);
    856     }
    857 
    858     /**
    859      * Updates a given conversation's self id.
    860      */
    861     @DoesNotRunOnMainThread
    862     public static void updateConversationSelfIdInTransaction(
    863             final DatabaseWrapper dbWrapper, final String conversationId, final String selfId) {
    864         Assert.isNotMainThread();
    865         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
    866         final ContentValues values = new ContentValues();
    867         if (addConversationSelfIdToContentValues(dbWrapper, selfId, values)) {
    868             updateConversationRowIfExists(dbWrapper, conversationId, values);
    869         }
    870     }
    871 
    872     @DoesNotRunOnMainThread
    873     public static String getConversationSelfId(final DatabaseWrapper dbWrapper,
    874             final String conversationId) {
    875         Assert.isNotMainThread();
    876         Cursor cursor = null;
    877         try {
    878             cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
    879                     new String[] { ConversationColumns.CURRENT_SELF_ID },
    880                     ConversationColumns._ID + "=?",
    881                     new String[] { conversationId },
    882                     null, null, null);
    883             Assert.inRange(cursor.getCount(), 0, 1);
    884             if (cursor.moveToFirst()) {
    885                 return cursor.getString(0);
    886             }
    887         } finally {
    888             if (cursor != null) {
    889                 cursor.close();
    890             }
    891         }
    892         return null;
    893     }
    894 
    895     /**
    896      * Frees up memory associated with phone number to participant id matching.
    897      */
    898     @DoesNotRunOnMainThread
    899     public static void clearParticipantIdCache() {
    900         Assert.isNotMainThread();
    901         synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
    902             sNormalizedPhoneNumberToParticipantIdCache.clear();
    903         }
    904     }
    905 
    906     @DoesNotRunOnMainThread
    907     public static ArrayList<String> getRecipientsForConversation(final DatabaseWrapper dbWrapper,
    908             final String conversationId) {
    909         Assert.isNotMainThread();
    910         final ArrayList<ParticipantData> participants =
    911                 getParticipantsForConversation(dbWrapper, conversationId);
    912 
    913         final ArrayList<String> recipients = new ArrayList<String>();
    914         for (final ParticipantData participant : participants) {
    915             recipients.add(participant.getSendDestination());
    916         }
    917 
    918         return recipients;
    919     }
    920 
    921     @DoesNotRunOnMainThread
    922     public static String getSmsServiceCenterForConversation(final DatabaseWrapper dbWrapper,
    923             final String conversationId) {
    924         Assert.isNotMainThread();
    925         Cursor cursor = null;
    926         try {
    927             cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
    928                     new String[] { ConversationColumns.SMS_SERVICE_CENTER },
    929                     ConversationColumns._ID + "=?",
    930                     new String[] { conversationId },
    931                     null, null, null);
    932             Assert.inRange(cursor.getCount(), 0, 1);
    933             if (cursor.moveToFirst()) {
    934                 return cursor.getString(0);
    935             }
    936         } finally {
    937             if (cursor != null) {
    938                 cursor.close();
    939             }
    940         }
    941         return null;
    942     }
    943 
    944     @DoesNotRunOnMainThread
    945     public static ParticipantData getExistingParticipant(final DatabaseWrapper dbWrapper,
    946             final String participantId) {
    947         Assert.isNotMainThread();
    948         ParticipantData participant = null;
    949         Cursor cursor = null;
    950         try {
    951             cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
    952                     ParticipantData.ParticipantsQuery.PROJECTION,
    953                     ParticipantColumns._ID + " =?",
    954                     new String[] { participantId }, null, null, null);
    955             Assert.inRange(cursor.getCount(), 0, 1);
    956             if (cursor.moveToFirst()) {
    957                 participant = ParticipantData.getFromCursor(cursor);
    958             }
    959         } finally {
    960             if (cursor != null) {
    961                 cursor.close();
    962             }
    963         }
    964 
    965         return participant;
    966     }
    967 
    968     static int getSelfSubscriptionId(final DatabaseWrapper dbWrapper,
    969             final String selfParticipantId) {
    970         final ParticipantData selfParticipant = BugleDatabaseOperations.getExistingParticipant(
    971                 dbWrapper, selfParticipantId);
    972         if (selfParticipant != null) {
    973             Assert.isTrue(selfParticipant.isSelf());
    974             return selfParticipant.getSubId();
    975         }
    976         return ParticipantData.DEFAULT_SELF_SUB_ID;
    977     }
    978 
    979     @VisibleForTesting
    980     @DoesNotRunOnMainThread
    981     public static ArrayList<ParticipantData> getParticipantsForConversation(
    982             final DatabaseWrapper dbWrapper, final String conversationId) {
    983         Assert.isNotMainThread();
    984         final ArrayList<ParticipantData> participants =
    985                 new ArrayList<ParticipantData>();
    986         Cursor cursor = null;
    987         try {
    988             cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
    989                     ParticipantData.ParticipantsQuery.PROJECTION,
    990                     ParticipantColumns._ID + " IN ( " + "SELECT "
    991                             + ConversationParticipantsColumns.PARTICIPANT_ID + " AS "
    992                             + ParticipantColumns._ID
    993                             + " FROM " + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE
    994                             + " WHERE " + ConversationParticipantsColumns.CONVERSATION_ID + " =? )",
    995                             new String[] { conversationId }, null, null, null);
    996 
    997             while (cursor.moveToNext()) {
    998                 participants.add(ParticipantData.getFromCursor(cursor));
    999             }
   1000         } finally {
   1001             if (cursor != null) {
   1002                 cursor.close();
   1003             }
   1004         }
   1005 
   1006         return participants;
   1007     }
   1008 
   1009     @DoesNotRunOnMainThread
   1010     public static MessageData readMessage(final DatabaseWrapper dbWrapper, final String messageId) {
   1011         Assert.isNotMainThread();
   1012         final MessageData message = readMessageData(dbWrapper, messageId);
   1013         if (message != null) {
   1014             readMessagePartsData(dbWrapper, message, false);
   1015         }
   1016         return message;
   1017     }
   1018 
   1019     @VisibleForTesting
   1020     static MessagePartData readMessagePartData(final DatabaseWrapper dbWrapper,
   1021             final String partId) {
   1022         MessagePartData messagePartData = null;
   1023         Cursor cursor = null;
   1024         try {
   1025             cursor = dbWrapper.query(DatabaseHelper.PARTS_TABLE,
   1026                     MessagePartData.getProjection(), PartColumns._ID + "=?",
   1027                     new String[] { partId }, null, null, null);
   1028             Assert.inRange(cursor.getCount(), 0, 1);
   1029             if (cursor.moveToFirst()) {
   1030                 messagePartData = MessagePartData.createFromCursor(cursor);
   1031             }
   1032         } finally {
   1033             if (cursor != null) {
   1034                 cursor.close();
   1035             }
   1036         }
   1037         return messagePartData;
   1038     }
   1039 
   1040     @DoesNotRunOnMainThread
   1041     public static MessageData readMessageData(final DatabaseWrapper dbWrapper,
   1042             final Uri smsMessageUri) {
   1043         Assert.isNotMainThread();
   1044         MessageData message = null;
   1045         Cursor cursor = null;
   1046         try {
   1047             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
   1048                     MessageData.getProjection(), MessageColumns.SMS_MESSAGE_URI + "=?",
   1049                     new String[] { smsMessageUri.toString() }, null, null, null);
   1050             Assert.inRange(cursor.getCount(), 0, 1);
   1051             if (cursor.moveToFirst()) {
   1052                 message = new MessageData();
   1053                 message.bind(cursor);
   1054             }
   1055         } finally {
   1056             if (cursor != null) {
   1057                 cursor.close();
   1058             }
   1059         }
   1060         return message;
   1061     }
   1062 
   1063     @DoesNotRunOnMainThread
   1064     public static MessageData readMessageData(final DatabaseWrapper dbWrapper,
   1065             final String messageId) {
   1066         Assert.isNotMainThread();
   1067         MessageData message = null;
   1068         Cursor cursor = null;
   1069         try {
   1070             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
   1071                     MessageData.getProjection(), MessageColumns._ID + "=?",
   1072                     new String[] { messageId }, null, null, null);
   1073             Assert.inRange(cursor.getCount(), 0, 1);
   1074             if (cursor.moveToFirst()) {
   1075                 message = new MessageData();
   1076                 message.bind(cursor);
   1077             }
   1078         } finally {
   1079             if (cursor != null) {
   1080                 cursor.close();
   1081             }
   1082         }
   1083         return message;
   1084     }
   1085 
   1086     /**
   1087      * Read all the parts for a message
   1088      * @param dbWrapper database
   1089      * @param message read parts for this message
   1090      * @param checkAttachmentFilesExist check each attachment file and only include if file exists
   1091      */
   1092     private static void readMessagePartsData(final DatabaseWrapper dbWrapper,
   1093             final MessageData message, final boolean checkAttachmentFilesExist) {
   1094         final ContentResolver contentResolver =
   1095                 Factory.get().getApplicationContext().getContentResolver();
   1096         Cursor cursor = null;
   1097         try {
   1098             cursor = dbWrapper.query(DatabaseHelper.PARTS_TABLE,
   1099                     MessagePartData.getProjection(), PartColumns.MESSAGE_ID + "=?",
   1100                     new String[] { message.getMessageId() }, null, null, null);
   1101             while (cursor.moveToNext()) {
   1102                 final MessagePartData messagePartData = MessagePartData.createFromCursor(cursor);
   1103                 if (checkAttachmentFilesExist && messagePartData.isAttachment() &&
   1104                         !UriUtil.isBugleAppResource(messagePartData.getContentUri())) {
   1105                     try {
   1106                         // Test that the file exists before adding the attachment to the draft
   1107                         final ParcelFileDescriptor fileDescriptor =
   1108                                 contentResolver.openFileDescriptor(
   1109                                         messagePartData.getContentUri(), "r");
   1110                         if (fileDescriptor != null) {
   1111                             fileDescriptor.close();
   1112                             message.addPart(messagePartData);
   1113                         }
   1114                     } catch (final IOException e) {
   1115                         // The attachment's temp storage no longer exists, just ignore the file
   1116                     } catch (final SecurityException e) {
   1117                         // Likely thrown by openFileDescriptor due to an expired access grant.
   1118                         if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
   1119                             LogUtil.d(LogUtil.BUGLE_TAG, "uri: " + messagePartData.getContentUri());
   1120                         }
   1121                     }
   1122                 } else {
   1123                     message.addPart(messagePartData);
   1124                 }
   1125             }
   1126         } finally {
   1127             if (cursor != null) {
   1128                 cursor.close();
   1129             }
   1130         }
   1131     }
   1132 
   1133     /**
   1134      * Write a message part to our local database
   1135      *
   1136      * @param dbWrapper     The database
   1137      * @param messagePart   The message part to insert
   1138      * @return The row id of the newly inserted part
   1139      */
   1140     static String insertNewMessagePartInTransaction(final DatabaseWrapper dbWrapper,
   1141             final MessagePartData messagePart, final String conversationId) {
   1142         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
   1143         Assert.isTrue(!TextUtils.isEmpty(messagePart.getMessageId()));
   1144 
   1145         // Insert a new part row
   1146         final SQLiteStatement insert = messagePart.getInsertStatement(dbWrapper, conversationId);
   1147         final long rowNumber = insert.executeInsert();
   1148 
   1149         Assert.inRange(rowNumber, 0, Long.MAX_VALUE);
   1150         final String partId = Long.toString(rowNumber);
   1151 
   1152         // Update the part id
   1153         messagePart.updatePartId(partId);
   1154 
   1155         return partId;
   1156     }
   1157 
   1158     /**
   1159      * Insert a message and its parts into the table
   1160      */
   1161     @DoesNotRunOnMainThread
   1162     public static void insertNewMessageInTransaction(final DatabaseWrapper dbWrapper,
   1163             final MessageData message) {
   1164         Assert.isNotMainThread();
   1165         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
   1166 
   1167         // Insert message row
   1168         final SQLiteStatement insert = message.getInsertStatement(dbWrapper);
   1169         final long rowNumber = insert.executeInsert();
   1170 
   1171         Assert.inRange(rowNumber, 0, Long.MAX_VALUE);
   1172         final String messageId = Long.toString(rowNumber);
   1173         message.updateMessageId(messageId);
   1174         //  Insert new parts
   1175         for (final MessagePartData messagePart : message.getParts()) {
   1176             messagePart.updateMessageId(messageId);
   1177             insertNewMessagePartInTransaction(dbWrapper, messagePart, message.getConversationId());
   1178         }
   1179     }
   1180 
   1181     /**
   1182      * Update a message and add its parts into the table
   1183      */
   1184     @DoesNotRunOnMainThread
   1185     public static void updateMessageInTransaction(final DatabaseWrapper dbWrapper,
   1186             final MessageData message) {
   1187         Assert.isNotMainThread();
   1188         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
   1189         final String messageId = message.getMessageId();
   1190         // Check message still exists (sms sync or delete might have purged it)
   1191         final MessageData current = BugleDatabaseOperations.readMessage(dbWrapper, messageId);
   1192         if (current != null) {
   1193             // Delete existing message parts)
   1194             deletePartsForMessage(dbWrapper, message.getMessageId());
   1195             //  Insert new parts
   1196             for (final MessagePartData messagePart : message.getParts()) {
   1197                 messagePart.updatePartId(null);
   1198                 messagePart.updateMessageId(message.getMessageId());
   1199                 insertNewMessagePartInTransaction(dbWrapper, messagePart,
   1200                         message.getConversationId());
   1201             }
   1202             //  Update message row
   1203             final ContentValues values = new ContentValues();
   1204             message.populate(values);
   1205             updateMessageRowIfExists(dbWrapper, message.getMessageId(), values);
   1206         }
   1207     }
   1208 
   1209     @DoesNotRunOnMainThread
   1210     public static void updateMessageAndPartsInTransaction(final DatabaseWrapper dbWrapper,
   1211             final MessageData message, final List<MessagePartData> partsToUpdate) {
   1212         Assert.isNotMainThread();
   1213         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
   1214         final ContentValues values = new ContentValues();
   1215         for (final MessagePartData messagePart : partsToUpdate) {
   1216             values.clear();
   1217             messagePart.populate(values);
   1218             updatePartRowIfExists(dbWrapper, messagePart.getPartId(), values);
   1219         }
   1220         values.clear();
   1221         message.populate(values);
   1222         updateMessageRowIfExists(dbWrapper, message.getMessageId(), values);
   1223     }
   1224 
   1225     /**
   1226      * Delete all parts for a message
   1227      */
   1228     static void deletePartsForMessage(final DatabaseWrapper dbWrapper,
   1229             final String messageId) {
   1230         final int cnt = dbWrapper.delete(DatabaseHelper.PARTS_TABLE,
   1231                 PartColumns.MESSAGE_ID + " =?",
   1232                 new String[] { messageId });
   1233         Assert.inRange(cnt, 0, Integer.MAX_VALUE);
   1234     }
   1235 
   1236     /**
   1237      * Delete one message and update the conversation (if necessary).
   1238      *
   1239      * @return number of rows deleted (should be 1 or 0).
   1240      */
   1241     @DoesNotRunOnMainThread
   1242     public static int deleteMessage(final DatabaseWrapper dbWrapper, final String messageId) {
   1243         Assert.isNotMainThread();
   1244         dbWrapper.beginTransaction();
   1245         try {
   1246             // Read message to find out which conversation it is in
   1247             final MessageData message = BugleDatabaseOperations.readMessage(dbWrapper, messageId);
   1248 
   1249             int count = 0;
   1250             if (message != null) {
   1251                 final String conversationId = message.getConversationId();
   1252                 // Delete message
   1253                 count = dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
   1254                         MessageColumns._ID + "=?", new String[] { messageId });
   1255 
   1256                 if (!deleteConversationIfEmptyInTransaction(dbWrapper, conversationId)) {
   1257                     // TODO: Should we leave the conversation sort timestamp alone?
   1258                     refreshConversationMetadataInTransaction(dbWrapper, conversationId,
   1259                             false/* shouldAutoSwitchSelfId */, false/*archived*/);
   1260                 }
   1261             }
   1262             dbWrapper.setTransactionSuccessful();
   1263             return count;
   1264         } finally {
   1265             dbWrapper.endTransaction();
   1266         }
   1267     }
   1268 
   1269     /**
   1270      * Deletes the conversation if there are zero non-draft messages left.
   1271      * <p>
   1272      * This is necessary because the telephony database has a trigger that deletes threads after
   1273      * their last message is deleted. We need to ensure that if a thread goes away, we also delete
   1274      * the conversation in Bugle. We don't store draft messages in telephony, so we ignore those
   1275      * when querying for the # of messages in the conversation.
   1276      *
   1277      * @return true if the conversation was deleted
   1278      */
   1279     @DoesNotRunOnMainThread
   1280     public static boolean deleteConversationIfEmptyInTransaction(final DatabaseWrapper dbWrapper,
   1281             final String conversationId) {
   1282         Assert.isNotMainThread();
   1283         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
   1284         Cursor cursor = null;
   1285         try {
   1286             // TODO: The refreshConversationMetadataInTransaction method below uses this
   1287             // same query; maybe they should share this logic?
   1288 
   1289             // Check to see if there are any (non-draft) messages in the conversation
   1290             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
   1291                     REFRESH_CONVERSATION_MESSAGE_PROJECTION,
   1292                     MessageColumns.CONVERSATION_ID + "=? AND " +
   1293                     MessageColumns.STATUS + "!=" + MessageData.BUGLE_STATUS_OUTGOING_DRAFT,
   1294                     new String[] { conversationId }, null, null,
   1295                     MessageColumns.RECEIVED_TIMESTAMP + " DESC", "1" /* limit */);
   1296             if (cursor.getCount() == 0) {
   1297                 dbWrapper.delete(DatabaseHelper.CONVERSATIONS_TABLE,
   1298                         ConversationColumns._ID + "=?", new String[] { conversationId });
   1299                 LogUtil.i(TAG,
   1300                         "BugleDatabaseOperations: Deleted empty conversation " + conversationId);
   1301                 return true;
   1302             } else {
   1303                 return false;
   1304             }
   1305         } finally {
   1306             if (cursor != null) {
   1307                 cursor.close();
   1308             }
   1309         }
   1310     }
   1311 
   1312     private static final String[] REFRESH_CONVERSATION_MESSAGE_PROJECTION = new String[] {
   1313         MessageColumns._ID,
   1314         MessageColumns.RECEIVED_TIMESTAMP,
   1315         MessageColumns.SENDER_PARTICIPANT_ID
   1316     };
   1317 
   1318     /**
   1319      * Update conversation snippet, timestamp and optionally self id to match latest message in
   1320      * conversation.
   1321      */
   1322     @DoesNotRunOnMainThread
   1323     public static void refreshConversationMetadataInTransaction(final DatabaseWrapper dbWrapper,
   1324             final String conversationId, final boolean shouldAutoSwitchSelfId,
   1325             boolean keepArchived) {
   1326         Assert.isNotMainThread();
   1327         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
   1328         Cursor cursor = null;
   1329         try {
   1330             // Check to see if there are any (non-draft) messages in the conversation
   1331             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
   1332                     REFRESH_CONVERSATION_MESSAGE_PROJECTION,
   1333                     MessageColumns.CONVERSATION_ID + "=? AND " +
   1334                     MessageColumns.STATUS + "!=" + MessageData.BUGLE_STATUS_OUTGOING_DRAFT,
   1335                     new String[] { conversationId }, null, null,
   1336                     MessageColumns.RECEIVED_TIMESTAMP + " DESC", "1" /* limit */);
   1337 
   1338             if (cursor.moveToFirst()) {
   1339                 // Refresh latest message in conversation
   1340                 final String latestMessageId = cursor.getString(0);
   1341                 final long latestMessageTimestamp = cursor.getLong(1);
   1342                 final String senderParticipantId = cursor.getString(2);
   1343                 final boolean senderBlocked = isBlockedParticipant(dbWrapper, senderParticipantId);
   1344                 updateConversationMetadataInTransaction(dbWrapper, conversationId,
   1345                         latestMessageId, latestMessageTimestamp, senderBlocked || keepArchived,
   1346                         shouldAutoSwitchSelfId);
   1347             }
   1348         } finally {
   1349             if (cursor != null) {
   1350                 cursor.close();
   1351             }
   1352         }
   1353     }
   1354 
   1355     /**
   1356      * When moving/removing an existing message update conversation metadata if necessary
   1357      * @param dbWrapper      db wrapper
   1358      * @param conversationId conversation to modify
   1359      * @param messageId      message that is leaving the conversation
   1360      * @param shouldAutoSwitchSelfId should we try to auto-switch the conversation's self-id as a
   1361      *        result of this call when we see a new latest message?
   1362      * @param keepArchived   should we keep the conversation archived despite refresh
   1363      */
   1364     @DoesNotRunOnMainThread
   1365     public static void maybeRefreshConversationMetadataInTransaction(
   1366             final DatabaseWrapper dbWrapper, final String conversationId, final String messageId,
   1367             final boolean shouldAutoSwitchSelfId, final boolean keepArchived) {
   1368         Assert.isNotMainThread();
   1369         boolean refresh = true;
   1370         if (!TextUtils.isEmpty(messageId)) {
   1371             refresh = false;
   1372             // Look for an existing conversation in the db with this conversation id
   1373             Cursor cursor = null;
   1374             try {
   1375                 cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
   1376                         new String[] { ConversationColumns.LATEST_MESSAGE_ID },
   1377                         ConversationColumns._ID + "=?",
   1378                         new String[] { conversationId },
   1379                         null, null, null);
   1380                 Assert.inRange(cursor.getCount(), 0, 1);
   1381                 if (cursor.moveToFirst()) {
   1382                     refresh = TextUtils.equals(cursor.getString(0), messageId);
   1383                 }
   1384             } finally {
   1385                 if (cursor != null) {
   1386                     cursor.close();
   1387                 }
   1388             }
   1389         }
   1390         if (refresh) {
   1391             // TODO: I think it is okay to delete the conversation if it is empty...
   1392             refreshConversationMetadataInTransaction(dbWrapper, conversationId,
   1393                     shouldAutoSwitchSelfId, keepArchived);
   1394         }
   1395     }
   1396 
   1397 
   1398 
   1399     // SQL statement to query latest message if for particular conversation
   1400     private static final String QUERY_CONVERSATIONS_LATEST_MESSAGE_SQL = "SELECT "
   1401             + ConversationColumns.LATEST_MESSAGE_ID + " FROM " + DatabaseHelper.CONVERSATIONS_TABLE
   1402             + " WHERE " + ConversationColumns._ID + "=? LIMIT 1";
   1403 
   1404     /**
   1405      * Note this is not thread safe so callers need to make sure they own the wrapper + statements
   1406      * while they call this and use the returned value.
   1407      */
   1408     @DoesNotRunOnMainThread
   1409     public static SQLiteStatement getQueryConversationsLatestMessageStatement(
   1410             final DatabaseWrapper db, final String conversationId) {
   1411         Assert.isNotMainThread();
   1412         final SQLiteStatement query = db.getStatementInTransaction(
   1413                 DatabaseWrapper.INDEX_QUERY_CONVERSATIONS_LATEST_MESSAGE,
   1414                 QUERY_CONVERSATIONS_LATEST_MESSAGE_SQL);
   1415         query.clearBindings();
   1416         query.bindString(1, conversationId);
   1417         return query;
   1418     }
   1419 
   1420     // SQL statement to query latest message if for particular conversation
   1421     private static final String QUERY_MESSAGES_LATEST_MESSAGE_SQL = "SELECT "
   1422             + MessageColumns._ID + " FROM " + DatabaseHelper.MESSAGES_TABLE
   1423             + " WHERE " + MessageColumns.CONVERSATION_ID + "=? ORDER BY "
   1424             + MessageColumns.RECEIVED_TIMESTAMP + " DESC LIMIT 1";
   1425 
   1426     /**
   1427      * Note this is not thread safe so callers need to make sure they own the wrapper + statements
   1428      * while they call this and use the returned value.
   1429      */
   1430     @DoesNotRunOnMainThread
   1431     public static SQLiteStatement getQueryMessagesLatestMessageStatement(
   1432             final DatabaseWrapper db, final String conversationId) {
   1433         Assert.isNotMainThread();
   1434         final SQLiteStatement query = db.getStatementInTransaction(
   1435                 DatabaseWrapper.INDEX_QUERY_MESSAGES_LATEST_MESSAGE,
   1436                 QUERY_MESSAGES_LATEST_MESSAGE_SQL);
   1437         query.clearBindings();
   1438         query.bindString(1, conversationId);
   1439         return query;
   1440     }
   1441 
   1442     /**
   1443      * Update conversation metadata if necessary
   1444      * @param dbWrapper      db wrapper
   1445      * @param conversationId conversation to modify
   1446      * @param shouldAutoSwitchSelfId should we try to auto-switch the conversation's self-id as a
   1447      *                               result of this call when we see a new latest message?
   1448      * @param keepArchived if the conversation should be kept archived
   1449      */
   1450     @DoesNotRunOnMainThread
   1451     public static void maybeRefreshConversationMetadataInTransaction(
   1452             final DatabaseWrapper dbWrapper, final String conversationId,
   1453             final boolean shouldAutoSwitchSelfId, boolean keepArchived) {
   1454         Assert.isNotMainThread();
   1455         String currentLatestMessageId = null;
   1456         String latestMessageId = null;
   1457         try {
   1458             final SQLiteStatement currentLatestMessageIdSql =
   1459                     getQueryConversationsLatestMessageStatement(dbWrapper, conversationId);
   1460             currentLatestMessageId = currentLatestMessageIdSql.simpleQueryForString();
   1461 
   1462             final SQLiteStatement latestMessageIdSql =
   1463                     getQueryMessagesLatestMessageStatement(dbWrapper, conversationId);
   1464             latestMessageId = latestMessageIdSql.simpleQueryForString();
   1465         } catch (final SQLiteDoneException e) {
   1466             LogUtil.e(TAG, "BugleDatabaseOperations: Query for latest message failed", e);
   1467         }
   1468 
   1469         if (TextUtils.isEmpty(currentLatestMessageId) ||
   1470                 !TextUtils.equals(currentLatestMessageId, latestMessageId)) {
   1471             refreshConversationMetadataInTransaction(dbWrapper, conversationId,
   1472                     shouldAutoSwitchSelfId, keepArchived);
   1473         }
   1474     }
   1475 
   1476     static boolean getConversationExists(final DatabaseWrapper dbWrapper,
   1477             final String conversationId) {
   1478         // Look for an existing conversation in the db with this conversation id
   1479         Cursor cursor = null;
   1480         try {
   1481             cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
   1482                     new String[] { /* No projection */},
   1483                     ConversationColumns._ID + "=?",
   1484                     new String[] { conversationId },
   1485                     null, null, null);
   1486             return cursor.getCount() == 1;
   1487         } finally {
   1488             if (cursor != null) {
   1489                 cursor.close();
   1490             }
   1491         }
   1492     }
   1493 
   1494     /** Preserve parts in message but clear the stored draft */
   1495     public static final int UPDATE_MODE_CLEAR_DRAFT = 1;
   1496     /** Add the message as a draft */
   1497     public static final int UPDATE_MODE_ADD_DRAFT = 2;
   1498 
   1499     /**
   1500      * Update draft message for specified conversation
   1501      * @param dbWrapper       local database (wrapped)
   1502      * @param conversationId  conversation to update
   1503      * @param message         Optional message to preserve attachments for (either as draft or for
   1504      *                        sending)
   1505      * @param updateMode      either {@link #UPDATE_MODE_CLEAR_DRAFT} or
   1506      *                        {@link #UPDATE_MODE_ADD_DRAFT}
   1507      * @return message id of newly written draft (else null)
   1508      */
   1509     @DoesNotRunOnMainThread
   1510     public static String updateDraftMessageData(final DatabaseWrapper dbWrapper,
   1511             final String conversationId, @Nullable final MessageData message,
   1512             final int updateMode) {
   1513         Assert.isNotMainThread();
   1514         Assert.notNull(conversationId);
   1515         Assert.inRange(updateMode, UPDATE_MODE_CLEAR_DRAFT, UPDATE_MODE_ADD_DRAFT);
   1516         String messageId = null;
   1517         Cursor cursor = null;
   1518         dbWrapper.beginTransaction();
   1519         try {
   1520             // Find all draft parts for the current conversation
   1521             final SimpleArrayMap<Uri, MessagePartData> currentDraftParts = new SimpleArrayMap<>();
   1522             cursor = dbWrapper.query(DatabaseHelper.DRAFT_PARTS_VIEW,
   1523                     MessagePartData.getProjection(),
   1524                     MessageColumns.CONVERSATION_ID + " =?",
   1525                     new String[] { conversationId }, null, null, null);
   1526             while (cursor.moveToNext()) {
   1527                 final MessagePartData part = MessagePartData.createFromCursor(cursor);
   1528                 if (part.isAttachment()) {
   1529                     currentDraftParts.put(part.getContentUri(), part);
   1530                 }
   1531             }
   1532             // Optionally, preserve attachments for "message"
   1533             final boolean conversationExists = getConversationExists(dbWrapper, conversationId);
   1534             if (message != null && conversationExists) {
   1535                 for (final MessagePartData part : message.getParts()) {
   1536                     if (part.isAttachment()) {
   1537                         currentDraftParts.remove(part.getContentUri());
   1538                     }
   1539                 }
   1540             }
   1541 
   1542             // Delete orphan content
   1543             for (int index = 0; index < currentDraftParts.size(); index++) {
   1544                 final MessagePartData part = currentDraftParts.valueAt(index);
   1545                 part.destroySync();
   1546             }
   1547 
   1548             // Delete existing draft (cascade deletes parts)
   1549             dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
   1550                     MessageColumns.STATUS + "=? AND " + MessageColumns.CONVERSATION_ID + "=?",
   1551                     new String[] {
   1552                         Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_DRAFT),
   1553                         conversationId
   1554                     });
   1555 
   1556             // Write new draft
   1557             if (updateMode == UPDATE_MODE_ADD_DRAFT && message != null
   1558                     && message.hasContent() && conversationExists) {
   1559                 Assert.equals(MessageData.BUGLE_STATUS_OUTGOING_DRAFT,
   1560                         message.getStatus());
   1561 
   1562                 // Now add draft to message table
   1563                 insertNewMessageInTransaction(dbWrapper, message);
   1564                 messageId = message.getMessageId();
   1565             }
   1566 
   1567             if (conversationExists) {
   1568                 updateConversationDraftSnippetAndPreviewInTransaction(
   1569                         dbWrapper, conversationId, message);
   1570 
   1571                 if (message != null && message.getSelfId() != null) {
   1572                     updateConversationSelfIdInTransaction(dbWrapper, conversationId,
   1573                             message.getSelfId());
   1574                 }
   1575             }
   1576 
   1577             dbWrapper.setTransactionSuccessful();
   1578         } finally {
   1579             dbWrapper.endTransaction();
   1580             if (cursor != null) {
   1581                 cursor.close();
   1582             }
   1583         }
   1584         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
   1585             LogUtil.v(TAG,
   1586                     "Updated draft message " + messageId + " for conversation " + conversationId);
   1587         }
   1588         return messageId;
   1589     }
   1590 
   1591     /**
   1592      * Read the first draft message associated with this conversation.
   1593      * If none present create an empty (sms) draft message.
   1594      */
   1595     @DoesNotRunOnMainThread
   1596     public static MessageData readDraftMessageData(final DatabaseWrapper dbWrapper,
   1597             final String conversationId, final String conversationSelfId) {
   1598         Assert.isNotMainThread();
   1599         MessageData message = null;
   1600         Cursor cursor = null;
   1601         dbWrapper.beginTransaction();
   1602         try {
   1603             cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
   1604                     MessageData.getProjection(),
   1605                     MessageColumns.STATUS + "=? AND " + MessageColumns.CONVERSATION_ID + "=?",
   1606                     new String[] {
   1607                         Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_DRAFT),
   1608                         conversationId
   1609                     }, null, null, null);
   1610             Assert.inRange(cursor.getCount(), 0, 1);
   1611             if (cursor.moveToFirst()) {
   1612                 message = new MessageData();
   1613                 message.bindDraft(cursor, conversationSelfId);
   1614                 readMessagePartsData(dbWrapper, message, true);
   1615                 // Disconnect draft parts from DB
   1616                 for (final MessagePartData part : message.getParts()) {
   1617                     part.updatePartId(null);
   1618                     part.updateMessageId(null);
   1619                 }
   1620                 message.updateMessageId(null);
   1621             }
   1622             dbWrapper.setTransactionSuccessful();
   1623         } finally {
   1624             dbWrapper.endTransaction();
   1625             if (cursor != null) {
   1626                 cursor.close();
   1627             }
   1628         }
   1629         return message;
   1630     }
   1631 
   1632     // Internal
   1633     private static void addParticipantToConversation(final DatabaseWrapper dbWrapper,
   1634             final ParticipantData participant, final String conversationId) {
   1635         final String participantId = getOrCreateParticipantInTransaction(dbWrapper, participant);
   1636         Assert.notNull(participantId);
   1637 
   1638         // Add the participant to the conversation participants table
   1639         final ContentValues values = new ContentValues();
   1640         values.put(ConversationParticipantsColumns.CONVERSATION_ID, conversationId);
   1641         values.put(ConversationParticipantsColumns.PARTICIPANT_ID, participantId);
   1642         dbWrapper.insert(DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE, null, values);
   1643     }
   1644 
   1645     /**
   1646      * Get string used as canonical recipient for participant cache for sub id
   1647      */
   1648     private static String getCanonicalRecipientFromSubId(final int subId) {
   1649         return "SELF(" + subId + ")";
   1650     }
   1651 
   1652     /**
   1653      * Maps from a sub id or phone number to a participant id if there is one.
   1654      *
   1655      * @return If the participant is available in our cache, or the DB, this returns the
   1656      * participant id for the given subid/phone number.  Otherwise it returns null.
   1657      */
   1658     @VisibleForTesting
   1659     private static String getParticipantId(final DatabaseWrapper dbWrapper,
   1660             final int subId, final String canonicalRecipient) {
   1661         // First check our memory cache for the participant Id
   1662         String participantId;
   1663         synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
   1664             participantId = sNormalizedPhoneNumberToParticipantIdCache.get(canonicalRecipient);
   1665         }
   1666 
   1667         if (participantId != null) {
   1668             return participantId;
   1669         }
   1670 
   1671         // This code will only be executed for incremental additions.
   1672         Cursor cursor = null;
   1673         try {
   1674             if (subId != ParticipantData.OTHER_THAN_SELF_SUB_ID) {
   1675                 // Now look for an existing participant in the db with this sub id.
   1676                 cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
   1677                         new String[] {ParticipantColumns._ID},
   1678                         ParticipantColumns.SUB_ID + "=?",
   1679                         new String[] { Integer.toString(subId) }, null, null, null);
   1680             } else {
   1681                 // Look for existing participant with this normalized phone number and no subId.
   1682                 cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
   1683                         new String[] {ParticipantColumns._ID},
   1684                         ParticipantColumns.NORMALIZED_DESTINATION + "=? AND "
   1685                                 + ParticipantColumns.SUB_ID + "=?",
   1686                                 new String[] {canonicalRecipient, Integer.toString(subId)},
   1687                                 null, null, null);
   1688             }
   1689 
   1690             if (cursor.moveToFirst()) {
   1691                 // TODO Is this assert correct for multi-sim where a new sim was put in?
   1692                 Assert.isTrue(cursor.getCount() == 1);
   1693 
   1694                 // We found an existing participant in the database
   1695                 participantId = cursor.getString(0);
   1696 
   1697                 synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
   1698                     // Add it to the cache for next time
   1699                     sNormalizedPhoneNumberToParticipantIdCache.put(canonicalRecipient,
   1700                             participantId);
   1701                 }
   1702             }
   1703         } finally {
   1704             if (cursor != null) {
   1705                 cursor.close();
   1706             }
   1707         }
   1708         return participantId;
   1709     }
   1710 
   1711     @DoesNotRunOnMainThread
   1712     public static ParticipantData getOrCreateSelf(final DatabaseWrapper dbWrapper,
   1713             final int subId) {
   1714         Assert.isNotMainThread();
   1715         ParticipantData participant = null;
   1716         dbWrapper.beginTransaction();
   1717         try {
   1718             final ParticipantData shell = ParticipantData.getSelfParticipant(subId);
   1719             final String participantId = getOrCreateParticipantInTransaction(dbWrapper, shell);
   1720             participant = getExistingParticipant(dbWrapper, participantId);
   1721             dbWrapper.setTransactionSuccessful();
   1722         } finally {
   1723             dbWrapper.endTransaction();
   1724         }
   1725         return participant;
   1726     }
   1727 
   1728     /**
   1729      * Lookup and if necessary create a new participant
   1730      * @param dbWrapper      Database wrapper
   1731      * @param participant    Participant to find/create
   1732      * @return participantId ParticipantId for existing or newly created participant
   1733      */
   1734     @DoesNotRunOnMainThread
   1735     public static String getOrCreateParticipantInTransaction(final DatabaseWrapper dbWrapper,
   1736             final ParticipantData participant) {
   1737         Assert.isNotMainThread();
   1738         Assert.isTrue(dbWrapper.getDatabase().inTransaction());
   1739         int subId = ParticipantData.OTHER_THAN_SELF_SUB_ID;
   1740         String participantId = null;
   1741         String canonicalRecipient = null;
   1742         if (participant.isSelf()) {
   1743             subId = participant.getSubId();
   1744             canonicalRecipient = getCanonicalRecipientFromSubId(subId);
   1745         } else {
   1746             canonicalRecipient = participant.getNormalizedDestination();
   1747         }
   1748         Assert.notNull(canonicalRecipient);
   1749         participantId = getParticipantId(dbWrapper, subId, canonicalRecipient);
   1750 
   1751         if (participantId != null) {
   1752             return participantId;
   1753         }
   1754 
   1755         if (!participant.isContactIdResolved()) {
   1756             // Refresh participant's name and avatar with matching contact in CP2.
   1757             ParticipantRefresh.refreshParticipant(dbWrapper, participant);
   1758         }
   1759 
   1760         // Insert the participant into the participants table
   1761         final ContentValues values = participant.toContentValues();
   1762         final long participantRow = dbWrapper.insert(DatabaseHelper.PARTICIPANTS_TABLE, null,
   1763                 values);
   1764         participantId = Long.toString(participantRow);
   1765         Assert.notNull(canonicalRecipient);
   1766 
   1767         synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
   1768             // Now that we've inserted it, add it to our cache
   1769             sNormalizedPhoneNumberToParticipantIdCache.put(canonicalRecipient, participantId);
   1770         }
   1771 
   1772         return participantId;
   1773     }
   1774 
   1775     @DoesNotRunOnMainThread
   1776     public static void updateDestination(final DatabaseWrapper dbWrapper,
   1777             final String destination, final boolean blocked) {
   1778         Assert.isNotMainThread();
   1779         final ContentValues values = new ContentValues();
   1780         values.put(ParticipantColumns.BLOCKED, blocked ? 1 : 0);
   1781         dbWrapper.update(DatabaseHelper.PARTICIPANTS_TABLE, values,
   1782                 ParticipantColumns.NORMALIZED_DESTINATION + "=? AND " +
   1783                         ParticipantColumns.SUB_ID + "=?",
   1784                 new String[] { destination, Integer.toString(
   1785                         ParticipantData.OTHER_THAN_SELF_SUB_ID) });
   1786     }
   1787 
   1788     @DoesNotRunOnMainThread
   1789     public static String getConversationFromOtherParticipantDestination(
   1790             final DatabaseWrapper db, final String otherDestination) {
   1791         Assert.isNotMainThread();
   1792         Cursor cursor = null;
   1793         try {
   1794             cursor = db.query(DatabaseHelper.CONVERSATIONS_TABLE,
   1795                     new String[] { ConversationColumns._ID },
   1796                     ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION + "=?",
   1797                     new String[] { otherDestination }, null, null, null);
   1798             Assert.inRange(cursor.getCount(), 0, 1);
   1799             if (cursor.moveToFirst()) {
   1800                 return cursor.getString(0);
   1801             }
   1802         } finally {
   1803             if (cursor != null) {
   1804                 cursor.close();
   1805             }
   1806         }
   1807         return null;
   1808     }
   1809 
   1810 
   1811     /**
   1812      * Get a list of conversations that contain any of participants specified.
   1813      */
   1814     private static HashSet<String> getConversationsForParticipants(
   1815             final ArrayList<String> participantIds) {
   1816         final DatabaseWrapper db = DataModel.get().getDatabase();
   1817         final HashSet<String> conversationIds = new HashSet<String>();
   1818 
   1819         final String selection = ConversationParticipantsColumns.PARTICIPANT_ID + "=?";
   1820         for (final String participantId : participantIds) {
   1821             final String[] selectionArgs = new String[] { participantId };
   1822             final Cursor cursor = db.query(DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE,
   1823                     ConversationParticipantsQuery.PROJECTION,
   1824                     selection, selectionArgs, null, null, null);
   1825 
   1826             if (cursor != null) {
   1827                 try {
   1828                     while (cursor.moveToNext()) {
   1829                         final String conversationId = cursor.getString(
   1830                                 ConversationParticipantsQuery.INDEX_CONVERSATION_ID);
   1831                         conversationIds.add(conversationId);
   1832                     }
   1833                 } finally {
   1834                     cursor.close();
   1835                 }
   1836             }
   1837         }
   1838 
   1839         return conversationIds;
   1840     }
   1841 
   1842     /**
   1843      * Refresh conversation names/avatars based on a list of participants that are changed.
   1844      */
   1845     @DoesNotRunOnMainThread
   1846     public static void refreshConversationsForParticipants(final ArrayList<String> participants) {
   1847         Assert.isNotMainThread();
   1848         final HashSet<String> conversationIds = getConversationsForParticipants(participants);
   1849         if (conversationIds.size() > 0) {
   1850             for (final String conversationId : conversationIds) {
   1851                 refreshConversation(conversationId);
   1852             }
   1853 
   1854             MessagingContentProvider.notifyConversationListChanged();
   1855             if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
   1856                 LogUtil.v(TAG, "Number of conversations refreshed:" + conversationIds.size());
   1857             }
   1858         }
   1859     }
   1860 
   1861     /**
   1862      * Refresh conversation names/avatars based on a changed participant.
   1863      */
   1864     @DoesNotRunOnMainThread
   1865     public static void refreshConversationsForParticipant(final String participantId) {
   1866         Assert.isNotMainThread();
   1867         final ArrayList<String> participantList = new ArrayList<String>(1);
   1868         participantList.add(participantId);
   1869         refreshConversationsForParticipants(participantList);
   1870     }
   1871 
   1872     /**
   1873      * Refresh one conversation.
   1874      */
   1875     private static void refreshConversation(final String conversationId) {
   1876         final DatabaseWrapper db = DataModel.get().getDatabase();
   1877 
   1878         db.beginTransaction();
   1879         try {
   1880             BugleDatabaseOperations.updateConversationNameAndAvatarInTransaction(db,
   1881                     conversationId);
   1882             db.setTransactionSuccessful();
   1883         } finally {
   1884             db.endTransaction();
   1885         }
   1886 
   1887         MessagingContentProvider.notifyParticipantsChanged(conversationId);
   1888         MessagingContentProvider.notifyMessagesChanged(conversationId);
   1889         MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
   1890     }
   1891 
   1892     @DoesNotRunOnMainThread
   1893     public static boolean updateRowIfExists(final DatabaseWrapper db, final String table,
   1894             final String rowKey, final String rowId, final ContentValues values) {
   1895         Assert.isNotMainThread();
   1896         final StringBuilder sb = new StringBuilder();
   1897         final ArrayList<String> whereValues = new ArrayList<String>(values.size() + 1);
   1898         whereValues.add(rowId);
   1899 
   1900         for (final String key : values.keySet()) {
   1901             if (sb.length() > 0) {
   1902                 sb.append(" OR ");
   1903             }
   1904             final Object value = values.get(key);
   1905             sb.append(key);
   1906             if (value != null) {
   1907                 sb.append(" IS NOT ?");
   1908                 whereValues.add(value.toString());
   1909             } else {
   1910                 sb.append(" IS NOT NULL");
   1911             }
   1912         }
   1913 
   1914         final String whereClause = rowKey + "=?" + " AND (" + sb.toString() + ")";
   1915         final String [] whereValuesArray = whereValues.toArray(new String[whereValues.size()]);
   1916         final int count = db.update(table, values, whereClause, whereValuesArray);
   1917         if (count > 1) {
   1918             LogUtil.w(LogUtil.BUGLE_TAG, "Updated more than 1 row " + count + "; " + table +
   1919                     " for " + rowKey + " = " + rowId + " (deleted?)");
   1920         }
   1921         Assert.inRange(count, 0, 1);
   1922         return (count >= 0);
   1923     }
   1924 }
   1925