Initial checkin of AOSP Messaging app.

b/23110861

Change-Id: I11db999bd10656801e618f78ab2b2ef74136fff1
This commit is contained in:
Mike Dodd
2015-08-12 12:47:26 -07:00
parent ef8c7abbcf
commit d3b009ae55
1645 changed files with 186271 additions and 0 deletions
@@ -0,0 +1,712 @@
/*
* 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.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.provider.Telephony.Mms;
import android.provider.Telephony.Sms;
import android.support.v4.util.LongSparseArray;
import android.text.TextUtils;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.DatabaseHelper;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.SyncManager;
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
import com.android.messaging.datamodel.SyncManager.ThreadInfoCache;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.mmslib.SqliteWrapper;
import com.android.messaging.sms.DatabaseMessages;
import com.android.messaging.sms.DatabaseMessages.DatabaseMessage;
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 com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* Class holding a pair of cursors - one for local db and one for telephony provider - allowing
* synchronous stepping through messages as part of sync.
*/
class SyncCursorPair {
private static final String TAG = LogUtil.BUGLE_TAG;
static final long SYNC_COMPLETE = -1L;
static final long SYNC_STARTING = Long.MAX_VALUE;
private CursorIterator mLocalCursorIterator;
private CursorIterator mRemoteCursorsIterator;
private final String mLocalSelection;
private final String mRemoteSmsSelection;
private final String mRemoteMmsSelection;
/**
* Check if SMS has been synchronized. We compare the counts of messages on both
* sides and return true if they are equal.
*
* Note that this may not be the most reliable way to tell if messages are in sync.
* For example, the local misses one message and has one obsolete message.
* However, we have background sms sync once a while, also some other events might
* trigger a full sync. So we will eventually catch up. And this should be rare to
* happen.
*
* @return If sms is in sync with telephony sms/mms providers
*/
static boolean allSynchronized(final DatabaseWrapper db) {
return isSynchronized(db, LOCAL_MESSAGES_SELECTION, null,
getSmsTypeSelectionSql(), null, getMmsTypeSelectionSql(), null);
}
SyncCursorPair(final long lowerBound, final long upperBound) {
mLocalSelection = getTimeConstrainedQuery(
LOCAL_MESSAGES_SELECTION,
MessageColumns.RECEIVED_TIMESTAMP,
lowerBound,
upperBound,
null /* threadColumn */, null /* threadId */);
mRemoteSmsSelection = getTimeConstrainedQuery(
getSmsTypeSelectionSql(),
"date",
lowerBound,
upperBound,
null /* threadColumn */, null /* threadId */);
mRemoteMmsSelection = getTimeConstrainedQuery(
getMmsTypeSelectionSql(),
"date",
((lowerBound < 0) ? lowerBound : (lowerBound + 999) / 1000), /*seconds*/
((upperBound < 0) ? upperBound : (upperBound + 999) / 1000), /*seconds*/
null /* threadColumn */, null /* threadId */);
}
SyncCursorPair(final long threadId, final String conversationId) {
mLocalSelection = getTimeConstrainedQuery(
LOCAL_MESSAGES_SELECTION,
MessageColumns.RECEIVED_TIMESTAMP,
-1L,
-1L,
MessageColumns.CONVERSATION_ID, conversationId);
// Find all SMS messages (excluding drafts) within the sync window
mRemoteSmsSelection = getTimeConstrainedQuery(
getSmsTypeSelectionSql(),
"date",
-1L,
-1L,
Sms.THREAD_ID, Long.toString(threadId));
mRemoteMmsSelection = getTimeConstrainedQuery(
getMmsTypeSelectionSql(),
"date",
-1L, /*seconds*/
-1L, /*seconds*/
Mms.THREAD_ID, Long.toString(threadId));
}
void query(final DatabaseWrapper db) {
// Load local messages in the sync window
mLocalCursorIterator = new LocalCursorIterator(db, mLocalSelection);
// Load remote messages in the sync window
mRemoteCursorsIterator = new RemoteCursorsIterator(mRemoteSmsSelection,
mRemoteMmsSelection);
}
boolean isSynchronized(final DatabaseWrapper db) {
return isSynchronized(db, mLocalSelection, null, mRemoteSmsSelection,
null, mRemoteMmsSelection, null);
}
void close() {
if (mLocalCursorIterator != null) {
mLocalCursorIterator.close();
}
if (mRemoteCursorsIterator != null) {
mRemoteCursorsIterator.close();
}
}
long scan(final int maxMessagesToScan,
final int maxMessagesToUpdate, final ArrayList<SmsMessage> smsToAdd,
final LongSparseArray<MmsMessage> mmsToAdd,
final ArrayList<LocalDatabaseMessage> messagesToDelete,
final SyncManager.ThreadInfoCache threadInfoCache) {
// Set of local messages matched with the timestamp of a remote message
final Set<DatabaseMessage> matchedLocalMessages = Sets.newHashSet();
// Set of remote messages matched with the timestamp of a local message
final Set<DatabaseMessage> matchedRemoteMessages = Sets.newHashSet();
long lastTimestampMillis = SYNC_STARTING;
// Number of messages scanned local and remote
int localCount = 0;
int remoteCount = 0;
// Seed the initial values of remote and local messages for comparison
DatabaseMessage remoteMessage = mRemoteCursorsIterator.next();
DatabaseMessage localMessage = mLocalCursorIterator.next();
// Iterate through messages on both sides in reverse time order
// Import messages in remote not in local, delete messages in local not in remote
while (localCount + remoteCount < maxMessagesToScan && smsToAdd.size()
+ mmsToAdd.size() + messagesToDelete.size() < maxMessagesToUpdate) {
if (remoteMessage == null && localMessage == null) {
// No more message on both sides - scan complete
lastTimestampMillis = SYNC_COMPLETE;
break;
} else if ((remoteMessage == null && localMessage != null) ||
(localMessage != null && remoteMessage != null &&
localMessage.getTimestampInMillis()
> remoteMessage.getTimestampInMillis())) {
// Found a local message that is not in remote db
// Delete the local message
messagesToDelete.add((LocalDatabaseMessage) localMessage);
lastTimestampMillis = Math.min(lastTimestampMillis,
localMessage.getTimestampInMillis());
// Advance to next local message
localMessage = mLocalCursorIterator.next();
localCount += 1;
} else if ((localMessage == null && remoteMessage != null) ||
(localMessage != null && remoteMessage != null &&
localMessage.getTimestampInMillis()
< remoteMessage.getTimestampInMillis())) {
// Found a remote message that is not in local db
// Add the remote message
saveMessageToAdd(smsToAdd, mmsToAdd, remoteMessage, threadInfoCache);
lastTimestampMillis = Math.min(lastTimestampMillis,
remoteMessage.getTimestampInMillis());
// Advance to next remote message
remoteMessage = mRemoteCursorsIterator.next();
remoteCount += 1;
} else {
// Found remote and local messages at the same timestamp
final long matchedTimestamp = localMessage.getTimestampInMillis();
lastTimestampMillis = Math.min(lastTimestampMillis, matchedTimestamp);
// Get the next local and remote messages
final DatabaseMessage remoteMessagePeek = mRemoteCursorsIterator.next();
final DatabaseMessage localMessagePeek = mLocalCursorIterator.next();
// Check if only one message on each side matches the current timestamp
// by looking at the next messages on both sides. If they are either null
// (meaning no more messages) or having a different timestamp. We want
// to optimize for this since this is the most common case when majority
// of the messages are in sync (so they one-to-one pair up at each timestamp),
// by not allocating the data structures required to compare a set of
// messages from both sides.
if ((remoteMessagePeek == null ||
remoteMessagePeek.getTimestampInMillis() != matchedTimestamp) &&
(localMessagePeek == null ||
localMessagePeek.getTimestampInMillis() != matchedTimestamp)) {
// Optimize the common case where only one message on each side
// that matches the same timestamp
if (!remoteMessage.equals(localMessage)) {
// local != remote
// Delete local message
messagesToDelete.add((LocalDatabaseMessage) localMessage);
// Add remote message
saveMessageToAdd(smsToAdd, mmsToAdd, remoteMessage, threadInfoCache);
}
// Get next local and remote messages
localMessage = localMessagePeek;
remoteMessage = remoteMessagePeek;
localCount += 1;
remoteCount += 1;
} else {
// Rare case in which multiple messages are in the same timestamp
// on either or both sides
// Gather all the matched remote messages
matchedRemoteMessages.clear();
matchedRemoteMessages.add(remoteMessage);
remoteCount += 1;
remoteMessage = remoteMessagePeek;
while (remoteMessage != null &&
remoteMessage.getTimestampInMillis() == matchedTimestamp) {
Assert.isTrue(!matchedRemoteMessages.contains(remoteMessage));
matchedRemoteMessages.add(remoteMessage);
remoteCount += 1;
remoteMessage = mRemoteCursorsIterator.next();
}
// Gather all the matched local messages
matchedLocalMessages.clear();
matchedLocalMessages.add(localMessage);
localCount += 1;
localMessage = localMessagePeek;
while (localMessage != null &&
localMessage.getTimestampInMillis() == matchedTimestamp) {
if (matchedLocalMessages.contains(localMessage)) {
// Duplicate message is local database is deleted
messagesToDelete.add((LocalDatabaseMessage) localMessage);
} else {
matchedLocalMessages.add(localMessage);
}
localCount += 1;
localMessage = mLocalCursorIterator.next();
}
// Delete messages local only
for (final DatabaseMessage msg : Sets.difference(
matchedLocalMessages, matchedRemoteMessages)) {
messagesToDelete.add((LocalDatabaseMessage) msg);
}
// Add messages remote only
for (final DatabaseMessage msg : Sets.difference(
matchedRemoteMessages, matchedLocalMessages)) {
saveMessageToAdd(smsToAdd, mmsToAdd, msg, threadInfoCache);
}
}
}
}
return lastTimestampMillis;
}
DatabaseMessage getLocalMessage() {
return mLocalCursorIterator.next();
}
DatabaseMessage getRemoteMessage() {
return mRemoteCursorsIterator.next();
}
int getLocalPosition() {
return mLocalCursorIterator.getPosition();
}
int getRemotePosition() {
return mRemoteCursorsIterator.getPosition();
}
int getLocalCount() {
return mLocalCursorIterator.getCount();
}
int getRemoteCount() {
return mRemoteCursorsIterator.getCount();
}
/**
* An iterator for a database cursor
*/
interface CursorIterator {
/**
* Move to next element in the cursor
*
* @return The next element (which becomes the current)
*/
public DatabaseMessage next();
/**
* Close the cursor
*/
public void close();
/**
* Get the position
*/
public int getPosition();
/**
* Get the count
*/
public int getCount();
}
private static final String ORDER_BY_DATE_DESC = "date DESC";
// A subquery that selects SMS/MMS messages in Bugle which are also in telephony
private static final String LOCAL_MESSAGES_SELECTION = String.format(
Locale.US,
"(%s NOTNULL)",
MessageColumns.SMS_MESSAGE_URI);
private static final String ORDER_BY_TIMESTAMP_DESC =
MessageColumns.RECEIVED_TIMESTAMP + " DESC";
// TODO : This should move into the provider
private static class LocalMessageQuery {
private static final String[] PROJECTION = new String[] {
MessageColumns._ID,
MessageColumns.RECEIVED_TIMESTAMP,
MessageColumns.SMS_MESSAGE_URI,
MessageColumns.PROTOCOL,
MessageColumns.CONVERSATION_ID,
};
private static final int INDEX_MESSAGE_ID = 0;
private static final int INDEX_MESSAGE_TIMESTAMP = 1;
private static final int INDEX_SMS_MESSAGE_URI = 2;
private static final int INDEX_MESSAGE_SMS_TYPE = 3;
private static final int INDEX_CONVERSATION_ID = 4;
}
/**
* This class provides the same DatabaseMessage interface over a local SMS db message
*/
private static LocalDatabaseMessage getLocalDatabaseMessage(final Cursor cursor) {
if (cursor == null) {
return null;
}
return new LocalDatabaseMessage(
cursor.getLong(LocalMessageQuery.INDEX_MESSAGE_ID),
cursor.getInt(LocalMessageQuery.INDEX_MESSAGE_SMS_TYPE),
cursor.getString(LocalMessageQuery.INDEX_SMS_MESSAGE_URI),
cursor.getLong(LocalMessageQuery.INDEX_MESSAGE_TIMESTAMP),
cursor.getString(LocalMessageQuery.INDEX_CONVERSATION_ID));
}
/**
* The buffered cursor iterator for local SMS
*/
private static class LocalCursorIterator implements CursorIterator {
private Cursor mCursor;
private final DatabaseWrapper mDatabase;
LocalCursorIterator(final DatabaseWrapper database, final String selection)
throws SQLiteException {
mDatabase = database;
try {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "SyncCursorPair: Querying for local messages; selection = "
+ selection);
}
mCursor = mDatabase.query(
DatabaseHelper.MESSAGES_TABLE,
LocalMessageQuery.PROJECTION,
selection,
null /*selectionArgs*/,
null/*groupBy*/,
null/*having*/,
ORDER_BY_TIMESTAMP_DESC);
} catch (final SQLiteException e) {
LogUtil.e(TAG, "SyncCursorPair: failed to query local sms/mms", e);
// Can't query local database. So let's throw up the exception and abort sync
// because we may end up import duplicate messages.
throw e;
}
}
@Override
public DatabaseMessage next() {
if (mCursor != null && mCursor.moveToNext()) {
return getLocalDatabaseMessage(mCursor);
}
return null;
}
@Override
public int getCount() {
return (mCursor == null ? 0 : mCursor.getCount());
}
@Override
public int getPosition() {
return (mCursor == null ? 0 : mCursor.getPosition());
}
@Override
public void close() {
if (mCursor != null) {
mCursor.close();
mCursor = null;
}
}
}
/**
* The cursor iterator for remote sms.
* Since SMS and MMS are stored in different tables in telephony provider,
* this class merges the two cursors and provides a unified view of messages
* from both cursors. Note that the order is DESC.
*/
private static class RemoteCursorsIterator implements CursorIterator {
private Cursor mSmsCursor;
private Cursor mMmsCursor;
private DatabaseMessage mNextSms;
private DatabaseMessage mNextMms;
RemoteCursorsIterator(final String smsSelection, final String mmsSelection)
throws SQLiteException {
mSmsCursor = null;
mMmsCursor = null;
try {
final Context context = Factory.get().getApplicationContext();
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "SyncCursorPair: Querying for remote SMS; selection = "
+ smsSelection);
}
mSmsCursor = SqliteWrapper.query(
context,
context.getContentResolver(),
Sms.CONTENT_URI,
SmsMessage.getProjection(),
smsSelection,
null /* selectionArgs */,
ORDER_BY_DATE_DESC);
if (mSmsCursor == null) {
LogUtil.w(TAG, "SyncCursorPair: Remote SMS query returned null cursor; "
+ "need to cancel sync");
throw new RuntimeException("Null cursor from remote SMS query");
}
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "SyncCursorPair: Querying for remote MMS; selection = "
+ mmsSelection);
}
mMmsCursor = SqliteWrapper.query(
context,
context.getContentResolver(),
Mms.CONTENT_URI,
DatabaseMessages.MmsMessage.getProjection(),
mmsSelection,
null /* selectionArgs */,
ORDER_BY_DATE_DESC);
if (mMmsCursor == null) {
LogUtil.w(TAG, "SyncCursorPair: Remote MMS query returned null cursor; "
+ "need to cancel sync");
throw new RuntimeException("Null cursor from remote MMS query");
}
// Move to the first element in the combined stream from both cursors
mNextSms = getSmsCursorNext();
mNextMms = getMmsCursorNext();
} catch (final SQLiteException e) {
LogUtil.e(TAG, "SyncCursorPair: failed to query remote messages", e);
// If we ignore this, the following code would think there is no remote message
// and will delete all the local sms. We should be cautious here. So instead,
// let's throw the exception to the caller and abort sms sync. We do the same
// thing if either of the remote cursors is null.
throw e;
}
}
@Override
public DatabaseMessage next() {
DatabaseMessage result = null;
if (mNextSms != null && mNextMms != null) {
if (mNextSms.getTimestampInMillis() >= mNextMms.getTimestampInMillis()) {
result = mNextSms;
mNextSms = getSmsCursorNext();
} else {
result = mNextMms;
mNextMms = getMmsCursorNext();
}
} else {
if (mNextSms != null) {
result = mNextSms;
mNextSms = getSmsCursorNext();
} else {
result = mNextMms;
mNextMms = getMmsCursorNext();
}
}
return result;
}
private DatabaseMessage getSmsCursorNext() {
if (mSmsCursor != null && mSmsCursor.moveToNext()) {
return SmsMessage.get(mSmsCursor);
}
return null;
}
private DatabaseMessage getMmsCursorNext() {
if (mMmsCursor != null && mMmsCursor.moveToNext()) {
return MmsMessage.get(mMmsCursor);
}
return null;
}
@Override
// Return approximate cursor position allowing for read ahead on two cursors (hence -1)
public int getPosition() {
return (mSmsCursor == null ? 0 : mSmsCursor.getPosition()) +
(mMmsCursor == null ? 0 : mMmsCursor.getPosition()) - 1;
}
@Override
public int getCount() {
return (mSmsCursor == null ? 0 : mSmsCursor.getCount()) +
(mMmsCursor == null ? 0 : mMmsCursor.getCount());
}
@Override
public void close() {
if (mSmsCursor != null) {
mSmsCursor.close();
mSmsCursor = null;
}
if (mMmsCursor != null) {
mMmsCursor.close();
mMmsCursor = null;
}
}
}
/**
* Type selection for importing sms messages. Only SENT and INBOX messages are imported.
*
* @return The SQL selection for importing sms messages
*/
public static String getSmsTypeSelectionSql() {
return MmsUtils.getSmsTypeSelectionSql();
}
/**
* Type selection for importing mms messages.
*
* Criteria:
* MESSAGE_BOX is INBOX, SENT or OUTBOX
* MESSAGE_TYPE is SEND_REQ (sent), RETRIEVE_CONF (received) or NOTIFICATION_IND (download)
*
* @return The SQL selection for importing mms messages. This selects the message type,
* not including the selection on timestamp.
*/
public static String getMmsTypeSelectionSql() {
return MmsUtils.getMmsTypeSelectionSql();
}
/**
* Get a SQL selection string using an existing selection and time window limits
* The limits are not applied if the value is < 0
*
* @param typeSelection The existing selection
* @param from The inclusive lower bound
* @param to The exclusive upper bound
* @return The created SQL selection
*/
private static String getTimeConstrainedQuery(final String typeSelection,
final String timeColumn, final long from, final long to,
final String threadColumn, final String threadId) {
final StringBuilder queryBuilder = new StringBuilder();
queryBuilder.append(typeSelection);
if (from > 0) {
queryBuilder.append(" AND ").append(timeColumn).append(">=").append(from);
}
if (to > 0) {
queryBuilder.append(" AND ").append(timeColumn).append("<").append(to);
}
if (!TextUtils.isEmpty(threadColumn) && !TextUtils.isEmpty(threadId)) {
queryBuilder.append(" AND ").append(threadColumn).append("=").append(threadId);
}
return queryBuilder.toString();
}
private static final String[] COUNT_PROJECTION = new String[] { "count()" };
private static int getCountFromCursor(final Cursor cursor) {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getInt(0);
}
// We should only return a number if we were able to read it from the cursor.
// Otherwise, we throw an exception to cancel the sync.
String cursorDesc = "";
if (cursor == null) {
cursorDesc = "null";
} else if (cursor.getCount() == 0) {
cursorDesc = "empty";
}
throw new IllegalArgumentException("Cannot get count from " + cursorDesc + " cursor");
}
private void saveMessageToAdd(final List<SmsMessage> smsToAdd,
final LongSparseArray<MmsMessage> mmsToAdd, final DatabaseMessage message,
final ThreadInfoCache threadInfoCache) {
long threadId;
if (message.getProtocol() == MessageData.PROTOCOL_MMS) {
final MmsMessage mms = (MmsMessage) message;
mmsToAdd.append(mms.getId(), mms);
threadId = mms.mThreadId;
} else {
final SmsMessage sms = (SmsMessage) message;
smsToAdd.add(sms);
threadId = sms.mThreadId;
}
// Cache the lookup and canonicalization of the phone number outside of the transaction...
threadInfoCache.getThreadRecipients(threadId);
}
/**
* Check if SMS has been synchronized. We compare the counts of messages on both
* sides and return true if they are equal.
*
* Note that this may not be the most reliable way to tell if messages are in sync.
* For example, the local misses one message and has one obsolete message.
* However, we have background sms sync once a while, also some other events might
* trigger a full sync. So we will eventually catch up. And this should be rare to
* happen.
*
* @return If sms is in sync with telephony sms/mms providers
*/
private static boolean isSynchronized(final DatabaseWrapper db, final String localSelection,
final String[] localSelectionArgs, final String smsSelection,
final String[] smsSelectionArgs, final String mmsSelection,
final String[] mmsSelectionArgs) {
final Context context = Factory.get().getApplicationContext();
Cursor localCursor = null;
Cursor remoteSmsCursor = null;
Cursor remoteMmsCursor = null;
try {
localCursor = db.query(
DatabaseHelper.MESSAGES_TABLE,
COUNT_PROJECTION,
localSelection,
localSelectionArgs,
null/*groupBy*/,
null/*having*/,
null/*orderBy*/);
final int localCount = getCountFromCursor(localCursor);
remoteSmsCursor = SqliteWrapper.query(
context,
context.getContentResolver(),
Sms.CONTENT_URI,
COUNT_PROJECTION,
smsSelection,
smsSelectionArgs,
null/*orderBy*/);
final int smsCount = getCountFromCursor(remoteSmsCursor);
remoteMmsCursor = SqliteWrapper.query(
context,
context.getContentResolver(),
Mms.CONTENT_URI,
COUNT_PROJECTION,
mmsSelection,
mmsSelectionArgs,
null/*orderBy*/);
final int mmsCount = getCountFromCursor(remoteMmsCursor);
final int remoteCount = smsCount + mmsCount;
final boolean isInSync = (localCount == remoteCount);
if (isInSync) {
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "SyncCursorPair: Same # of local and remote messages = "
+ localCount);
}
} else {
LogUtil.i(TAG, "SyncCursorPair: Not in sync; # local messages = " + localCount
+ ", # remote message = " + remoteCount);
}
return isInSync;
} catch (final Exception e) {
LogUtil.e(TAG, "SyncCursorPair: failed to query local or remote message counts", e);
// If something is wrong in querying database, assume we are synced so
// we don't retry indefinitely
} finally {
if (localCursor != null) {
localCursor.close();
}
if (remoteSmsCursor != null) {
remoteSmsCursor.close();
}
if (remoteMmsCursor != null) {
remoteMmsCursor.close();
}
}
return true;
}
}