Initial checkin of AOSP Messaging app.
b/23110861 Change-Id: I11db999bd10656801e618f78ab2b2ef74136fff1
This commit is contained in:
383
src/com/android/messaging/datamodel/action/SyncMessageBatch.java
Normal file
383
src/com/android/messaging/datamodel/action/SyncMessageBatch.java
Normal file
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
* 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.action;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.provider.Telephony;
|
||||
import android.provider.Telephony.Mms;
|
||||
import android.provider.Telephony.Sms;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.SyncManager.ThreadInfoCache;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.mmslib.pdu.PduHeaders;
|
||||
import com.android.messaging.sms.DatabaseMessages.LocalDatabaseMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.MmsMessage;
|
||||
import com.android.messaging.sms.DatabaseMessages.SmsMessage;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Update local database with a batch of messages to add/delete in one transaction
|
||||
*/
|
||||
class SyncMessageBatch {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
// Variables used during executeAction
|
||||
private final HashSet<String> mConversationsToUpdate;
|
||||
// Cache of thread->conversationId map
|
||||
private final ThreadInfoCache mCache;
|
||||
|
||||
// Set of SMS messages to add
|
||||
private final ArrayList<SmsMessage> mSmsToAdd;
|
||||
// Set of MMS messages to add
|
||||
private final ArrayList<MmsMessage> mMmsToAdd;
|
||||
// Set of local messages to delete
|
||||
private final ArrayList<LocalDatabaseMessage> mMessagesToDelete;
|
||||
|
||||
SyncMessageBatch(final ArrayList<SmsMessage> smsToAdd,
|
||||
final ArrayList<MmsMessage> mmsToAdd,
|
||||
final ArrayList<LocalDatabaseMessage> messagesToDelete,
|
||||
final ThreadInfoCache cache) {
|
||||
mSmsToAdd = smsToAdd;
|
||||
mMmsToAdd = mmsToAdd;
|
||||
mMessagesToDelete = messagesToDelete;
|
||||
mCache = cache;
|
||||
mConversationsToUpdate = new HashSet<String>();
|
||||
}
|
||||
|
||||
void updateLocalDatabase() {
|
||||
// Perform local database changes in one transaction
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
// Store all the SMS messages
|
||||
for (final SmsMessage sms : mSmsToAdd) {
|
||||
storeSms(db, sms);
|
||||
}
|
||||
// Store all the MMS messages
|
||||
for (final MmsMessage mms : mMmsToAdd) {
|
||||
storeMms(db, mms);
|
||||
}
|
||||
// Keep track of conversations with messages deleted
|
||||
for (final LocalDatabaseMessage message : mMessagesToDelete) {
|
||||
mConversationsToUpdate.add(message.getConversationId());
|
||||
}
|
||||
// Batch delete local messages
|
||||
batchDelete(db, DatabaseHelper.MESSAGES_TABLE, MessageColumns._ID,
|
||||
messageListToIds(mMessagesToDelete));
|
||||
|
||||
for (final LocalDatabaseMessage message : mMessagesToDelete) {
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "SyncMessageBatch: Deleted message " + message.getLocalId()
|
||||
+ " for SMS/MMS " + message.getUri() + " with timestamp "
|
||||
+ message.getTimestampInMillis());
|
||||
}
|
||||
}
|
||||
|
||||
// Update conversation state for imported messages, like snippet,
|
||||
updateConversations(db);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private static String[] messageListToIds(final List<LocalDatabaseMessage> messagesToDelete) {
|
||||
final String[] ids = new String[messagesToDelete.size()];
|
||||
for (int i = 0; i < ids.length; i++) {
|
||||
ids[i] = Long.toString(messagesToDelete.get(i).getLocalId());
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the SMS message into local database.
|
||||
*
|
||||
* @param sms
|
||||
*/
|
||||
private void storeSms(final DatabaseWrapper db, final SmsMessage sms) {
|
||||
if (sms.mBody == null) {
|
||||
LogUtil.w(TAG, "SyncMessageBatch: SMS " + sms.mUri + " has no body; adding empty one");
|
||||
// try to fix it
|
||||
sms.mBody = "";
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(sms.mAddress)) {
|
||||
LogUtil.e(TAG, "SyncMessageBatch: SMS has no address; using unknown sender");
|
||||
// try to fix it
|
||||
sms.mAddress = ParticipantData.getUnknownSenderDestination();
|
||||
}
|
||||
|
||||
// TODO : We need to also deal with messages in a failed/retry state
|
||||
final boolean isOutgoing = sms.mType != Sms.MESSAGE_TYPE_INBOX;
|
||||
|
||||
final String otherPhoneNumber = sms.mAddress;
|
||||
|
||||
// A forced resync of all messages should still keep the archived states.
|
||||
// The database upgrade code notifies sync manager of this. We need to
|
||||
// honor the original customization to this conversation if created.
|
||||
final String conversationId = mCache.getOrCreateConversation(db, sms.mThreadId, sms.mSubId,
|
||||
DataModel.get().getSyncManager().getCustomizationForThread(sms.mThreadId));
|
||||
if (conversationId == null) {
|
||||
// Cannot create conversation for this message? This should not happen.
|
||||
LogUtil.e(TAG, "SyncMessageBatch: Failed to create conversation for SMS thread "
|
||||
+ sms.mThreadId);
|
||||
return;
|
||||
}
|
||||
final ParticipantData self = ParticipantData.getSelfParticipant(sms.getSubId());
|
||||
final String selfId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
|
||||
final ParticipantData sender = isOutgoing ?
|
||||
self :
|
||||
ParticipantData.getFromRawPhoneBySimLocale(otherPhoneNumber, sms.getSubId());
|
||||
final String participantId = (isOutgoing ? selfId :
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, sender));
|
||||
|
||||
final int bugleStatus = bugleStatusForSms(isOutgoing, sms.mType, sms.mStatus);
|
||||
|
||||
final MessageData message = MessageData.createSmsMessage(
|
||||
sms.mUri,
|
||||
participantId,
|
||||
selfId,
|
||||
conversationId,
|
||||
bugleStatus,
|
||||
sms.mSeen,
|
||||
sms.mRead,
|
||||
sms.mTimestampSentInMillis,
|
||||
sms.mTimestampInMillis,
|
||||
sms.mBody);
|
||||
|
||||
// Inserting sms content into messages table
|
||||
try {
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
rethrowSQLiteConstraintExceptionWithDetails(e, db, sms.mUri, sms.mThreadId,
|
||||
conversationId, selfId, participantId);
|
||||
}
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "SyncMessageBatch: Inserted new message " + message.getMessageId()
|
||||
+ " for SMS " + message.getSmsMessageUri() + " received at "
|
||||
+ message.getReceivedTimeStamp());
|
||||
}
|
||||
|
||||
// Keep track of updated conversation for later updating the conversation snippet, etc.
|
||||
mConversationsToUpdate.add(conversationId);
|
||||
}
|
||||
|
||||
public static int bugleStatusForSms(final boolean isOutgoing, final int type,
|
||||
final int status) {
|
||||
int bugleStatus = MessageData.BUGLE_STATUS_UNKNOWN;
|
||||
// For a message we sync either
|
||||
if (isOutgoing) {
|
||||
// Outgoing message not yet been sent
|
||||
if (type == Telephony.Sms.MESSAGE_TYPE_FAILED ||
|
||||
type == Telephony.Sms.MESSAGE_TYPE_OUTBOX ||
|
||||
type == Telephony.Sms.MESSAGE_TYPE_QUEUED ||
|
||||
(type == Telephony.Sms.MESSAGE_TYPE_SENT &&
|
||||
status == Telephony.Sms.STATUS_FAILED)) {
|
||||
// Not sent counts as failed and available for manual resend
|
||||
bugleStatus = MessageData.BUGLE_STATUS_OUTGOING_FAILED;
|
||||
} else if (status == Sms.STATUS_COMPLETE) {
|
||||
bugleStatus = MessageData.BUGLE_STATUS_OUTGOING_DELIVERED;
|
||||
} else {
|
||||
// Otherwise outgoing message is complete
|
||||
bugleStatus = MessageData.BUGLE_STATUS_OUTGOING_COMPLETE;
|
||||
}
|
||||
} else {
|
||||
// All incoming SMS messages are complete
|
||||
bugleStatus = MessageData.BUGLE_STATUS_INCOMING_COMPLETE;
|
||||
}
|
||||
return bugleStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the MMS message into local database
|
||||
*
|
||||
* @param mms
|
||||
*/
|
||||
private void storeMms(final DatabaseWrapper db, final MmsMessage mms) {
|
||||
if (mms.mParts.size() < 1) {
|
||||
LogUtil.w(TAG, "SyncMessageBatch: MMS " + mms.mUri + " has no parts");
|
||||
}
|
||||
|
||||
// TODO : We need to also deal with messages in a failed/retry state
|
||||
final boolean isOutgoing = mms.mType != Mms.MESSAGE_BOX_INBOX;
|
||||
final boolean isNotification = (mms.mMmsMessageType ==
|
||||
PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND);
|
||||
|
||||
final String senderId = mms.mSender;
|
||||
|
||||
// A forced resync of all messages should still keep the archived states.
|
||||
// The database upgrade code notifies sync manager of this. We need to
|
||||
// honor the original customization to this conversation if created.
|
||||
final String conversationId = mCache.getOrCreateConversation(db, mms.mThreadId, mms.mSubId,
|
||||
DataModel.get().getSyncManager().getCustomizationForThread(mms.mThreadId));
|
||||
if (conversationId == null) {
|
||||
LogUtil.e(TAG, "SyncMessageBatch: Failed to create conversation for MMS thread "
|
||||
+ mms.mThreadId);
|
||||
return;
|
||||
}
|
||||
final ParticipantData self = ParticipantData.getSelfParticipant(mms.getSubId());
|
||||
final String selfId =
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
|
||||
final ParticipantData sender = isOutgoing ?
|
||||
self : ParticipantData.getFromRawPhoneBySimLocale(senderId, mms.getSubId());
|
||||
final String participantId = (isOutgoing ? selfId :
|
||||
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, sender));
|
||||
|
||||
final int bugleStatus = MmsUtils.bugleStatusForMms(isOutgoing, isNotification, mms.mType);
|
||||
|
||||
// Import message and all of the parts.
|
||||
// TODO : For now we are importing these in the order we found them in the MMS
|
||||
// database. Ideally we would load and parse the SMIL which describes how the parts relate
|
||||
// to one another.
|
||||
|
||||
// TODO: Need to set correct status on message
|
||||
final MessageData message = MmsUtils.createMmsMessage(mms, conversationId, participantId,
|
||||
selfId, bugleStatus);
|
||||
|
||||
// Inserting mms content into messages table
|
||||
try {
|
||||
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
rethrowSQLiteConstraintExceptionWithDetails(e, db, mms.mUri, mms.mThreadId,
|
||||
conversationId, selfId, participantId);
|
||||
}
|
||||
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
|
||||
LogUtil.v(TAG, "SyncMessageBatch: Inserted new message " + message.getMessageId()
|
||||
+ " for MMS " + message.getSmsMessageUri() + " received at "
|
||||
+ message.getReceivedTimeStamp());
|
||||
}
|
||||
|
||||
// Keep track of updated conversation for later updating the conversation snippet, etc.
|
||||
mConversationsToUpdate.add(conversationId);
|
||||
}
|
||||
|
||||
// TODO: Remove this after we no longer see this crash (b/18375758)
|
||||
private static void rethrowSQLiteConstraintExceptionWithDetails(SQLiteConstraintException e,
|
||||
DatabaseWrapper db, String messageUri, long threadId, String conversationId,
|
||||
String selfId, String senderId) {
|
||||
// Add some extra debug information to the exception for tracking down b/18375758.
|
||||
// The default detail message for SQLiteConstraintException tells us that a foreign
|
||||
// key constraint failed, but not which one! Messages have foreign keys to 3 tables:
|
||||
// conversations, participants (self), participants (sender). We'll query each one
|
||||
// to determine which one(s) violated the constraint, and then throw a new exception
|
||||
// with those details.
|
||||
|
||||
String foundConversationId = null;
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
// Look for an existing conversation in the db with the conversation id
|
||||
cursor = db.rawQuery("SELECT " + ConversationColumns._ID
|
||||
+ " FROM " + DatabaseHelper.CONVERSATIONS_TABLE
|
||||
+ " WHERE " + ConversationColumns._ID + "=" + conversationId,
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
Assert.isTrue(cursor.getCount() == 1);
|
||||
foundConversationId = cursor.getString(0);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
ParticipantData foundSelfParticipant =
|
||||
BugleDatabaseOperations.getExistingParticipant(db, selfId);
|
||||
ParticipantData foundSenderParticipant =
|
||||
BugleDatabaseOperations.getExistingParticipant(db, senderId);
|
||||
|
||||
String errorMsg = "SQLiteConstraintException while inserting message for " + messageUri
|
||||
+ "; conversation id from getOrCreateConversation = " + conversationId
|
||||
+ " (lookup thread = " + threadId + "), found conversation id = "
|
||||
+ foundConversationId + ", found self participant = "
|
||||
+ LogUtil.sanitizePII(foundSelfParticipant.getNormalizedDestination())
|
||||
+ " (lookup id = " + selfId + "), found sender participant = "
|
||||
+ LogUtil.sanitizePII(foundSenderParticipant.getNormalizedDestination())
|
||||
+ " (lookup id = " + senderId + ")";
|
||||
throw new RuntimeException(errorMsg, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the tracked latest message info to update conversations, including
|
||||
* latest chat message and sort timestamp.
|
||||
*/
|
||||
private void updateConversations(final DatabaseWrapper db) {
|
||||
for (final String conversationId : mConversationsToUpdate) {
|
||||
if (BugleDatabaseOperations.deleteConversationIfEmptyInTransaction(db,
|
||||
conversationId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final boolean archived = mCache.isArchived(conversationId);
|
||||
// Always attempt to auto-switch conversation self id for sync/import case.
|
||||
BugleDatabaseOperations.maybeRefreshConversationMetadataInTransaction(db,
|
||||
conversationId, true /*shouldAutoSwitchSelfId*/, archived /*keepArchived*/);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Batch delete database rows by matching a column with a list of values, usually some
|
||||
* kind of IDs.
|
||||
*
|
||||
* @param table
|
||||
* @param column
|
||||
* @param ids
|
||||
* @return Total number of deleted messages
|
||||
*/
|
||||
private static int batchDelete(final DatabaseWrapper db, final String table,
|
||||
final String column, final String[] ids) {
|
||||
int totalDeleted = 0;
|
||||
final int totalIds = ids.length;
|
||||
for (int start = 0; start < totalIds; start += MmsUtils.MAX_IDS_PER_QUERY) {
|
||||
final int end = Math.min(start + MmsUtils.MAX_IDS_PER_QUERY, totalIds); //excluding
|
||||
final int count = end - start;
|
||||
final String batchSelection = String.format(
|
||||
Locale.US,
|
||||
"%s IN %s",
|
||||
column,
|
||||
MmsUtils.getSqlInOperand(count));
|
||||
final String[] batchSelectionArgs = Arrays.copyOfRange(ids, start, end);
|
||||
final int deleted = db.delete(
|
||||
table,
|
||||
batchSelection,
|
||||
batchSelectionArgs);
|
||||
totalDeleted += deleted;
|
||||
}
|
||||
return totalDeleted;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user