Test: make messaging Bug: 76692459 Change-Id: I9103450622f129311545648ac75f5b2f9fc2d04a Merged-In: I9103450622f129311545648ac75f5b2f9fc2d04a
713 lines
30 KiB
Java
713 lines
30 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.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 androidx.collection.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;
|
|
}
|
|
}
|