Do a comparative sync between local and server voicemails.
Currently a sync will completely wipe local voicemails and replace them
with the server voicemails. Instead, we will do a comparative sync to be
less disruptive to the user, and properly notify when a new voicemail is
available on the server. The changes are as follows:
* Rename DirtyVoicemailQuery to VoicemailsQueryHelper to handle all
queries related to the voicemail provider.
* Implement the comparative sync logic in OmtpVvmSyncService
* Fixed bug when receiving mailbox update sync message in
WrappedMessageData (tried to parse a field that did not exist)
Bug: 20764449
Change-Id: Iaae2a9fe7184e827d72baf1d5d5a72a36b3124a6
diff --git a/src/com/android/phone/vvm/omtp/OmtpVvmSyncService.java b/src/com/android/phone/vvm/omtp/OmtpVvmSyncService.java
index fc444ba..c9a1c32 100644
--- a/src/com/android/phone/vvm/omtp/OmtpVvmSyncService.java
+++ b/src/com/android/phone/vvm/omtp/OmtpVvmSyncService.java
@@ -26,24 +26,20 @@
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
-import android.database.Cursor;
-import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.provider.VoicemailContract;
-import android.provider.VoicemailContract.Voicemails;
import android.telecom.Voicemail;
import com.android.phone.vvm.omtp.imap.ImapHelper;
-import com.android.phone.vvm.omtp.sync.DirtyVoicemailQuery;
+import com.android.phone.vvm.omtp.sync.VoicemailsQueryHelper;
-import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* A service to run the VvmSyncAdapter.
@@ -73,106 +69,81 @@
* Sync triggers should pass this extra to clear the database and freshly populate from the
* server.
*/
- public static final String SYNC_EXTRAS_CLEAR_AND_RELOAD = "extra_clear_and_reload";
+ public static final String SYNC_EXTRAS_DOWNLOAD = "extra_download";
private Context mContext;
- private ContentResolver mContentResolver;
public OmtpVvmSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
mContext = context;
- mContentResolver = context.getContentResolver();
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority,
ContentProviderClient provider, SyncResult syncResult) {
ImapHelper imapHelper = new ImapHelper(mContext, account);
+ VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false)) {
- List<Voicemail> readVoicemails = new ArrayList<Voicemail>();
- List<Voicemail> deletedVoicemails = new ArrayList<Voicemail>();
+ List<Voicemail> readVoicemails = queryHelper.getReadVoicemails();
+ List<Voicemail> deletedVoicemails = queryHelper.getDeletedVoicemails();
- Cursor cursor = DirtyVoicemailQuery.getDirtyVoicemails(mContext);
- if (cursor == null) {
- return;
- }
- try {
- while (cursor.moveToNext()) {
- final long id = cursor.getLong(DirtyVoicemailQuery._ID);
- final String sourceData = cursor.getString(DirtyVoicemailQuery.SOURCE_DATA);
- final boolean isRead = cursor.getInt(DirtyVoicemailQuery.IS_READ) == 1;
- final boolean deleted = cursor.getInt(DirtyVoicemailQuery.DELETED) == 1;
- Voicemail voicemail = Voicemail.createForUpdate(id, sourceData).build();
- if (deleted) {
- // Check deleted first because if the voicemail is deleted, there's no
- // need to mark as read.
- deletedVoicemails.add(voicemail);
- } else if (isRead) {
- readVoicemails.add(voicemail);
- }
- }
- } finally {
- cursor.close();
- }
- if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
+ if (deletedVoicemails != null &&
+ imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
// We want to delete selectively instead of all the voicemails for this provider
// in case the state changed since the IMAP query was completed.
- deleteFromDatabase(deletedVoicemails);
+ queryHelper.deleteFromDatabase(deletedVoicemails);
}
- if (imapHelper.markMessagesAsRead(readVoicemails)) {
- markReadInDatabase(readVoicemails);
+ if (readVoicemails != null && imapHelper.markMessagesAsRead(readVoicemails)) {
+ queryHelper.markReadInDatabase(readVoicemails);
}
}
- if (extras.getBoolean(SYNC_EXTRAS_CLEAR_AND_RELOAD, false)) {
- // Fetch voicemails first before deleting local copy, the fetching may take awhile.
- List<Voicemail> voicemails = imapHelper.fetchAllVoicemails();
+ if (extras.getBoolean(SYNC_EXTRAS_DOWNLOAD, false)) {
+ List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails();
+ List<Voicemail> localVoicemails = queryHelper.getAllVoicemails();
- // Deleting current local messages ensure that we start with a fresh copy
- // and also don't need to deal with comparing between local and server.
- VoicemailContract.Voicemails.deleteAll(mContext);
+ if (localVoicemails == null || serverVoicemails == null) {
+ // Null value means the query failed.
+ return;
+ }
- if (voicemails != null) {
- VoicemailContract.Voicemails.insert(mContext, voicemails);
+ Map<String, Voicemail> remoteMap = buildMap(serverVoicemails);
+
+ // Go through all the local voicemails and check if they are on the server.
+ // They may be read or deleted on the server but not locally. Perform the
+ // appropriate local operation if the status differs from the server. Remove
+ // the messages that exist both locally and on the server to know which server
+ // messages to insert locally.
+ for (int i = 0; i < localVoicemails.size(); i++) {
+ Voicemail localVoicemail = localVoicemails.get(i);
+ Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData());
+ if (remoteVoicemail == null) {
+ queryHelper.deleteFromDatabase(localVoicemail);
+ } else {
+ if (remoteVoicemail.isRead() != localVoicemail.isRead()) {
+ queryHelper.markReadInDatabase(localVoicemail);
+ }
+ }
+ }
+
+ // The leftover messages are messages that exist on the server but not locally.
+ for (Voicemail remoteVoicemail : remoteMap.values()) {
+ VoicemailContract.Voicemails.insert(mContext, remoteVoicemail);
}
}
}
/**
- * Deletes a list of voicemails from the voicemail content provider.
- *
- * @param voicemails The list of voicemails to delete
- * @return The number of voicemails deleted
+ * Builds a map from provider data to message for the given collection of voicemails.
*/
- public int deleteFromDatabase(List<Voicemail> voicemails) {
- int count = voicemails.size();
- for (int i = 0; i < count; i++) {
- mContentResolver.delete(Voicemails.CONTENT_URI, Voicemails._ID + "=?",
- new String[] { Long.toString(voicemails.get(i).getId()) });
+ private Map<String, Voicemail> buildMap(List<Voicemail> messages) {
+ Map<String, Voicemail> map = new HashMap<String, Voicemail>();
+ for (Voicemail message : messages) {
+ map.put(message.getSourceData(), message);
}
- return count;
- }
-
- /**
- * Sends an update command to the voicemail content provider for a list of voicemails.
- * From the view of the provider, since the updater is the owner of the entry, a blank
- * "update" means that the voicemail source is indicating that the server has up-to-date
- * information on the voicemail. This flips the "dirty" bit to "0".
- *
- * @param voicemails The list of voicemails to update
- * @return The number of voicemails updated
- */
- public int markReadInDatabase(List<Voicemail> voicemails) {
- int count = voicemails.size();
- for (int i = 0; i < count; i++) {
- Uri uri = ContentUris.withAppendedId(
- VoicemailContract.Voicemails.buildSourceUri(mContext.getPackageName()),
- voicemails.get(i).getId());
- mContentResolver.update(uri, new ContentValues(), null, null);
- }
- return count;
+ return map;
}
}
}
diff --git a/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java b/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
index 57979ff..1270658 100644
--- a/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
+++ b/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
@@ -34,8 +34,6 @@
import com.android.phone.vvm.omtp.OmtpVvmSyncAccountManager;
import com.android.phone.vvm.omtp.OmtpVvmSyncService.OmtpVvmSyncAdapter;
-import java.io.UnsupportedEncodingException;
-
/**
* Receive SMS messages and send for processing by the OMTP visual voicemail source.
*/
@@ -96,7 +94,7 @@
case OmtpConstants.MAILBOX_UPDATE:
// Needs a total resync
Bundle bundle = new Bundle();
- bundle.putBoolean(OmtpVvmSyncAdapter.SYNC_EXTRAS_CLEAR_AND_RELOAD, true);
+ bundle.putBoolean(OmtpVvmSyncAdapter.SYNC_EXTRAS_DOWNLOAD, true);
ContentResolver.requestSync(
new Account(mPhoneAccount.getId(), OmtpVvmSyncAccountManager.ACCOUNT_TYPE),
VoicemailContract.AUTHORITY, bundle);
@@ -105,7 +103,7 @@
// Not implemented in V1
break;
default:
- Log.e(TAG, "Unrecognized sync trigger event: "+message.getSyncTriggerEvent());
+ Log.e(TAG, "Unrecognized sync trigger event: " + message.getSyncTriggerEvent());
break;
}
}
@@ -126,7 +124,7 @@
VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_OK);
Bundle bundle = new Bundle();
- bundle.putBoolean(OmtpVvmSyncAdapter.SYNC_EXTRAS_CLEAR_AND_RELOAD, true);
+ bundle.putBoolean(OmtpVvmSyncAdapter.SYNC_EXTRAS_DOWNLOAD, true);
bundle.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true);
ContentResolver.requestSync(account, VoicemailContract.AUTHORITY, bundle);
}
diff --git a/src/com/android/phone/vvm/omtp/sms/WrappedMessageData.java b/src/com/android/phone/vvm/omtp/sms/WrappedMessageData.java
index 109dfb2..b4c86d4 100644
--- a/src/com/android/phone/vvm/omtp/sms/WrappedMessageData.java
+++ b/src/com/android/phone/vvm/omtp/sms/WrappedMessageData.java
@@ -66,6 +66,10 @@
*/
String extractString(final String field) {
String value = mFields.get(field);
+ if (value == null) {
+ return null;
+ }
+
String[] possibleValues = OmtpConstants.possibleValuesMap.get(field);
if (possibleValues == null) {
return value;
diff --git a/src/com/android/phone/vvm/omtp/sync/DirtyVoicemailQuery.java b/src/com/android/phone/vvm/omtp/sync/DirtyVoicemailQuery.java
deleted file mode 100644
index 8c70372..0000000
--- a/src/com/android/phone/vvm/omtp/sync/DirtyVoicemailQuery.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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.phone.vvm.omtp.sync;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.VoicemailContract;
-import android.provider.VoicemailContract.Voicemails;
-
-/**
- * Construct a query to get dirty voicemails.
- */
-public class DirtyVoicemailQuery {
- final static String[] PROJECTION = new String[] {
- Voicemails._ID, // 0
- Voicemails.SOURCE_DATA, // 1
- Voicemails.IS_READ, // 2
- Voicemails.DELETED, // 3
- };
-
- public static final int _ID = 0;
- public static final int SOURCE_DATA = 1;
- public static final int IS_READ = 2;
- public static final int DELETED = 3;
-
- final static String SELECTION = Voicemails.DIRTY + "=1";
-
- /**
- * Get all the locally modified voicemails that have not been synced to the server.
- *
- * @param context The context from the package calling the method. This will be the source.
- * @return A list of all locally modified voicemails.
- */
- public static Cursor getDirtyVoicemails(Context context) {
- ContentResolver contentResolver = context.getContentResolver();
- Uri sourceUri = VoicemailContract.Voicemails.buildSourceUri(context.getPackageName());
- return contentResolver.query(sourceUri, PROJECTION, SELECTION, null, null);
- }
-}
diff --git a/src/com/android/phone/vvm/omtp/sync/VoicemailsQueryHelper.java b/src/com/android/phone/vvm/omtp/sync/VoicemailsQueryHelper.java
new file mode 100644
index 0000000..151afed
--- /dev/null
+++ b/src/com/android/phone/vvm/omtp/sync/VoicemailsQueryHelper.java
@@ -0,0 +1,171 @@
+/*
+ * 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.phone.vvm.omtp.sync;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.telecom.Voicemail;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Construct a queries to interact with the voicemails table.
+ */
+public class VoicemailsQueryHelper {
+ final static String[] PROJECTION = new String[] {
+ Voicemails._ID, // 0
+ Voicemails.SOURCE_DATA, // 1
+ Voicemails.IS_READ, // 2
+ Voicemails.DELETED, // 3
+ };
+
+ public static final int _ID = 0;
+ public static final int SOURCE_DATA = 1;
+ public static final int IS_READ = 2;
+ public static final int DELETED = 3;
+
+ final static String READ_SELECTION = Voicemails.DIRTY + "=1 AND " + Voicemails.DELETED + "!=1";
+ final static String DELETED_SELECTION = Voicemails.DELETED + "=1";
+
+ private Context mContext;
+ private ContentResolver mContentResolver;
+ private Uri mSourceUri;
+
+ public VoicemailsQueryHelper(Context context) {
+ mContext = context;
+ mContentResolver = context.getContentResolver();
+ mSourceUri = VoicemailContract.Voicemails.buildSourceUri(mContext.getPackageName());
+ }
+
+ /**
+ * Get all the local read voicemails that have not been synced to the server.
+ *
+ * @return A list of read voicemails.
+ */
+ public List<Voicemail> getReadVoicemails() {
+ return getLocalVoicemails(READ_SELECTION);
+ }
+
+ /**
+ * Get all the locally deleted voicemails that have not been synced to the server.
+ *
+ * @return A list of deleted voicemails.
+ */
+ public List<Voicemail> getDeletedVoicemails() {
+ return getLocalVoicemails(DELETED_SELECTION);
+ }
+
+ /**
+ * Get all voicemails locally stored.
+ *
+ * @return A list of all locally stored voicemails.
+ */
+ public List<Voicemail> getAllVoicemails() {
+ return getLocalVoicemails(null);
+ }
+
+ /**
+ * Utility method to make queries to the voicemail database.
+ *
+ * @param selection A filter declaring which rows to return. {@code null} returns all rows.
+ * @return A list of voicemails according to the selection statement.
+ */
+ private List<Voicemail> getLocalVoicemails(String selection) {
+ Cursor cursor = mContentResolver.query(mSourceUri, PROJECTION, selection, null, null);
+ if (cursor == null) {
+ return null;
+ }
+ try {
+ List<Voicemail> voicemails = new ArrayList<Voicemail>();
+ while (cursor.moveToNext()) {
+ final long id = cursor.getLong(VoicemailsQueryHelper._ID);
+ final String sourceData = cursor.getString(VoicemailsQueryHelper.SOURCE_DATA);
+ Voicemail voicemail = Voicemail.createForUpdate(id, sourceData).build();
+ voicemails.add(voicemail);
+ }
+ return voicemails;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Deletes a list of voicemails from the voicemail content provider.
+ *
+ * @param voicemails The list of voicemails to delete
+ * @return The number of voicemails deleted
+ */
+ public int deleteFromDatabase(List<Voicemail> voicemails) {
+ int count = voicemails.size();
+ if (count == 0) {
+ return 0;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < count; i++) {
+ if (i > 0) {
+ sb.append(",");
+ }
+ sb.append(voicemails.get(i).getId());
+ }
+
+ String selectionStatement = String.format(Voicemails._ID + " IN (%s)", sb.toString());
+ return mContentResolver.delete(Voicemails.CONTENT_URI, selectionStatement, null);
+ }
+
+ /**
+ * Utility method to delete a single voicemail.
+ */
+ public void deleteFromDatabase(Voicemail voicemail) {
+ mContentResolver.delete(Voicemails.CONTENT_URI, Voicemails._ID + "=?",
+ new String[] { Long.toString(voicemail.getId()) });
+ }
+
+ /**
+ * Sends an update command to the voicemail content provider for a list of voicemails.
+ * From the view of the provider, since the updater is the owner of the entry, a blank
+ * "update" means that the voicemail source is indicating that the server has up-to-date
+ * information on the voicemail. This flips the "dirty" bit to "0".
+ *
+ * @param voicemails The list of voicemails to update
+ * @return The number of voicemails updated
+ */
+ public int markReadInDatabase(List<Voicemail> voicemails) {
+ int count = voicemails.size();
+ for (int i = 0; i < count; i++) {
+ markReadInDatabase(voicemails.get(i));
+ }
+ return count;
+ }
+
+ /**
+ * Utility method to mark single message as read.
+ */
+ public void markReadInDatabase(Voicemail voicemail) {
+ Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Voicemails.IS_READ, "1");
+ mContentResolver.update(uri, contentValues, null, null);
+ }
+}