Implement undoing ContactSaveService join operation
Add intent for splitting a newly joined contact into its previous
groupings of rawContactIds.
Join several contacts intent accepts a receiver, and will send it
the rawContactids of the contacts that were joined.
Bug:28622101
Change-Id: I748ab7b817396be4ad42d0094e0adab1cc7192ee
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 5c9c899..08eb1c7 100755
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -45,6 +45,7 @@
import android.provider.ContactsContract.Profile;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.RawContactsEntity;
+import android.support.v4.os.ResultReceiver;
import android.util.Log;
import android.widget.Toast;
@@ -59,7 +60,6 @@
import com.android.contacts.common.model.account.AccountWithDataSet;
import com.android.contacts.common.util.PermissionsUtil;
import com.android.contacts.compat.PinnedPositionsCompat;
-import com.android.contacts.activities.ContactEditorBaseActivity.ContactEditor.SaveMode;
import com.android.contacts.util.ContactPhotoUtils;
import com.google.common.collect.Lists;
@@ -86,6 +86,8 @@
public static final String EXTRA_DATA_SET = "dataSet";
public static final String EXTRA_CONTENT_VALUES = "contentValues";
public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
+ public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
+ public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
public static final String ACTION_SAVE_CONTACT = "saveContact";
public static final String EXTRA_CONTACT_STATE = "state";
@@ -114,6 +116,8 @@
public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
public static final String EXTRA_DATA_ID = "dataId";
+ public static final String ACTION_SPLIT_CONTACT = "splitContact";
+
public static final String ACTION_JOIN_CONTACTS = "joinContacts";
public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
public static final String EXTRA_CONTACT_ID1 = "contactId1";
@@ -125,6 +129,10 @@
public static final String ACTION_SET_RINGTONE = "setRingtone";
public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
+ public static final int CP2_ERROR = 0;
+ public static final int CONTACTS_LINKED = 1;
+ public static final int CONTACTS_SPLIT = 2;
+
private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
Data.MIMETYPE,
Data.IS_PRIMARY,
@@ -264,6 +272,8 @@
deleteMultipleContacts(intent);
} else if (ACTION_DELETE_CONTACT.equals(action)) {
deleteContact(intent);
+ } else if (ACTION_SPLIT_CONTACT.equals(action)) {
+ splitContact(intent);
} else if (ACTION_JOIN_CONTACTS.equals(action)) {
joinContacts(intent);
} else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
@@ -1113,6 +1123,85 @@
}
/**
+ * Creates an intent that can be sent to this service to split a contact into it's constituent
+ * pieces.
+ */
+ public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
+ ResultReceiver receiver) {
+ final Intent serviceIntent = new Intent(context, ContactSaveService.class);
+ serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
+ serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
+ serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
+ return serviceIntent;
+ }
+
+ private void splitContact(Intent intent) {
+ final long rawContactIds[][] = (long[][]) intent
+ .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
+ if (rawContactIds == null) {
+ Log.e(TAG, "Invalid argument for splitContact request");
+ return;
+ }
+ final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
+ final ContentResolver resolver = getContentResolver();
+ final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
+ final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
+ for (int i = 0; i < rawContactIds.length; i++) {
+ for (int j = 0; j < rawContactIds.length; j++) {
+ if (i != j) {
+ if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
+ if (receiver != null) {
+ receiver.send(CP2_ERROR, new Bundle());
+ return;
+ }
+ }
+ }
+ }
+ }
+ if (operations.size() > 0 && !applyOperations(resolver, operations)) {
+ if (receiver != null) {
+ receiver.send(CP2_ERROR, new Bundle());
+ }
+ return;
+ }
+ if (receiver != null) {
+ receiver.send(CONTACTS_SPLIT, new Bundle());
+ } else {
+ showToast(R.string.contactUnlinkedToast);
+ }
+ }
+
+ /**
+ * Adds insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
+ * and {@param rawContactIds2} to {@param operations}.
+ * @return false if an error occurred, true otherwise.
+ */
+ private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
+ long[] rawContactIds1, long[] rawContactIds2) {
+ if (rawContactIds1 == null || rawContactIds2 == null) {
+ Log.e(TAG, "Invalid arguments for splitContact request");
+ return false;
+ }
+ // For each pair of raw contacts, insert an aggregation exception
+ final ContentResolver resolver = getContentResolver();
+ // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
+ final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
+ for (int i = 0; i < rawContactIds1.length; i++) {
+ for (int j = 0; j < rawContactIds2.length; j++) {
+ buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
+ // Before we get to 500 we need to flush the operations list
+ if (operations.size() > 0 && operations.size() % batchSize == 0) {
+ if (!applyOperations(resolver, operations)) {
+ return false;
+ }
+ operations.clear();
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
* Creates an intent that can be sent to this service to join two contacts.
* The resulting contact uses the name from {@param contactId1} if possible.
*/
@@ -1135,13 +1224,22 @@
* Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
* No special attention is paid to where the resulting contact's name is taken from.
*/
- public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
- Intent serviceIntent = new Intent(context, ContactSaveService.class);
+ public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
+ ResultReceiver receiver) {
+ final Intent serviceIntent = new Intent(context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
+ serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
return serviceIntent;
}
+ /**
+ * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
+ * No special attention is paid to where the resulting contact's name is taken from.
+ */
+ public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
+ return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
+ }
private interface JoinContactQuery {
String[] PROJECTION = {
@@ -1173,9 +1271,11 @@
private void joinSeveralContacts(Intent intent) {
final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
+ final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
// Load raw contact IDs for all contacts involved.
- long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
+ final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
+ final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
if (rawContactIds == null) {
Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
return;
@@ -1193,21 +1293,33 @@
}
// Before we get to 500 we need to flush the operations list
if (operations.size() > 0 && operations.size() % batchSize == 0) {
- if (!applyJoinOperations(resolver, operations)) {
+ if (!applyOperations(resolver, operations)) {
+ if (receiver != null) {
+ receiver.send(CP2_ERROR, new Bundle());
+ }
return;
}
operations.clear();
}
}
}
- if (operations.size() > 0 && !applyJoinOperations(resolver, operations)) {
+ if (operations.size() > 0 && !applyOperations(resolver, operations)) {
+ if (receiver != null) {
+ receiver.send(CP2_ERROR, new Bundle());
+ }
return;
}
- showToast(R.string.contactsJoinedMessage);
+ if (receiver != null) {
+ final Bundle result = new Bundle();
+ result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
+ receiver.send(CONTACTS_LINKED, result);
+ } else {
+ showToast(R.string.contactsJoinedMessage);
+ }
}
/** Returns true if the batch was successfully applied and false otherwise. */
- private boolean applyJoinOperations(ContentResolver resolver,
+ private boolean applyOperations(ContentResolver resolver,
ArrayList<ContentProviderOperation> operations) {
try {
resolver.applyBatch(ContactsContract.AUTHORITY, operations);
@@ -1219,7 +1331,6 @@
}
}
-
private void joinContacts(Intent intent) {
long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
@@ -1296,13 +1407,61 @@
deliverCallback(callbackIntent);
}
+ /**
+ * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
+ * array of the return value holds an array of raw contact ids for one contactId.
+ * @param contactIds
+ * @return
+ */
+ private long[][] getSeparatedRawContactIds(long[] contactIds) {
+ final long[][] rawContactIds = new long[contactIds.length][];
+ for (int i = 0; i < contactIds.length; i++) {
+ rawContactIds[i] = getRawContactIds(contactIds[i]);
+ }
+ return rawContactIds;
+ }
+
+ /**
+ * Gets the raw contact ids associated with {@param contactId}.
+ * @param contactId
+ * @return Array of raw contact ids.
+ */
+ private long[] getRawContactIds(long contactId) {
+ final ContentResolver resolver = getContentResolver();
+ long rawContactIds[];
+
+ final StringBuilder queryBuilder = new StringBuilder();
+ queryBuilder.append(RawContacts.CONTACT_ID)
+ .append("=")
+ .append(String.valueOf(contactId));
+
+ final Cursor c = resolver.query(RawContacts.CONTENT_URI,
+ JoinContactQuery.PROJECTION,
+ queryBuilder.toString(),
+ null, null);
+ if (c == null) {
+ Log.e(TAG, "Unable to open Contacts DB cursor");
+ return null;
+ }
+ try {
+ rawContactIds = new long[c.getCount()];
+ for (int i = 0; i < rawContactIds.length; i++) {
+ c.moveToPosition(i);
+ final long rawContactId = c.getLong(JoinContactQuery._ID);
+ rawContactIds[i] = rawContactId;
+ }
+ } finally {
+ c.close();
+ }
+ return rawContactIds;
+ }
+
private long[] getRawContactIdsForAggregation(long[] contactIds) {
if (contactIds == null) {
return null;
}
final ContentResolver resolver = getContentResolver();
- long rawContactIds[];
final StringBuilder queryBuilder = new StringBuilder();
final String stringContactIds[] = new String[contactIds.length];
@@ -1327,6 +1486,7 @@
showToast(R.string.contactSavedErrorToast);
return null;
}
+ long rawContactIds[];
try {
if (c.getCount() < 2) {
Log.e(TAG, "Not enough raw contacts to aggregate together.");
@@ -1362,6 +1522,19 @@
}
/**
+ * Construct a {@link AggregationExceptions#TYPE_KEEP_SEPARATE} ContentProviderOperation.
+ */
+ private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
+ long rawContactId1, long rawContactId2) {
+ final Builder builder =
+ ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
+ builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_SEPARATE);
+ builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
+ builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
+ operations.add(builder.build());
+ }
+
+ /**
* Shows a toast on the UI thread.
*/
private void showToast(final int message) {