Initial checkin of AOSP Messaging app.
b/23110861 Change-Id: I11db999bd10656801e618f78ab2b2ef74136fff1
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user