PorcessPendingMessagesAction queues one message for sending/downloading associated with a subscription triggering current action at a time. And the ConnectivityUtil also works based on subscriptions so that pending messages can be processed regardless of other phones' state in multi-sim case. It includes cleanup code as well. Test: Manual Change-Id: Id6b4f4a0aa6a3291e7a4d8a5d3f0fbb9db3c5b86 Signed-off-by: Taesu Lee <taesu82.lee@samsung.com>
1925 lines
82 KiB
Java
1925 lines
82 KiB
Java
/*
|
|
* Copyright (C) 2015 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.messaging.datamodel;
|
|
|
|
import android.content.ContentResolver;
|
|
import android.content.ContentValues;
|
|
import android.database.Cursor;
|
|
import android.database.sqlite.SQLiteDoneException;
|
|
import android.database.sqlite.SQLiteStatement;
|
|
import android.net.Uri;
|
|
import android.os.ParcelFileDescriptor;
|
|
import androidx.collection.ArrayMap;
|
|
import androidx.collection.SimpleArrayMap;
|
|
import android.text.TextUtils;
|
|
|
|
import com.android.messaging.Factory;
|
|
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
|
|
import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns;
|
|
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
|
import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
|
|
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
|
|
import com.android.messaging.datamodel.ParticipantRefresh.ConversationParticipantsQuery;
|
|
import com.android.messaging.datamodel.data.ConversationListItemData;
|
|
import com.android.messaging.datamodel.data.MessageData;
|
|
import com.android.messaging.datamodel.data.MessagePartData;
|
|
import com.android.messaging.datamodel.data.ParticipantData;
|
|
import com.android.messaging.sms.MmsUtils;
|
|
import com.android.messaging.ui.UIIntents;
|
|
import com.android.messaging.util.Assert;
|
|
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
|
|
import com.android.messaging.util.AvatarUriUtil;
|
|
import com.android.messaging.util.ContentType;
|
|
import com.android.messaging.util.LogUtil;
|
|
import com.android.messaging.util.OsUtil;
|
|
import com.android.messaging.util.PhoneUtils;
|
|
import com.android.messaging.util.UriUtil;
|
|
import com.android.messaging.widget.WidgetConversationProvider;
|
|
import com.google.common.annotations.VisibleForTesting;
|
|
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import javax.annotation.Nullable;
|
|
|
|
|
|
/**
|
|
* This class manages updating our local database
|
|
*/
|
|
public class BugleDatabaseOperations {
|
|
|
|
private static final String TAG = LogUtil.BUGLE_DATABASE_TAG;
|
|
|
|
// Global cache of phone numbers -> participant id mapping since this call is expensive.
|
|
private static final ArrayMap<String, String> sNormalizedPhoneNumberToParticipantIdCache =
|
|
new ArrayMap<String, String>();
|
|
|
|
/**
|
|
* Convert list of recipient strings (email/phone number) into list of ConversationParticipants
|
|
*
|
|
* @param recipients The recipient list
|
|
* @param refSubId The subId used to normalize phone numbers in the recipients
|
|
*/
|
|
static ArrayList<ParticipantData> getConversationParticipantsFromRecipients(
|
|
final List<String> recipients, final int refSubId) {
|
|
// Generate a list of partially formed participants
|
|
final ArrayList<ParticipantData> participants = new
|
|
ArrayList<ParticipantData>();
|
|
|
|
if (recipients != null) {
|
|
for (final String recipient : recipients) {
|
|
participants.add(ParticipantData.getFromRawPhoneBySimLocale(recipient, refSubId));
|
|
}
|
|
}
|
|
return participants;
|
|
}
|
|
|
|
/**
|
|
* Sanitize a given list of conversation participants by de-duping and stripping out self
|
|
* phone number in group conversation.
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static void sanitizeConversationParticipants(final List<ParticipantData> participants) {
|
|
Assert.isNotMainThread();
|
|
if (participants.size() > 0) {
|
|
// First remove redundant phone numbers
|
|
final HashSet<String> recipients = new HashSet<String>();
|
|
for (int i = participants.size() - 1; i >= 0; i--) {
|
|
final String recipient = participants.get(i).getNormalizedDestination();
|
|
if (!recipients.contains(recipient)) {
|
|
recipients.add(recipient);
|
|
} else {
|
|
participants.remove(i);
|
|
}
|
|
}
|
|
if (participants.size() > 1) {
|
|
// Remove self phone number from group conversation.
|
|
final HashSet<String> selfNumbers =
|
|
PhoneUtils.getDefault().getNormalizedSelfNumbers();
|
|
int removed = 0;
|
|
// Do this two-pass scan to avoid unnecessary memory allocation.
|
|
// Prescan to count the self numbers in the list
|
|
for (final ParticipantData p : participants) {
|
|
if (selfNumbers.contains(p.getNormalizedDestination())) {
|
|
removed++;
|
|
}
|
|
}
|
|
// If all are self numbers, maybe that's what the user wants, just leave
|
|
// the participants as is. Otherwise, do another scan to remove self numbers.
|
|
if (removed < participants.size()) {
|
|
for (int i = participants.size() - 1; i >= 0; i--) {
|
|
final String recipient = participants.get(i).getNormalizedDestination();
|
|
if (selfNumbers.contains(recipient)) {
|
|
participants.remove(i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert list of ConversationParticipants into recipient strings (email/phone number)
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static ArrayList<String> getRecipientsFromConversationParticipants(
|
|
final List<ParticipantData> participants) {
|
|
Assert.isNotMainThread();
|
|
// First find the thread id for this list of participants.
|
|
final ArrayList<String> recipients = new ArrayList<String>();
|
|
|
|
for (final ParticipantData participant : participants) {
|
|
recipients.add(participant.getSendDestination());
|
|
}
|
|
return recipients;
|
|
}
|
|
|
|
/**
|
|
* Get or create a conversation based on the message's thread id
|
|
*
|
|
* NOTE: There are phones on which you can't get the recipients from the thread id for SMS
|
|
* until you have a message, so use getOrCreateConversationFromRecipient instead.
|
|
*
|
|
* TODO: Should this be in MMS/SMS code?
|
|
*
|
|
* @param db the database
|
|
* @param threadId The message's thread
|
|
* @param senderBlocked Flag whether sender of message is in blocked people list
|
|
* @param refSubId The reference subId for canonicalize phone numbers
|
|
* @return conversationId
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static String getOrCreateConversationFromThreadId(final DatabaseWrapper db,
|
|
final long threadId, final boolean senderBlocked, final int refSubId) {
|
|
Assert.isNotMainThread();
|
|
final List<String> recipients = MmsUtils.getRecipientsByThread(threadId);
|
|
final ArrayList<ParticipantData> participants =
|
|
getConversationParticipantsFromRecipients(recipients, refSubId);
|
|
|
|
return getOrCreateConversation(db, threadId, senderBlocked, participants, false, false,
|
|
null);
|
|
}
|
|
|
|
/**
|
|
* Get or create a conversation based on provided recipient
|
|
*
|
|
* @param db the database
|
|
* @param threadId The message's thread
|
|
* @param senderBlocked Flag whether sender of message is in blocked people list
|
|
* @param recipient recipient for thread
|
|
* @return conversationId
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static String getOrCreateConversationFromRecipient(final DatabaseWrapper db,
|
|
final long threadId, final boolean senderBlocked, final ParticipantData recipient) {
|
|
Assert.isNotMainThread();
|
|
final ArrayList<ParticipantData> recipients = new ArrayList<>(1);
|
|
recipients.add(recipient);
|
|
return getOrCreateConversation(db, threadId, senderBlocked, recipients, false, false, null);
|
|
}
|
|
|
|
/**
|
|
* Get or create a conversation based on provided participants
|
|
*
|
|
* @param db the database
|
|
* @param threadId The message's thread
|
|
* @param archived Flag whether the conversation should be created archived
|
|
* @param participants list of conversation participants
|
|
* @param noNotification If notification should be disabled
|
|
* @param noVibrate If vibrate on notification should be disabled
|
|
* @param soundUri If there is custom sound URI
|
|
* @return a conversation id
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static String getOrCreateConversation(final DatabaseWrapper db, final long threadId,
|
|
final boolean archived, final ArrayList<ParticipantData> participants,
|
|
boolean noNotification, boolean noVibrate, String soundUri) {
|
|
Assert.isNotMainThread();
|
|
|
|
// Check to see if this conversation is already in out local db cache
|
|
String conversationId = BugleDatabaseOperations.getExistingConversation(db, threadId,
|
|
false);
|
|
|
|
if (conversationId == null) {
|
|
final String conversationName = ConversationListItemData.generateConversationName(
|
|
participants);
|
|
|
|
// Create the conversation with the default self participant which always maps to
|
|
// the system default subscription.
|
|
final ParticipantData self = ParticipantData.getSelfParticipant(
|
|
ParticipantData.DEFAULT_SELF_SUB_ID);
|
|
|
|
db.beginTransaction();
|
|
try {
|
|
// Look up the "self" participantId (creating if necessary)
|
|
final String selfId =
|
|
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
|
|
// Create a new conversation
|
|
conversationId = BugleDatabaseOperations.createConversationInTransaction(
|
|
db, threadId, conversationName, selfId, participants, archived,
|
|
noNotification, noVibrate, soundUri);
|
|
db.setTransactionSuccessful();
|
|
} finally {
|
|
db.endTransaction();
|
|
}
|
|
}
|
|
|
|
return conversationId;
|
|
}
|
|
|
|
/**
|
|
* Get a conversation from the local DB based on the message's thread id.
|
|
*
|
|
* @param dbWrapper The database
|
|
* @param threadId The message's thread in the SMS database
|
|
* @param senderBlocked Flag whether sender of message is in blocked people list
|
|
* @return The existing conversation id or null
|
|
*/
|
|
@VisibleForTesting
|
|
@DoesNotRunOnMainThread
|
|
public static String getExistingConversation(final DatabaseWrapper dbWrapper,
|
|
final long threadId, final boolean senderBlocked) {
|
|
Assert.isNotMainThread();
|
|
String conversationId = null;
|
|
|
|
Cursor cursor = null;
|
|
try {
|
|
// Look for an existing conversation in the db with this thread id
|
|
cursor = dbWrapper.rawQuery("SELECT " + ConversationColumns._ID
|
|
+ " FROM " + DatabaseHelper.CONVERSATIONS_TABLE
|
|
+ " WHERE " + ConversationColumns.SMS_THREAD_ID + "=" + threadId,
|
|
null);
|
|
|
|
if (cursor.moveToFirst()) {
|
|
Assert.isTrue(cursor.getCount() == 1);
|
|
conversationId = cursor.getString(0);
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
return conversationId;
|
|
}
|
|
|
|
/**
|
|
* Get the thread id for an existing conversation from the local DB.
|
|
*
|
|
* @param dbWrapper The database
|
|
* @param conversationId The conversation to look up thread for
|
|
* @return The thread id. Returns -1 if the conversation was not found or if it was found
|
|
* but the thread column was NULL.
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static long getThreadId(final DatabaseWrapper dbWrapper, final String conversationId) {
|
|
Assert.isNotMainThread();
|
|
long threadId = -1;
|
|
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
|
|
new String[] { ConversationColumns.SMS_THREAD_ID },
|
|
ConversationColumns._ID + " =?",
|
|
new String[] { conversationId },
|
|
null, null, null);
|
|
|
|
if (cursor.moveToFirst()) {
|
|
Assert.isTrue(cursor.getCount() == 1);
|
|
if (!cursor.isNull(0)) {
|
|
threadId = cursor.getLong(0);
|
|
}
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
return threadId;
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static boolean isBlockedDestination(final DatabaseWrapper db, final String destination) {
|
|
Assert.isNotMainThread();
|
|
return isBlockedParticipant(db, destination, ParticipantColumns.NORMALIZED_DESTINATION);
|
|
}
|
|
|
|
static boolean isBlockedParticipant(final DatabaseWrapper db, final String participantId) {
|
|
return isBlockedParticipant(db, participantId, ParticipantColumns._ID);
|
|
}
|
|
|
|
static boolean isBlockedParticipant(final DatabaseWrapper db, final String value,
|
|
final String column) {
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE,
|
|
new String[] { ParticipantColumns.BLOCKED },
|
|
column + "=? AND " + ParticipantColumns.SUB_ID + "=?",
|
|
new String[] { value,
|
|
Integer.toString(ParticipantData.OTHER_THAN_SELF_SUB_ID) },
|
|
null, null, null);
|
|
|
|
Assert.inRange(cursor.getCount(), 0, 1);
|
|
if (cursor.moveToFirst()) {
|
|
return cursor.getInt(0) == 1;
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
return false; // if there's no row, it's not blocked :-)
|
|
}
|
|
|
|
/**
|
|
* Create a conversation in the local DB based on the message's thread id.
|
|
*
|
|
* It's up to the caller to make sure that this is all inside a transaction. It will return
|
|
* null if it's not in the local DB.
|
|
*
|
|
* @param dbWrapper The database
|
|
* @param threadId The message's thread
|
|
* @param selfId The selfId to make default for this conversation
|
|
* @param archived Flag whether the conversation should be created archived
|
|
* @param noNotification If notification should be disabled
|
|
* @param noVibrate If vibrate on notification should be disabled
|
|
* @param soundUri The customized sound
|
|
* @return The existing conversation id or new conversation id
|
|
*/
|
|
static String createConversationInTransaction(final DatabaseWrapper dbWrapper,
|
|
final long threadId, final String conversationName, final String selfId,
|
|
final List<ParticipantData> participants, final boolean archived,
|
|
boolean noNotification, boolean noVibrate, String soundUri) {
|
|
// We want conversation and participant creation to be atomic
|
|
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
|
boolean hasEmailAddress = false;
|
|
for (final ParticipantData participant : participants) {
|
|
Assert.isTrue(!participant.isSelf());
|
|
if (participant.isEmail()) {
|
|
hasEmailAddress = true;
|
|
}
|
|
}
|
|
|
|
// TODO : Conversations state - normal vs. archived
|
|
|
|
// Insert a new local conversation for this thread id
|
|
final ContentValues values = new ContentValues();
|
|
values.put(ConversationColumns.SMS_THREAD_ID, threadId);
|
|
// Start with conversation hidden - sending a message or saving a draft will change that
|
|
values.put(ConversationColumns.SORT_TIMESTAMP, 0L);
|
|
values.put(ConversationColumns.CURRENT_SELF_ID, selfId);
|
|
values.put(ConversationColumns.PARTICIPANT_COUNT, participants.size());
|
|
values.put(ConversationColumns.INCLUDE_EMAIL_ADDRESS, (hasEmailAddress ? 1 : 0));
|
|
if (archived) {
|
|
values.put(ConversationColumns.ARCHIVE_STATUS, 1);
|
|
}
|
|
if (noNotification) {
|
|
values.put(ConversationColumns.NOTIFICATION_ENABLED, 0);
|
|
}
|
|
if (noVibrate) {
|
|
values.put(ConversationColumns.NOTIFICATION_VIBRATION, 0);
|
|
}
|
|
if (!TextUtils.isEmpty(soundUri)) {
|
|
values.put(ConversationColumns.NOTIFICATION_SOUND_URI, soundUri);
|
|
}
|
|
|
|
fillParticipantData(values, participants);
|
|
|
|
final long conversationRowId = dbWrapper.insert(DatabaseHelper.CONVERSATIONS_TABLE, null,
|
|
values);
|
|
|
|
Assert.isTrue(conversationRowId != -1);
|
|
if (conversationRowId == -1) {
|
|
LogUtil.e(TAG, "BugleDatabaseOperations : failed to insert conversation into table");
|
|
return null;
|
|
}
|
|
|
|
final String conversationId = Long.toString(conversationRowId);
|
|
|
|
// Make sure that participants are added for this conversation
|
|
for (final ParticipantData participant : participants) {
|
|
// TODO: Use blocking information
|
|
addParticipantToConversation(dbWrapper, participant, conversationId);
|
|
}
|
|
|
|
// Now fully resolved participants available can update conversation name / avatar.
|
|
// b/16437575: We cannot use the participants directly, but instead have to call
|
|
// getParticipantsForConversation() to retrieve the actual participants. This is needed
|
|
// because the call to addParticipantToConversation() won't fill up the ParticipantData
|
|
// if the participant already exists in the participant table. For example, say you have
|
|
// an existing conversation with John. Now if you create a new group conversation with
|
|
// Jeff & John with only their phone numbers, then when we try to add John's number to the
|
|
// group conversation, we see that he's already in the participant table, therefore we
|
|
// short-circuit any steps to actually fill out the ParticipantData for John other than
|
|
// just returning his participant id. Eventually, the ParticipantData we have is still the
|
|
// raw data with just the phone number. getParticipantsForConversation(), on the other
|
|
// hand, will fill out all the info for each participant from the participants table.
|
|
updateConversationNameAndAvatarInTransaction(dbWrapper, conversationId,
|
|
getParticipantsForConversation(dbWrapper, conversationId));
|
|
|
|
return conversationId;
|
|
}
|
|
|
|
private static void fillParticipantData(final ContentValues values,
|
|
final List<ParticipantData> participants) {
|
|
if (participants != null && !participants.isEmpty()) {
|
|
final Uri avatarUri = AvatarUriUtil.createAvatarUri(participants);
|
|
values.put(ConversationColumns.ICON, avatarUri.toString());
|
|
|
|
long contactId;
|
|
String lookupKey;
|
|
String destination;
|
|
if (participants.size() == 1) {
|
|
final ParticipantData firstParticipant = participants.get(0);
|
|
contactId = firstParticipant.getContactId();
|
|
lookupKey = firstParticipant.getLookupKey();
|
|
destination = firstParticipant.getNormalizedDestination();
|
|
} else {
|
|
contactId = 0;
|
|
lookupKey = null;
|
|
destination = null;
|
|
}
|
|
|
|
values.put(ConversationColumns.PARTICIPANT_CONTACT_ID, contactId);
|
|
values.put(ConversationColumns.PARTICIPANT_LOOKUP_KEY, lookupKey);
|
|
values.put(ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION, destination);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete conversation and associated messages/parts
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static boolean deleteConversation(final DatabaseWrapper dbWrapper,
|
|
final String conversationId, final long cutoffTimestamp) {
|
|
Assert.isNotMainThread();
|
|
dbWrapper.beginTransaction();
|
|
boolean conversationDeleted = false;
|
|
boolean conversationMessagesDeleted = false;
|
|
try {
|
|
// Delete existing messages
|
|
if (cutoffTimestamp == Long.MAX_VALUE) {
|
|
// Delete parts and messages
|
|
dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
|
|
MessageColumns.CONVERSATION_ID + "=?", new String[] { conversationId });
|
|
conversationMessagesDeleted = true;
|
|
} else {
|
|
// Delete all messages prior to the cutoff
|
|
dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
|
|
MessageColumns.CONVERSATION_ID + "=? AND "
|
|
+ MessageColumns.RECEIVED_TIMESTAMP + "<=?",
|
|
new String[] { conversationId, Long.toString(cutoffTimestamp) });
|
|
|
|
// Delete any draft message. The delete above may not always include the draft,
|
|
// because under certain scenarios (e.g. sending messages in progress), the draft
|
|
// timestamp can be larger than the cutoff time, which is generally the conversation
|
|
// sort timestamp. Because of how the sms/mms provider works on some newer
|
|
// devices, it's important that we never delete all the messages in a conversation
|
|
// without also deleting the conversation itself (see b/20262204 for details).
|
|
dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
|
|
MessageColumns.STATUS + "=? AND " + MessageColumns.CONVERSATION_ID + "=?",
|
|
new String[] {
|
|
Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_DRAFT),
|
|
conversationId
|
|
});
|
|
|
|
// Check to see if there are any messages left in the conversation
|
|
final long count = dbWrapper.queryNumEntries(DatabaseHelper.MESSAGES_TABLE,
|
|
MessageColumns.CONVERSATION_ID + "=?", new String[] { conversationId });
|
|
conversationMessagesDeleted = (count == 0);
|
|
|
|
// Log detail information if there are still messages left in the conversation
|
|
if (!conversationMessagesDeleted) {
|
|
final long maxTimestamp =
|
|
getConversationMaxTimestamp(dbWrapper, conversationId);
|
|
LogUtil.w(TAG, "BugleDatabaseOperations:"
|
|
+ " cannot delete all messages in a conversation"
|
|
+ ", after deletion: count=" + count
|
|
+ ", max timestamp=" + maxTimestamp
|
|
+ ", cutoff timestamp=" + cutoffTimestamp);
|
|
}
|
|
}
|
|
|
|
if (conversationMessagesDeleted) {
|
|
// Delete conversation row
|
|
final int count = dbWrapper.delete(DatabaseHelper.CONVERSATIONS_TABLE,
|
|
ConversationColumns._ID + "=?", new String[] { conversationId });
|
|
conversationDeleted = (count > 0);
|
|
}
|
|
dbWrapper.setTransactionSuccessful();
|
|
} finally {
|
|
dbWrapper.endTransaction();
|
|
}
|
|
return conversationDeleted;
|
|
}
|
|
|
|
private static final String MAX_RECEIVED_TIMESTAMP =
|
|
"MAX(" + MessageColumns.RECEIVED_TIMESTAMP + ")";
|
|
/**
|
|
* Get the max received timestamp of a conversation's messages
|
|
*/
|
|
private static long getConversationMaxTimestamp(final DatabaseWrapper dbWrapper,
|
|
final String conversationId) {
|
|
final Cursor cursor = dbWrapper.query(
|
|
DatabaseHelper.MESSAGES_TABLE,
|
|
new String[]{ MAX_RECEIVED_TIMESTAMP },
|
|
MessageColumns.CONVERSATION_ID + "=?",
|
|
new String[]{ conversationId },
|
|
null, null, null);
|
|
if (cursor != null) {
|
|
try {
|
|
if (cursor.moveToFirst()) {
|
|
return cursor.getLong(0);
|
|
}
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static void updateConversationMetadataInTransaction(final DatabaseWrapper dbWrapper,
|
|
final String conversationId, final String messageId, final long latestTimestamp,
|
|
final boolean keepArchived, final String smsServiceCenter,
|
|
final boolean shouldAutoSwitchSelfId) {
|
|
Assert.isNotMainThread();
|
|
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
|
|
|
final ContentValues values = new ContentValues();
|
|
values.put(ConversationColumns.LATEST_MESSAGE_ID, messageId);
|
|
values.put(ConversationColumns.SORT_TIMESTAMP, latestTimestamp);
|
|
if (!TextUtils.isEmpty(smsServiceCenter)) {
|
|
values.put(ConversationColumns.SMS_SERVICE_CENTER, smsServiceCenter);
|
|
}
|
|
|
|
// When the conversation gets updated with new messages, unarchive the conversation unless
|
|
// the sender is blocked, or we have been told to keep it archived.
|
|
if (!keepArchived) {
|
|
values.put(ConversationColumns.ARCHIVE_STATUS, 0);
|
|
}
|
|
|
|
final MessageData message = readMessage(dbWrapper, messageId);
|
|
addSnippetTextAndPreviewToContentValues(message, false /* showDraft */, values);
|
|
|
|
if (shouldAutoSwitchSelfId) {
|
|
addSelfIdAutoSwitchInfoToContentValues(dbWrapper, message, conversationId, values);
|
|
}
|
|
|
|
// Conversation always exists as this method is called from ActionService only after
|
|
// reading and if necessary creating the conversation.
|
|
updateConversationRow(dbWrapper, conversationId, values);
|
|
|
|
if (shouldAutoSwitchSelfId && OsUtil.isAtLeastL_MR1()) {
|
|
// Normally, the draft message compose UI trusts its UI state for providing up-to-date
|
|
// conversation self id. Therefore, notify UI through local broadcast receiver about
|
|
// this external change so the change can be properly reflected.
|
|
UIIntents.get().broadcastConversationSelfIdChange(dbWrapper.getContext(),
|
|
conversationId, getConversationSelfId(dbWrapper, conversationId));
|
|
}
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static void updateConversationMetadataInTransaction(final DatabaseWrapper db,
|
|
final String conversationId, final String messageId, final long latestTimestamp,
|
|
final boolean keepArchived, final boolean shouldAutoSwitchSelfId) {
|
|
Assert.isNotMainThread();
|
|
updateConversationMetadataInTransaction(
|
|
db, conversationId, messageId, latestTimestamp, keepArchived, null,
|
|
shouldAutoSwitchSelfId);
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static void updateConversationArchiveStatusInTransaction(final DatabaseWrapper dbWrapper,
|
|
final String conversationId, final boolean isArchived) {
|
|
Assert.isNotMainThread();
|
|
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
|
final ContentValues values = new ContentValues();
|
|
values.put(ConversationColumns.ARCHIVE_STATUS, isArchived ? 1 : 0);
|
|
updateConversationRowIfExists(dbWrapper, conversationId, values);
|
|
}
|
|
|
|
static void addSnippetTextAndPreviewToContentValues(final MessageData message,
|
|
final boolean showDraft, final ContentValues values) {
|
|
values.put(ConversationColumns.SHOW_DRAFT, showDraft ? 1 : 0);
|
|
values.put(ConversationColumns.SNIPPET_TEXT, message.getMessageText());
|
|
values.put(ConversationColumns.SUBJECT_TEXT, message.getMmsSubject());
|
|
|
|
String type = null;
|
|
String uriString = null;
|
|
for (final MessagePartData part : message.getParts()) {
|
|
if (part.isAttachment() &&
|
|
ContentType.isConversationListPreviewableType(part.getContentType())) {
|
|
uriString = part.getContentUri().toString();
|
|
type = part.getContentType();
|
|
break;
|
|
}
|
|
}
|
|
values.put(ConversationColumns.PREVIEW_CONTENT_TYPE, type);
|
|
values.put(ConversationColumns.PREVIEW_URI, uriString);
|
|
}
|
|
|
|
/**
|
|
* Adds self-id auto switch info for a conversation if the last message has a different
|
|
* subscription than the conversation's.
|
|
* @return true if self id will need to be changed, false otherwise.
|
|
*/
|
|
static boolean addSelfIdAutoSwitchInfoToContentValues(final DatabaseWrapper dbWrapper,
|
|
final MessageData message, final String conversationId, final ContentValues values) {
|
|
// Only auto switch conversation self for incoming messages.
|
|
if (!OsUtil.isAtLeastL_MR1() || !message.getIsIncoming()) {
|
|
return false;
|
|
}
|
|
|
|
final String conversationSelfId = getConversationSelfId(dbWrapper, conversationId);
|
|
final String messageSelfId = message.getSelfId();
|
|
|
|
if (conversationSelfId == null || messageSelfId == null) {
|
|
return false;
|
|
}
|
|
|
|
// Get the sub IDs in effect for both the message and the conversation and compare them:
|
|
// 1. If message is unbound (using default sub id), then the message was sent with
|
|
// pre-MSIM support. Don't auto-switch because we don't know the subscription for the
|
|
// message.
|
|
// 2. If message is bound,
|
|
// i. If conversation is unbound, use the system default sub id as its effective sub.
|
|
// ii. If conversation is bound, use its subscription directly.
|
|
// Compare the message sub id with the conversation's effective sub id. If they are
|
|
// different, auto-switch the conversation to the message's sub.
|
|
final ParticipantData conversationSelf = getExistingParticipant(dbWrapper,
|
|
conversationSelfId);
|
|
final ParticipantData messageSelf = getExistingParticipant(dbWrapper, messageSelfId);
|
|
if (!messageSelf.isActiveSubscription()) {
|
|
// Don't switch if the message subscription is no longer active.
|
|
return false;
|
|
}
|
|
final int messageSubId = messageSelf.getSubId();
|
|
if (messageSubId == ParticipantData.DEFAULT_SELF_SUB_ID) {
|
|
return false;
|
|
}
|
|
|
|
final int conversationEffectiveSubId =
|
|
PhoneUtils.getDefault().getEffectiveSubId(conversationSelf.getSubId());
|
|
|
|
if (conversationEffectiveSubId != messageSubId) {
|
|
return addConversationSelfIdToContentValues(dbWrapper, messageSelf.getId(), values);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Adds conversation self id updates to ContentValues given. This performs check on the selfId
|
|
* to ensure it's valid and active.
|
|
* @return true if self id will need to be changed, false otherwise.
|
|
*/
|
|
static boolean addConversationSelfIdToContentValues(final DatabaseWrapper dbWrapper,
|
|
final String selfId, final ContentValues values) {
|
|
// Make sure the selfId passed in is valid and active.
|
|
final String selection = ParticipantColumns._ID + "=? AND " +
|
|
ParticipantColumns.SIM_SLOT_ID + "<>?";
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
|
|
new String[] { ParticipantColumns._ID }, selection,
|
|
new String[] { selfId, String.valueOf(ParticipantData.INVALID_SLOT_ID) },
|
|
null, null, null);
|
|
|
|
if (cursor != null && cursor.getCount() > 0) {
|
|
values.put(ConversationColumns.CURRENT_SELF_ID, selfId);
|
|
return true;
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static void updateConversationDraftSnippetAndPreviewInTransaction(
|
|
final DatabaseWrapper dbWrapper, final String conversationId,
|
|
final MessageData draftMessage) {
|
|
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
|
|
|
long sortTimestamp = 0L;
|
|
Cursor cursor = null;
|
|
try {
|
|
// Check to find the latest message in the conversation
|
|
cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
|
|
REFRESH_CONVERSATION_MESSAGE_PROJECTION,
|
|
MessageColumns.CONVERSATION_ID + "=?",
|
|
new String[]{conversationId}, null, null,
|
|
MessageColumns.RECEIVED_TIMESTAMP + " DESC", "1" /* limit */);
|
|
|
|
if (cursor.moveToFirst()) {
|
|
sortTimestamp = cursor.getLong(1);
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
|
|
final ContentValues values = new ContentValues();
|
|
if (draftMessage == null || !draftMessage.hasContent()) {
|
|
values.put(ConversationColumns.SHOW_DRAFT, 0);
|
|
values.put(ConversationColumns.DRAFT_SNIPPET_TEXT, "");
|
|
values.put(ConversationColumns.DRAFT_SUBJECT_TEXT, "");
|
|
values.put(ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE, "");
|
|
values.put(ConversationColumns.DRAFT_PREVIEW_URI, "");
|
|
} else {
|
|
sortTimestamp = Math.max(sortTimestamp, draftMessage.getReceivedTimeStamp());
|
|
values.put(ConversationColumns.SHOW_DRAFT, 1);
|
|
values.put(ConversationColumns.DRAFT_SNIPPET_TEXT, draftMessage.getMessageText());
|
|
values.put(ConversationColumns.DRAFT_SUBJECT_TEXT, draftMessage.getMmsSubject());
|
|
String type = null;
|
|
String uriString = null;
|
|
for (final MessagePartData part : draftMessage.getParts()) {
|
|
if (part.isAttachment() &&
|
|
ContentType.isConversationListPreviewableType(part.getContentType())) {
|
|
uriString = part.getContentUri().toString();
|
|
type = part.getContentType();
|
|
break;
|
|
}
|
|
}
|
|
values.put(ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE, type);
|
|
values.put(ConversationColumns.DRAFT_PREVIEW_URI, uriString);
|
|
}
|
|
values.put(ConversationColumns.SORT_TIMESTAMP, sortTimestamp);
|
|
// Called in transaction after reading conversation row
|
|
updateConversationRow(dbWrapper, conversationId, values);
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static boolean updateConversationRowIfExists(final DatabaseWrapper dbWrapper,
|
|
final String conversationId, final ContentValues values) {
|
|
Assert.isNotMainThread();
|
|
return updateRowIfExists(dbWrapper, DatabaseHelper.CONVERSATIONS_TABLE,
|
|
ConversationColumns._ID, conversationId, values);
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static void updateConversationRow(final DatabaseWrapper dbWrapper,
|
|
final String conversationId, final ContentValues values) {
|
|
Assert.isNotMainThread();
|
|
final boolean exists = updateConversationRowIfExists(dbWrapper, conversationId, values);
|
|
Assert.isTrue(exists);
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static boolean updateMessageRowIfExists(final DatabaseWrapper dbWrapper,
|
|
final String messageId, final ContentValues values) {
|
|
Assert.isNotMainThread();
|
|
return updateRowIfExists(dbWrapper, DatabaseHelper.MESSAGES_TABLE, MessageColumns._ID,
|
|
messageId, values);
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static void updateMessageRow(final DatabaseWrapper dbWrapper,
|
|
final String messageId, final ContentValues values) {
|
|
Assert.isNotMainThread();
|
|
final boolean exists = updateMessageRowIfExists(dbWrapper, messageId, values);
|
|
Assert.isTrue(exists);
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static boolean updatePartRowIfExists(final DatabaseWrapper dbWrapper,
|
|
final String partId, final ContentValues values) {
|
|
Assert.isNotMainThread();
|
|
return updateRowIfExists(dbWrapper, DatabaseHelper.PARTS_TABLE, PartColumns._ID,
|
|
partId, values);
|
|
}
|
|
|
|
/**
|
|
* Returns the default conversation name based on its participants.
|
|
*/
|
|
private static String getDefaultConversationName(final List<ParticipantData> participants) {
|
|
return ConversationListItemData.generateConversationName(participants);
|
|
}
|
|
|
|
/**
|
|
* Updates a given conversation's name based on its participants.
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static void updateConversationNameAndAvatarInTransaction(
|
|
final DatabaseWrapper dbWrapper, final String conversationId) {
|
|
Assert.isNotMainThread();
|
|
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
|
|
|
final ArrayList<ParticipantData> participants =
|
|
getParticipantsForConversation(dbWrapper, conversationId);
|
|
updateConversationNameAndAvatarInTransaction(dbWrapper, conversationId, participants);
|
|
}
|
|
|
|
/**
|
|
* Updates a given conversation's name based on its participants.
|
|
*/
|
|
private static void updateConversationNameAndAvatarInTransaction(
|
|
final DatabaseWrapper dbWrapper, final String conversationId,
|
|
final List<ParticipantData> participants) {
|
|
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
|
|
|
final ContentValues values = new ContentValues();
|
|
values.put(ConversationColumns.NAME,
|
|
getDefaultConversationName(participants));
|
|
|
|
// Fill in IS_ENTERPRISE.
|
|
final boolean hasAnyEnterpriseContact =
|
|
ConversationListItemData.hasAnyEnterpriseContact(participants);
|
|
values.put(ConversationColumns.IS_ENTERPRISE, hasAnyEnterpriseContact);
|
|
|
|
fillParticipantData(values, participants);
|
|
|
|
// Used by background thread when refreshing conversation so conversation could be deleted.
|
|
updateConversationRowIfExists(dbWrapper, conversationId, values);
|
|
|
|
WidgetConversationProvider.notifyConversationRenamed(Factory.get().getApplicationContext(),
|
|
conversationId);
|
|
}
|
|
|
|
/**
|
|
* Updates a given conversation's self id.
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static void updateConversationSelfIdInTransaction(
|
|
final DatabaseWrapper dbWrapper, final String conversationId, final String selfId) {
|
|
Assert.isNotMainThread();
|
|
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
|
final ContentValues values = new ContentValues();
|
|
if (addConversationSelfIdToContentValues(dbWrapper, selfId, values)) {
|
|
updateConversationRowIfExists(dbWrapper, conversationId, values);
|
|
}
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static String getConversationSelfId(final DatabaseWrapper dbWrapper,
|
|
final String conversationId) {
|
|
Assert.isNotMainThread();
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
|
|
new String[] { ConversationColumns.CURRENT_SELF_ID },
|
|
ConversationColumns._ID + "=?",
|
|
new String[] { conversationId },
|
|
null, null, null);
|
|
Assert.inRange(cursor.getCount(), 0, 1);
|
|
if (cursor.moveToFirst()) {
|
|
return cursor.getString(0);
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Frees up memory associated with phone number to participant id matching.
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static void clearParticipantIdCache() {
|
|
Assert.isNotMainThread();
|
|
synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
|
|
sNormalizedPhoneNumberToParticipantIdCache.clear();
|
|
}
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static ArrayList<String> getRecipientsForConversation(final DatabaseWrapper dbWrapper,
|
|
final String conversationId) {
|
|
Assert.isNotMainThread();
|
|
final ArrayList<ParticipantData> participants =
|
|
getParticipantsForConversation(dbWrapper, conversationId);
|
|
|
|
final ArrayList<String> recipients = new ArrayList<String>();
|
|
for (final ParticipantData participant : participants) {
|
|
recipients.add(participant.getSendDestination());
|
|
}
|
|
|
|
return recipients;
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static String getSmsServiceCenterForConversation(final DatabaseWrapper dbWrapper,
|
|
final String conversationId) {
|
|
Assert.isNotMainThread();
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
|
|
new String[] { ConversationColumns.SMS_SERVICE_CENTER },
|
|
ConversationColumns._ID + "=?",
|
|
new String[] { conversationId },
|
|
null, null, null);
|
|
Assert.inRange(cursor.getCount(), 0, 1);
|
|
if (cursor.moveToFirst()) {
|
|
return cursor.getString(0);
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static ParticipantData getExistingParticipant(final DatabaseWrapper dbWrapper,
|
|
final String participantId) {
|
|
Assert.isNotMainThread();
|
|
ParticipantData participant = null;
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
|
|
ParticipantData.ParticipantsQuery.PROJECTION,
|
|
ParticipantColumns._ID + " =?",
|
|
new String[] { participantId }, null, null, null);
|
|
Assert.inRange(cursor.getCount(), 0, 1);
|
|
if (cursor.moveToFirst()) {
|
|
participant = ParticipantData.getFromCursor(cursor);
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
return participant;
|
|
}
|
|
|
|
public static int getSelfSubscriptionId(final DatabaseWrapper dbWrapper,
|
|
final String selfParticipantId) {
|
|
final ParticipantData selfParticipant = BugleDatabaseOperations.getExistingParticipant(
|
|
dbWrapper, selfParticipantId);
|
|
if (selfParticipant != null) {
|
|
Assert.isTrue(selfParticipant.isSelf());
|
|
return selfParticipant.getSubId();
|
|
}
|
|
return ParticipantData.DEFAULT_SELF_SUB_ID;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
@DoesNotRunOnMainThread
|
|
public static ArrayList<ParticipantData> getParticipantsForConversation(
|
|
final DatabaseWrapper dbWrapper, final String conversationId) {
|
|
Assert.isNotMainThread();
|
|
final ArrayList<ParticipantData> participants =
|
|
new ArrayList<ParticipantData>();
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
|
|
ParticipantData.ParticipantsQuery.PROJECTION,
|
|
ParticipantColumns._ID + " IN ( " + "SELECT "
|
|
+ ConversationParticipantsColumns.PARTICIPANT_ID + " AS "
|
|
+ ParticipantColumns._ID
|
|
+ " FROM " + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE
|
|
+ " WHERE " + ConversationParticipantsColumns.CONVERSATION_ID + " =? )",
|
|
new String[] { conversationId }, null, null, null);
|
|
|
|
while (cursor.moveToNext()) {
|
|
participants.add(ParticipantData.getFromCursor(cursor));
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
return participants;
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static MessageData readMessage(final DatabaseWrapper dbWrapper, final String messageId) {
|
|
Assert.isNotMainThread();
|
|
final MessageData message = readMessageData(dbWrapper, messageId);
|
|
if (message != null) {
|
|
readMessagePartsData(dbWrapper, message, false);
|
|
}
|
|
return message;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
static MessagePartData readMessagePartData(final DatabaseWrapper dbWrapper,
|
|
final String partId) {
|
|
MessagePartData messagePartData = null;
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = dbWrapper.query(DatabaseHelper.PARTS_TABLE,
|
|
MessagePartData.getProjection(), PartColumns._ID + "=?",
|
|
new String[] { partId }, null, null, null);
|
|
Assert.inRange(cursor.getCount(), 0, 1);
|
|
if (cursor.moveToFirst()) {
|
|
messagePartData = MessagePartData.createFromCursor(cursor);
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
return messagePartData;
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static MessageData readMessageData(final DatabaseWrapper dbWrapper,
|
|
final Uri smsMessageUri) {
|
|
Assert.isNotMainThread();
|
|
MessageData message = null;
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
|
|
MessageData.getProjection(), MessageColumns.SMS_MESSAGE_URI + "=?",
|
|
new String[] { smsMessageUri.toString() }, null, null, null);
|
|
Assert.inRange(cursor.getCount(), 0, 1);
|
|
if (cursor.moveToFirst()) {
|
|
message = new MessageData();
|
|
message.bind(cursor);
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
return message;
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static MessageData readMessageData(final DatabaseWrapper dbWrapper,
|
|
final String messageId) {
|
|
Assert.isNotMainThread();
|
|
MessageData message = null;
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
|
|
MessageData.getProjection(), MessageColumns._ID + "=?",
|
|
new String[] { messageId }, null, null, null);
|
|
Assert.inRange(cursor.getCount(), 0, 1);
|
|
if (cursor.moveToFirst()) {
|
|
message = new MessageData();
|
|
message.bind(cursor);
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
return message;
|
|
}
|
|
|
|
/**
|
|
* Read all the parts for a message
|
|
* @param dbWrapper database
|
|
* @param message read parts for this message
|
|
* @param checkAttachmentFilesExist check each attachment file and only include if file exists
|
|
*/
|
|
private static void readMessagePartsData(final DatabaseWrapper dbWrapper,
|
|
final MessageData message, final boolean checkAttachmentFilesExist) {
|
|
final ContentResolver contentResolver =
|
|
Factory.get().getApplicationContext().getContentResolver();
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = dbWrapper.query(DatabaseHelper.PARTS_TABLE,
|
|
MessagePartData.getProjection(), PartColumns.MESSAGE_ID + "=?",
|
|
new String[] { message.getMessageId() }, null, null, null);
|
|
while (cursor.moveToNext()) {
|
|
final MessagePartData messagePartData = MessagePartData.createFromCursor(cursor);
|
|
if (checkAttachmentFilesExist && messagePartData.isAttachment() &&
|
|
!UriUtil.isBugleAppResource(messagePartData.getContentUri())) {
|
|
try {
|
|
// Test that the file exists before adding the attachment to the draft
|
|
final ParcelFileDescriptor fileDescriptor =
|
|
contentResolver.openFileDescriptor(
|
|
messagePartData.getContentUri(), "r");
|
|
if (fileDescriptor != null) {
|
|
fileDescriptor.close();
|
|
message.addPart(messagePartData);
|
|
}
|
|
} catch (final IOException e) {
|
|
// The attachment's temp storage no longer exists, just ignore the file
|
|
} catch (final SecurityException e) {
|
|
// Likely thrown by openFileDescriptor due to an expired access grant.
|
|
if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
|
|
LogUtil.d(LogUtil.BUGLE_TAG, "uri: " + messagePartData.getContentUri());
|
|
}
|
|
}
|
|
} else {
|
|
message.addPart(messagePartData);
|
|
}
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write a message part to our local database
|
|
*
|
|
* @param dbWrapper The database
|
|
* @param messagePart The message part to insert
|
|
* @return The row id of the newly inserted part
|
|
*/
|
|
static String insertNewMessagePartInTransaction(final DatabaseWrapper dbWrapper,
|
|
final MessagePartData messagePart, final String conversationId) {
|
|
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
|
Assert.isTrue(!TextUtils.isEmpty(messagePart.getMessageId()));
|
|
|
|
// Insert a new part row
|
|
final SQLiteStatement insert = messagePart.getInsertStatement(dbWrapper, conversationId);
|
|
final long rowNumber = insert.executeInsert();
|
|
|
|
Assert.inRange(rowNumber, 0, Long.MAX_VALUE);
|
|
final String partId = Long.toString(rowNumber);
|
|
|
|
// Update the part id
|
|
messagePart.updatePartId(partId);
|
|
|
|
return partId;
|
|
}
|
|
|
|
/**
|
|
* Insert a message and its parts into the table
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static void insertNewMessageInTransaction(final DatabaseWrapper dbWrapper,
|
|
final MessageData message) {
|
|
Assert.isNotMainThread();
|
|
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
|
|
|
// Insert message row
|
|
final SQLiteStatement insert = message.getInsertStatement(dbWrapper);
|
|
final long rowNumber = insert.executeInsert();
|
|
|
|
Assert.inRange(rowNumber, 0, Long.MAX_VALUE);
|
|
final String messageId = Long.toString(rowNumber);
|
|
message.updateMessageId(messageId);
|
|
// Insert new parts
|
|
for (final MessagePartData messagePart : message.getParts()) {
|
|
messagePart.updateMessageId(messageId);
|
|
insertNewMessagePartInTransaction(dbWrapper, messagePart, message.getConversationId());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update a message and add its parts into the table
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static void updateMessageInTransaction(final DatabaseWrapper dbWrapper,
|
|
final MessageData message) {
|
|
Assert.isNotMainThread();
|
|
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
|
final String messageId = message.getMessageId();
|
|
// Check message still exists (sms sync or delete might have purged it)
|
|
final MessageData current = BugleDatabaseOperations.readMessage(dbWrapper, messageId);
|
|
if (current != null) {
|
|
// Delete existing message parts)
|
|
deletePartsForMessage(dbWrapper, message.getMessageId());
|
|
// Insert new parts
|
|
for (final MessagePartData messagePart : message.getParts()) {
|
|
messagePart.updatePartId(null);
|
|
messagePart.updateMessageId(message.getMessageId());
|
|
insertNewMessagePartInTransaction(dbWrapper, messagePart,
|
|
message.getConversationId());
|
|
}
|
|
// Update message row
|
|
final ContentValues values = new ContentValues();
|
|
message.populate(values);
|
|
updateMessageRowIfExists(dbWrapper, message.getMessageId(), values);
|
|
}
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static void updateMessageAndPartsInTransaction(final DatabaseWrapper dbWrapper,
|
|
final MessageData message, final List<MessagePartData> partsToUpdate) {
|
|
Assert.isNotMainThread();
|
|
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
|
final ContentValues values = new ContentValues();
|
|
for (final MessagePartData messagePart : partsToUpdate) {
|
|
values.clear();
|
|
messagePart.populate(values);
|
|
updatePartRowIfExists(dbWrapper, messagePart.getPartId(), values);
|
|
}
|
|
values.clear();
|
|
message.populate(values);
|
|
updateMessageRowIfExists(dbWrapper, message.getMessageId(), values);
|
|
}
|
|
|
|
/**
|
|
* Delete all parts for a message
|
|
*/
|
|
static void deletePartsForMessage(final DatabaseWrapper dbWrapper,
|
|
final String messageId) {
|
|
final int cnt = dbWrapper.delete(DatabaseHelper.PARTS_TABLE,
|
|
PartColumns.MESSAGE_ID + " =?",
|
|
new String[] { messageId });
|
|
Assert.inRange(cnt, 0, Integer.MAX_VALUE);
|
|
}
|
|
|
|
/**
|
|
* Delete one message and update the conversation (if necessary).
|
|
*
|
|
* @return number of rows deleted (should be 1 or 0).
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static int deleteMessage(final DatabaseWrapper dbWrapper, final String messageId) {
|
|
Assert.isNotMainThread();
|
|
dbWrapper.beginTransaction();
|
|
try {
|
|
// Read message to find out which conversation it is in
|
|
final MessageData message = BugleDatabaseOperations.readMessage(dbWrapper, messageId);
|
|
|
|
int count = 0;
|
|
if (message != null) {
|
|
final String conversationId = message.getConversationId();
|
|
// Delete message
|
|
count = dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
|
|
MessageColumns._ID + "=?", new String[] { messageId });
|
|
|
|
if (!deleteConversationIfEmptyInTransaction(dbWrapper, conversationId)) {
|
|
// TODO: Should we leave the conversation sort timestamp alone?
|
|
refreshConversationMetadataInTransaction(dbWrapper, conversationId,
|
|
false/* shouldAutoSwitchSelfId */, false/*archived*/);
|
|
}
|
|
}
|
|
dbWrapper.setTransactionSuccessful();
|
|
return count;
|
|
} finally {
|
|
dbWrapper.endTransaction();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes the conversation if there are zero non-draft messages left.
|
|
* <p>
|
|
* This is necessary because the telephony database has a trigger that deletes threads after
|
|
* their last message is deleted. We need to ensure that if a thread goes away, we also delete
|
|
* the conversation in Bugle. We don't store draft messages in telephony, so we ignore those
|
|
* when querying for the # of messages in the conversation.
|
|
*
|
|
* @return true if the conversation was deleted
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static boolean deleteConversationIfEmptyInTransaction(final DatabaseWrapper dbWrapper,
|
|
final String conversationId) {
|
|
Assert.isNotMainThread();
|
|
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
|
Cursor cursor = null;
|
|
try {
|
|
// TODO: The refreshConversationMetadataInTransaction method below uses this
|
|
// same query; maybe they should share this logic?
|
|
|
|
// Check to see if there are any (non-draft) messages in the conversation
|
|
cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
|
|
REFRESH_CONVERSATION_MESSAGE_PROJECTION,
|
|
MessageColumns.CONVERSATION_ID + "=? AND " +
|
|
MessageColumns.STATUS + "!=" + MessageData.BUGLE_STATUS_OUTGOING_DRAFT,
|
|
new String[] { conversationId }, null, null,
|
|
MessageColumns.RECEIVED_TIMESTAMP + " DESC", "1" /* limit */);
|
|
if (cursor.getCount() == 0) {
|
|
dbWrapper.delete(DatabaseHelper.CONVERSATIONS_TABLE,
|
|
ConversationColumns._ID + "=?", new String[] { conversationId });
|
|
LogUtil.i(TAG,
|
|
"BugleDatabaseOperations: Deleted empty conversation " + conversationId);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static final String[] REFRESH_CONVERSATION_MESSAGE_PROJECTION = new String[] {
|
|
MessageColumns._ID,
|
|
MessageColumns.RECEIVED_TIMESTAMP,
|
|
MessageColumns.SENDER_PARTICIPANT_ID
|
|
};
|
|
|
|
/**
|
|
* Update conversation snippet, timestamp and optionally self id to match latest message in
|
|
* conversation.
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static void refreshConversationMetadataInTransaction(final DatabaseWrapper dbWrapper,
|
|
final String conversationId, final boolean shouldAutoSwitchSelfId,
|
|
boolean keepArchived) {
|
|
Assert.isNotMainThread();
|
|
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
|
Cursor cursor = null;
|
|
try {
|
|
// Check to see if there are any (non-draft) messages in the conversation
|
|
cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
|
|
REFRESH_CONVERSATION_MESSAGE_PROJECTION,
|
|
MessageColumns.CONVERSATION_ID + "=? AND " +
|
|
MessageColumns.STATUS + "!=" + MessageData.BUGLE_STATUS_OUTGOING_DRAFT,
|
|
new String[] { conversationId }, null, null,
|
|
MessageColumns.RECEIVED_TIMESTAMP + " DESC", "1" /* limit */);
|
|
|
|
if (cursor.moveToFirst()) {
|
|
// Refresh latest message in conversation
|
|
final String latestMessageId = cursor.getString(0);
|
|
final long latestMessageTimestamp = cursor.getLong(1);
|
|
final String senderParticipantId = cursor.getString(2);
|
|
final boolean senderBlocked = isBlockedParticipant(dbWrapper, senderParticipantId);
|
|
updateConversationMetadataInTransaction(dbWrapper, conversationId,
|
|
latestMessageId, latestMessageTimestamp, senderBlocked || keepArchived,
|
|
shouldAutoSwitchSelfId);
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When moving/removing an existing message update conversation metadata if necessary
|
|
* @param dbWrapper db wrapper
|
|
* @param conversationId conversation to modify
|
|
* @param messageId message that is leaving the conversation
|
|
* @param shouldAutoSwitchSelfId should we try to auto-switch the conversation's self-id as a
|
|
* result of this call when we see a new latest message?
|
|
* @param keepArchived should we keep the conversation archived despite refresh
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static void maybeRefreshConversationMetadataInTransaction(
|
|
final DatabaseWrapper dbWrapper, final String conversationId, final String messageId,
|
|
final boolean shouldAutoSwitchSelfId, final boolean keepArchived) {
|
|
Assert.isNotMainThread();
|
|
boolean refresh = true;
|
|
if (!TextUtils.isEmpty(messageId)) {
|
|
refresh = false;
|
|
// Look for an existing conversation in the db with this conversation id
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
|
|
new String[] { ConversationColumns.LATEST_MESSAGE_ID },
|
|
ConversationColumns._ID + "=?",
|
|
new String[] { conversationId },
|
|
null, null, null);
|
|
Assert.inRange(cursor.getCount(), 0, 1);
|
|
if (cursor.moveToFirst()) {
|
|
refresh = TextUtils.equals(cursor.getString(0), messageId);
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
}
|
|
if (refresh) {
|
|
// TODO: I think it is okay to delete the conversation if it is empty...
|
|
refreshConversationMetadataInTransaction(dbWrapper, conversationId,
|
|
shouldAutoSwitchSelfId, keepArchived);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// SQL statement to query latest message if for particular conversation
|
|
private static final String QUERY_CONVERSATIONS_LATEST_MESSAGE_SQL = "SELECT "
|
|
+ ConversationColumns.LATEST_MESSAGE_ID + " FROM " + DatabaseHelper.CONVERSATIONS_TABLE
|
|
+ " WHERE " + ConversationColumns._ID + "=? LIMIT 1";
|
|
|
|
/**
|
|
* Note this is not thread safe so callers need to make sure they own the wrapper + statements
|
|
* while they call this and use the returned value.
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static SQLiteStatement getQueryConversationsLatestMessageStatement(
|
|
final DatabaseWrapper db, final String conversationId) {
|
|
Assert.isNotMainThread();
|
|
final SQLiteStatement query = db.getStatementInTransaction(
|
|
DatabaseWrapper.INDEX_QUERY_CONVERSATIONS_LATEST_MESSAGE,
|
|
QUERY_CONVERSATIONS_LATEST_MESSAGE_SQL);
|
|
query.clearBindings();
|
|
query.bindString(1, conversationId);
|
|
return query;
|
|
}
|
|
|
|
// SQL statement to query latest message if for particular conversation
|
|
private static final String QUERY_MESSAGES_LATEST_MESSAGE_SQL = "SELECT "
|
|
+ MessageColumns._ID + " FROM " + DatabaseHelper.MESSAGES_TABLE
|
|
+ " WHERE " + MessageColumns.CONVERSATION_ID + "=? ORDER BY "
|
|
+ MessageColumns.RECEIVED_TIMESTAMP + " DESC LIMIT 1";
|
|
|
|
/**
|
|
* Note this is not thread safe so callers need to make sure they own the wrapper + statements
|
|
* while they call this and use the returned value.
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static SQLiteStatement getQueryMessagesLatestMessageStatement(
|
|
final DatabaseWrapper db, final String conversationId) {
|
|
Assert.isNotMainThread();
|
|
final SQLiteStatement query = db.getStatementInTransaction(
|
|
DatabaseWrapper.INDEX_QUERY_MESSAGES_LATEST_MESSAGE,
|
|
QUERY_MESSAGES_LATEST_MESSAGE_SQL);
|
|
query.clearBindings();
|
|
query.bindString(1, conversationId);
|
|
return query;
|
|
}
|
|
|
|
/**
|
|
* Update conversation metadata if necessary
|
|
* @param dbWrapper db wrapper
|
|
* @param conversationId conversation to modify
|
|
* @param shouldAutoSwitchSelfId should we try to auto-switch the conversation's self-id as a
|
|
* result of this call when we see a new latest message?
|
|
* @param keepArchived if the conversation should be kept archived
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static void maybeRefreshConversationMetadataInTransaction(
|
|
final DatabaseWrapper dbWrapper, final String conversationId,
|
|
final boolean shouldAutoSwitchSelfId, boolean keepArchived) {
|
|
Assert.isNotMainThread();
|
|
String currentLatestMessageId = null;
|
|
String latestMessageId = null;
|
|
try {
|
|
final SQLiteStatement currentLatestMessageIdSql =
|
|
getQueryConversationsLatestMessageStatement(dbWrapper, conversationId);
|
|
currentLatestMessageId = currentLatestMessageIdSql.simpleQueryForString();
|
|
|
|
final SQLiteStatement latestMessageIdSql =
|
|
getQueryMessagesLatestMessageStatement(dbWrapper, conversationId);
|
|
latestMessageId = latestMessageIdSql.simpleQueryForString();
|
|
} catch (final SQLiteDoneException e) {
|
|
LogUtil.e(TAG, "BugleDatabaseOperations: Query for latest message failed", e);
|
|
}
|
|
|
|
if (TextUtils.isEmpty(currentLatestMessageId) ||
|
|
!TextUtils.equals(currentLatestMessageId, latestMessageId)) {
|
|
refreshConversationMetadataInTransaction(dbWrapper, conversationId,
|
|
shouldAutoSwitchSelfId, keepArchived);
|
|
}
|
|
}
|
|
|
|
static boolean getConversationExists(final DatabaseWrapper dbWrapper,
|
|
final String conversationId) {
|
|
// Look for an existing conversation in the db with this conversation id
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
|
|
new String[] { /* No projection */},
|
|
ConversationColumns._ID + "=?",
|
|
new String[] { conversationId },
|
|
null, null, null);
|
|
return cursor.getCount() == 1;
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Preserve parts in message but clear the stored draft */
|
|
public static final int UPDATE_MODE_CLEAR_DRAFT = 1;
|
|
/** Add the message as a draft */
|
|
public static final int UPDATE_MODE_ADD_DRAFT = 2;
|
|
|
|
/**
|
|
* Update draft message for specified conversation
|
|
* @param dbWrapper local database (wrapped)
|
|
* @param conversationId conversation to update
|
|
* @param message Optional message to preserve attachments for (either as draft or for
|
|
* sending)
|
|
* @param updateMode either {@link #UPDATE_MODE_CLEAR_DRAFT} or
|
|
* {@link #UPDATE_MODE_ADD_DRAFT}
|
|
* @return message id of newly written draft (else null)
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static String updateDraftMessageData(final DatabaseWrapper dbWrapper,
|
|
final String conversationId, @Nullable final MessageData message,
|
|
final int updateMode) {
|
|
Assert.isNotMainThread();
|
|
Assert.notNull(conversationId);
|
|
Assert.inRange(updateMode, UPDATE_MODE_CLEAR_DRAFT, UPDATE_MODE_ADD_DRAFT);
|
|
String messageId = null;
|
|
Cursor cursor = null;
|
|
dbWrapper.beginTransaction();
|
|
try {
|
|
// Find all draft parts for the current conversation
|
|
final SimpleArrayMap<Uri, MessagePartData> currentDraftParts = new SimpleArrayMap<>();
|
|
cursor = dbWrapper.query(DatabaseHelper.DRAFT_PARTS_VIEW,
|
|
MessagePartData.getProjection(),
|
|
MessageColumns.CONVERSATION_ID + " =?",
|
|
new String[] { conversationId }, null, null, null);
|
|
while (cursor.moveToNext()) {
|
|
final MessagePartData part = MessagePartData.createFromCursor(cursor);
|
|
if (part.isAttachment()) {
|
|
currentDraftParts.put(part.getContentUri(), part);
|
|
}
|
|
}
|
|
// Optionally, preserve attachments for "message"
|
|
final boolean conversationExists = getConversationExists(dbWrapper, conversationId);
|
|
if (message != null && conversationExists) {
|
|
for (final MessagePartData part : message.getParts()) {
|
|
if (part.isAttachment()) {
|
|
currentDraftParts.remove(part.getContentUri());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete orphan content
|
|
for (int index = 0; index < currentDraftParts.size(); index++) {
|
|
final MessagePartData part = currentDraftParts.valueAt(index);
|
|
part.destroySync();
|
|
}
|
|
|
|
// Delete existing draft (cascade deletes parts)
|
|
dbWrapper.delete(DatabaseHelper.MESSAGES_TABLE,
|
|
MessageColumns.STATUS + "=? AND " + MessageColumns.CONVERSATION_ID + "=?",
|
|
new String[] {
|
|
Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_DRAFT),
|
|
conversationId
|
|
});
|
|
|
|
// Write new draft
|
|
if (updateMode == UPDATE_MODE_ADD_DRAFT && message != null
|
|
&& message.hasContent() && conversationExists) {
|
|
Assert.equals(MessageData.BUGLE_STATUS_OUTGOING_DRAFT,
|
|
message.getStatus());
|
|
|
|
// Now add draft to message table
|
|
insertNewMessageInTransaction(dbWrapper, message);
|
|
messageId = message.getMessageId();
|
|
}
|
|
|
|
if (conversationExists) {
|
|
updateConversationDraftSnippetAndPreviewInTransaction(
|
|
dbWrapper, conversationId, message);
|
|
|
|
if (message != null && message.getSelfId() != null) {
|
|
updateConversationSelfIdInTransaction(dbWrapper, conversationId,
|
|
message.getSelfId());
|
|
}
|
|
}
|
|
|
|
dbWrapper.setTransactionSuccessful();
|
|
} finally {
|
|
dbWrapper.endTransaction();
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
|
LogUtil.v(TAG,
|
|
"Updated draft message " + messageId + " for conversation " + conversationId);
|
|
}
|
|
return messageId;
|
|
}
|
|
|
|
/**
|
|
* Read the first draft message associated with this conversation.
|
|
* If none present create an empty (sms) draft message.
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static MessageData readDraftMessageData(final DatabaseWrapper dbWrapper,
|
|
final String conversationId, final String conversationSelfId) {
|
|
Assert.isNotMainThread();
|
|
MessageData message = null;
|
|
Cursor cursor = null;
|
|
dbWrapper.beginTransaction();
|
|
try {
|
|
cursor = dbWrapper.query(DatabaseHelper.MESSAGES_TABLE,
|
|
MessageData.getProjection(),
|
|
MessageColumns.STATUS + "=? AND " + MessageColumns.CONVERSATION_ID + "=?",
|
|
new String[] {
|
|
Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_DRAFT),
|
|
conversationId
|
|
}, null, null, null);
|
|
Assert.inRange(cursor.getCount(), 0, 1);
|
|
if (cursor.moveToFirst()) {
|
|
message = new MessageData();
|
|
message.bindDraft(cursor, conversationSelfId);
|
|
readMessagePartsData(dbWrapper, message, true);
|
|
// Disconnect draft parts from DB
|
|
for (final MessagePartData part : message.getParts()) {
|
|
part.updatePartId(null);
|
|
part.updateMessageId(null);
|
|
}
|
|
message.updateMessageId(null);
|
|
}
|
|
dbWrapper.setTransactionSuccessful();
|
|
} finally {
|
|
dbWrapper.endTransaction();
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
return message;
|
|
}
|
|
|
|
// Internal
|
|
private static void addParticipantToConversation(final DatabaseWrapper dbWrapper,
|
|
final ParticipantData participant, final String conversationId) {
|
|
final String participantId = getOrCreateParticipantInTransaction(dbWrapper, participant);
|
|
Assert.notNull(participantId);
|
|
|
|
// Add the participant to the conversation participants table
|
|
final ContentValues values = new ContentValues();
|
|
values.put(ConversationParticipantsColumns.CONVERSATION_ID, conversationId);
|
|
values.put(ConversationParticipantsColumns.PARTICIPANT_ID, participantId);
|
|
dbWrapper.insert(DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE, null, values);
|
|
}
|
|
|
|
/**
|
|
* Get string used as canonical recipient for participant cache for sub id
|
|
*/
|
|
private static String getCanonicalRecipientFromSubId(final int subId) {
|
|
return "SELF(" + subId + ")";
|
|
}
|
|
|
|
/**
|
|
* Maps from a sub id or phone number to a participant id if there is one.
|
|
*
|
|
* @return If the participant is available in our cache, or the DB, this returns the
|
|
* participant id for the given subid/phone number. Otherwise it returns null.
|
|
*/
|
|
@VisibleForTesting
|
|
private static String getParticipantId(final DatabaseWrapper dbWrapper,
|
|
final int subId, final String canonicalRecipient) {
|
|
// First check our memory cache for the participant Id
|
|
String participantId;
|
|
synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
|
|
participantId = sNormalizedPhoneNumberToParticipantIdCache.get(canonicalRecipient);
|
|
}
|
|
|
|
if (participantId != null) {
|
|
return participantId;
|
|
}
|
|
|
|
// This code will only be executed for incremental additions.
|
|
Cursor cursor = null;
|
|
try {
|
|
if (subId != ParticipantData.OTHER_THAN_SELF_SUB_ID) {
|
|
// Now look for an existing participant in the db with this sub id.
|
|
cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
|
|
new String[] {ParticipantColumns._ID},
|
|
ParticipantColumns.SUB_ID + "=?",
|
|
new String[] { Integer.toString(subId) }, null, null, null);
|
|
} else {
|
|
// Look for existing participant with this normalized phone number and no subId.
|
|
cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
|
|
new String[] {ParticipantColumns._ID},
|
|
ParticipantColumns.NORMALIZED_DESTINATION + "=? AND "
|
|
+ ParticipantColumns.SUB_ID + "=?",
|
|
new String[] {canonicalRecipient, Integer.toString(subId)},
|
|
null, null, null);
|
|
}
|
|
|
|
if (cursor.moveToFirst()) {
|
|
// TODO Is this assert correct for multi-sim where a new sim was put in?
|
|
Assert.isTrue(cursor.getCount() == 1);
|
|
|
|
// We found an existing participant in the database
|
|
participantId = cursor.getString(0);
|
|
|
|
synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
|
|
// Add it to the cache for next time
|
|
sNormalizedPhoneNumberToParticipantIdCache.put(canonicalRecipient,
|
|
participantId);
|
|
}
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
return participantId;
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static ParticipantData getOrCreateSelf(final DatabaseWrapper dbWrapper,
|
|
final int subId) {
|
|
Assert.isNotMainThread();
|
|
ParticipantData participant = null;
|
|
dbWrapper.beginTransaction();
|
|
try {
|
|
final ParticipantData shell = ParticipantData.getSelfParticipant(subId);
|
|
final String participantId = getOrCreateParticipantInTransaction(dbWrapper, shell);
|
|
participant = getExistingParticipant(dbWrapper, participantId);
|
|
dbWrapper.setTransactionSuccessful();
|
|
} finally {
|
|
dbWrapper.endTransaction();
|
|
}
|
|
return participant;
|
|
}
|
|
|
|
/**
|
|
* Lookup and if necessary create a new participant
|
|
* @param dbWrapper Database wrapper
|
|
* @param participant Participant to find/create
|
|
* @return participantId ParticipantId for existing or newly created participant
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static String getOrCreateParticipantInTransaction(final DatabaseWrapper dbWrapper,
|
|
final ParticipantData participant) {
|
|
Assert.isNotMainThread();
|
|
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
|
int subId = ParticipantData.OTHER_THAN_SELF_SUB_ID;
|
|
String participantId = null;
|
|
String canonicalRecipient = null;
|
|
if (participant.isSelf()) {
|
|
subId = participant.getSubId();
|
|
canonicalRecipient = getCanonicalRecipientFromSubId(subId);
|
|
} else {
|
|
canonicalRecipient = participant.getNormalizedDestination();
|
|
}
|
|
Assert.notNull(canonicalRecipient);
|
|
participantId = getParticipantId(dbWrapper, subId, canonicalRecipient);
|
|
|
|
if (participantId != null) {
|
|
return participantId;
|
|
}
|
|
|
|
if (!participant.isContactIdResolved()) {
|
|
// Refresh participant's name and avatar with matching contact in CP2.
|
|
ParticipantRefresh.refreshParticipant(dbWrapper, participant);
|
|
}
|
|
|
|
// Insert the participant into the participants table
|
|
final ContentValues values = participant.toContentValues();
|
|
final long participantRow = dbWrapper.insert(DatabaseHelper.PARTICIPANTS_TABLE, null,
|
|
values);
|
|
participantId = Long.toString(participantRow);
|
|
Assert.notNull(canonicalRecipient);
|
|
|
|
synchronized (sNormalizedPhoneNumberToParticipantIdCache) {
|
|
// Now that we've inserted it, add it to our cache
|
|
sNormalizedPhoneNumberToParticipantIdCache.put(canonicalRecipient, participantId);
|
|
}
|
|
|
|
return participantId;
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static void updateDestination(final DatabaseWrapper dbWrapper,
|
|
final String destination, final boolean blocked) {
|
|
Assert.isNotMainThread();
|
|
final ContentValues values = new ContentValues();
|
|
values.put(ParticipantColumns.BLOCKED, blocked ? 1 : 0);
|
|
dbWrapper.update(DatabaseHelper.PARTICIPANTS_TABLE, values,
|
|
ParticipantColumns.NORMALIZED_DESTINATION + "=? AND " +
|
|
ParticipantColumns.SUB_ID + "=?",
|
|
new String[] { destination, Integer.toString(
|
|
ParticipantData.OTHER_THAN_SELF_SUB_ID) });
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static String getConversationFromOtherParticipantDestination(
|
|
final DatabaseWrapper db, final String otherDestination) {
|
|
Assert.isNotMainThread();
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = db.query(DatabaseHelper.CONVERSATIONS_TABLE,
|
|
new String[] { ConversationColumns._ID },
|
|
ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION + "=?",
|
|
new String[] { otherDestination }, null, null, null);
|
|
Assert.inRange(cursor.getCount(), 0, 1);
|
|
if (cursor.moveToFirst()) {
|
|
return cursor.getString(0);
|
|
}
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get a list of conversations that contain any of participants specified.
|
|
*/
|
|
private static HashSet<String> getConversationsForParticipants(
|
|
final ArrayList<String> participantIds) {
|
|
final DatabaseWrapper db = DataModel.get().getDatabase();
|
|
final HashSet<String> conversationIds = new HashSet<String>();
|
|
|
|
final String selection = ConversationParticipantsColumns.PARTICIPANT_ID + "=?";
|
|
for (final String participantId : participantIds) {
|
|
final String[] selectionArgs = new String[] { participantId };
|
|
final Cursor cursor = db.query(DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE,
|
|
ConversationParticipantsQuery.PROJECTION,
|
|
selection, selectionArgs, null, null, null);
|
|
|
|
if (cursor != null) {
|
|
try {
|
|
while (cursor.moveToNext()) {
|
|
final String conversationId = cursor.getString(
|
|
ConversationParticipantsQuery.INDEX_CONVERSATION_ID);
|
|
conversationIds.add(conversationId);
|
|
}
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
return conversationIds;
|
|
}
|
|
|
|
/**
|
|
* Refresh conversation names/avatars based on a list of participants that are changed.
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static void refreshConversationsForParticipants(final ArrayList<String> participants) {
|
|
Assert.isNotMainThread();
|
|
final HashSet<String> conversationIds = getConversationsForParticipants(participants);
|
|
if (conversationIds.size() > 0) {
|
|
for (final String conversationId : conversationIds) {
|
|
refreshConversation(conversationId);
|
|
}
|
|
|
|
MessagingContentProvider.notifyConversationListChanged();
|
|
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
|
LogUtil.v(TAG, "Number of conversations refreshed:" + conversationIds.size());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh conversation names/avatars based on a changed participant.
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
public static void refreshConversationsForParticipant(final String participantId) {
|
|
Assert.isNotMainThread();
|
|
final ArrayList<String> participantList = new ArrayList<String>(1);
|
|
participantList.add(participantId);
|
|
refreshConversationsForParticipants(participantList);
|
|
}
|
|
|
|
/**
|
|
* Refresh one conversation.
|
|
*/
|
|
private static void refreshConversation(final String conversationId) {
|
|
final DatabaseWrapper db = DataModel.get().getDatabase();
|
|
|
|
db.beginTransaction();
|
|
try {
|
|
BugleDatabaseOperations.updateConversationNameAndAvatarInTransaction(db,
|
|
conversationId);
|
|
db.setTransactionSuccessful();
|
|
} finally {
|
|
db.endTransaction();
|
|
}
|
|
|
|
MessagingContentProvider.notifyParticipantsChanged(conversationId);
|
|
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
|
MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
|
|
}
|
|
|
|
@DoesNotRunOnMainThread
|
|
public static boolean updateRowIfExists(final DatabaseWrapper db, final String table,
|
|
final String rowKey, final String rowId, final ContentValues values) {
|
|
Assert.isNotMainThread();
|
|
final StringBuilder sb = new StringBuilder();
|
|
final ArrayList<String> whereValues = new ArrayList<String>(values.size() + 1);
|
|
whereValues.add(rowId);
|
|
|
|
for (final String key : values.keySet()) {
|
|
if (sb.length() > 0) {
|
|
sb.append(" OR ");
|
|
}
|
|
final Object value = values.get(key);
|
|
sb.append(key);
|
|
if (value != null) {
|
|
sb.append(" IS NOT ?");
|
|
whereValues.add(value.toString());
|
|
} else {
|
|
sb.append(" IS NOT NULL");
|
|
}
|
|
}
|
|
|
|
final String whereClause = rowKey + "=?" + " AND (" + sb.toString() + ")";
|
|
final String [] whereValuesArray = whereValues.toArray(new String[whereValues.size()]);
|
|
final int count = db.update(table, values, whereClause, whereValuesArray);
|
|
if (count > 1) {
|
|
LogUtil.w(LogUtil.BUGLE_TAG, "Updated more than 1 row " + count + "; " + table +
|
|
" for " + rowKey + " = " + rowId + " (deleted?)");
|
|
}
|
|
Assert.inRange(count, 0, 1);
|
|
return (count >= 0);
|
|
}
|
|
}
|