blob: a926c7809ab3639e85f624b12fc6e3d0c29f4862 [file] [log] [blame]
/*
* Copyright (C) 2010 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.contacts;
import com.android.contacts.R;
import com.google.android.collect.Lists;
import com.google.android.collect.Sets;
import android.accounts.Account;
import android.app.IntentService;
import android.content.ContentProviderOperation;
import android.content.ContentProviderOperation.Builder;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
import android.util.Log;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/**
* A service responsible for saving changes to the content provider.
*/
public class ContactSaveService extends IntentService {
private static final String TAG = "ContactSaveService";
public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
public static final String EXTRA_ACCOUNT_NAME = "accountName";
public static final String EXTRA_ACCOUNT_TYPE = "accountType";
public static final String EXTRA_CONTENT_VALUES = "contentValues";
public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
public static final String EXTRA_OPERATIONS = "Operations";
public static final String ACTION_CREATE_GROUP = "createGroup";
public static final String ACTION_RENAME_GROUP = "renameGroup";
public static final String ACTION_DELETE_GROUP = "deleteGroup";
public static final String EXTRA_GROUP_ID = "groupId";
public static final String EXTRA_GROUP_LABEL = "groupLabel";
public static final String ACTION_SET_STARRED = "setStarred";
public static final String ACTION_DELETE_CONTACT = "delete";
public static final String EXTRA_CONTACT_URI = "contactUri";
public static final String EXTRA_STARRED_FLAG = "starred";
public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
public static final String EXTRA_DATA_ID = "dataId";
public static final String ACTION_JOIN_CONTACTS = "joinContacts";
public static final String EXTRA_CONTACT_ID1 = "contactId1";
public static final String EXTRA_CONTACT_ID2 = "contactId2";
public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
Data.MIMETYPE,
Data.IS_PRIMARY,
Data.DATA1,
Data.DATA2,
Data.DATA3,
Data.DATA4,
Data.DATA5,
Data.DATA6,
Data.DATA7,
Data.DATA8,
Data.DATA9,
Data.DATA10,
Data.DATA11,
Data.DATA12,
Data.DATA13,
Data.DATA14,
Data.DATA15
);
public ContactSaveService() {
super(TAG);
setIntentRedelivery(true);
}
@Override
protected void onHandleIntent(Intent intent) {
String action = intent.getAction();
if (ACTION_NEW_RAW_CONTACT.equals(action)) {
createRawContact(intent);
} else if (ACTION_CREATE_GROUP.equals(action)) {
createGroup(intent);
} else if (ACTION_RENAME_GROUP.equals(action)) {
renameGroup(intent);
} else if (ACTION_DELETE_GROUP.equals(action)) {
deleteGroup(intent);
} else if (ACTION_SET_STARRED.equals(action)) {
setStarred(intent);
} else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
setSuperPrimary(intent);
} else if (ACTION_CLEAR_PRIMARY.equals(action)) {
clearPrimary(intent);
} else if (ACTION_DELETE_CONTACT.equals(action)) {
deleteContact(intent);
} else if (ACTION_JOIN_CONTACTS.equals(action)) {
joinContacts(intent);
}
}
/**
* Creates an intent that can be sent to this service to create a new raw contact
* using data presented as a set of ContentValues.
*/
public static Intent createNewRawContactIntent(Context context,
ArrayList<ContentValues> values, Account account, Class<?> callbackActivity,
String callbackAction) {
Intent serviceIntent = new Intent(
context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
if (account != null) {
serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
}
serviceIntent.putParcelableArrayListExtra(
ContactSaveService.EXTRA_CONTENT_VALUES, values);
// Callback intent will be invoked by the service once the new contact is
// created. The service will put the URI of the new contact as "data" on
// the callback intent.
Intent callbackIntent = new Intent(context, callbackActivity);
callbackIntent.setAction(callbackAction);
callbackIntent.setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
return serviceIntent;
}
private void createRawContact(Intent intent) {
String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
.withValue(RawContacts.ACCOUNT_NAME, accountName)
.withValue(RawContacts.ACCOUNT_TYPE, accountType)
.build());
int size = valueList.size();
for (int i = 0; i < size; i++) {
ContentValues values = valueList.get(i);
values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
.withValues(values)
.build());
}
ContentResolver resolver = getContentResolver();
ContentProviderResult[] results;
try {
results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
} catch (Exception e) {
throw new RuntimeException("Failed to store new contact", e);
}
Uri rawContactUri = results[0].uri;
callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
startActivity(callbackIntent);
}
/**
* Creates an intent that can be sent to this service to create a new group.
*/
public static Intent createNewGroupIntent(Context context, Account account, String label,
Class<?> callbackActivity, String callbackAction) {
Intent serviceIntent = new Intent(context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
// Callback intent will be invoked by the service once the new group is
// created. The service will put a group membership row in the extras
// of the callback intent.
Intent callbackIntent = new Intent(context, callbackActivity);
callbackIntent.setAction(callbackAction);
callbackIntent.setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
return serviceIntent;
}
private void createGroup(Intent intent) {
String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
ContentValues values = new ContentValues();
values.put(Groups.ACCOUNT_TYPE, accountType);
values.put(Groups.ACCOUNT_NAME, accountName);
values.put(Groups.TITLE, label);
Uri groupUri = getContentResolver().insert(Groups.CONTENT_URI, values);
if (groupUri == null) {
return;
}
values.clear();
values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
startActivity(callbackIntent);
}
/**
* Creates an intent that can be sent to this service to rename a group.
*/
public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel) {
Intent serviceIntent = new Intent(context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
return serviceIntent;
}
private void renameGroup(Intent intent) {
long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
if (groupId == -1) {
Log.e(TAG, "Invalid arguments for renameGroup request");
return;
}
ContentValues values = new ContentValues();
values.put(Groups.TITLE, label);
getContentResolver().update(
ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), values, null, null);
}
/**
* Creates an intent that can be sent to this service to delete a group.
*/
public static Intent createGroupDeletionIntent(Context context, long groupId) {
Intent serviceIntent = new Intent(context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
return serviceIntent;
}
private void deleteGroup(Intent intent) {
long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
if (groupId == -1) {
Log.e(TAG, "Invalid arguments for deleteGroup request");
return;
}
getContentResolver().delete(
ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
}
/**
* Creates an intent that can be sent to this service to star or un-star a contact.
*/
public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
Intent serviceIntent = new Intent(context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
return serviceIntent;
}
private void setStarred(Intent intent) {
Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
if (contactUri == null) {
Log.e(TAG, "Invalid arguments for setStarred request");
return;
}
final ContentValues values = new ContentValues(1);
values.put(Contacts.STARRED, value);
getContentResolver().update(contactUri, values, null, null);
}
/**
* Creates an intent that sets the selected data item as super primary (default)
*/
public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
Intent serviceIntent = new Intent(context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
return serviceIntent;
}
private void setSuperPrimary(Intent intent) {
long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
if (dataId == -1) {
Log.e(TAG, "Invalid arguments for setSuperPrimary request");
return;
}
// Update the primary values in the data record.
ContentValues values = new ContentValues(1);
values.put(Data.IS_SUPER_PRIMARY, 1);
values.put(Data.IS_PRIMARY, 1);
getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
values, null, null);
}
/**
* Creates an intent that clears the primary flag of all data items that belong to the same
* raw_contact as the given data item. Will only clear, if the data item was primary before
* this call
*/
public static Intent createClearPrimaryIntent(Context context, long dataId) {
Intent serviceIntent = new Intent(context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
return serviceIntent;
}
private void clearPrimary(Intent intent) {
long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
if (dataId == -1) {
Log.e(TAG, "Invalid arguments for clearPrimary request");
return;
}
// Update the primary values in the data record.
ContentValues values = new ContentValues(1);
values.put(Data.IS_SUPER_PRIMARY, 0);
values.put(Data.IS_PRIMARY, 0);
getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
values, null, null);
}
/**
* Creates an intent that can be sent to this service to delete a contact.
*/
public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
Intent serviceIntent = new Intent(context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
return serviceIntent;
}
private void deleteContact(Intent intent) {
Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
if (contactUri == null) {
Log.e(TAG, "Invalid arguments for deleteContact request");
return;
}
getContentResolver().delete(contactUri, null, null);
}
/**
* Creates an intent that can be sent to this service to join two contacts.
*/
public static Intent createJoinContactsIntent(Context context, long contactId1,
long contactId2, boolean contactWritable,
Class<?> callbackActivity, String callbackAction) {
Intent serviceIntent = new Intent(context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
// Callback intent will be invoked by the service once the contacts are joined.
Intent callbackIntent = new Intent(context, callbackActivity);
callbackIntent.setAction(callbackAction);
callbackIntent.setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
return serviceIntent;
}
private interface JoinContactQuery {
String[] PROJECTION = {
RawContacts._ID,
RawContacts.CONTACT_ID,
RawContacts.NAME_VERIFIED,
RawContacts.DISPLAY_NAME_SOURCE,
};
String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
int _ID = 0;
int CONTACT_ID = 1;
int NAME_VERIFIED = 2;
int DISPLAY_NAME_SOURCE = 3;
}
private void joinContacts(Intent intent) {
long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
if (contactId1 == -1 || contactId2 == -1) {
Log.e(TAG, "Invalid arguments for joinContacts request");
return;
}
final ContentResolver resolver = getContentResolver();
// Load raw contact IDs for all raw contacts involved - currently edited and selected
// in the join UIs
Cursor c = resolver.query(RawContacts.CONTENT_URI,
JoinContactQuery.PROJECTION,
JoinContactQuery.SELECTION,
new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
long rawContactIds[];
long verifiedNameRawContactId = -1;
try {
int maxDisplayNameSource = -1;
rawContactIds = new long[c.getCount()];
for (int i = 0; i < rawContactIds.length; i++) {
c.moveToPosition(i);
long rawContactId = c.getLong(JoinContactQuery._ID);
rawContactIds[i] = rawContactId;
int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
if (nameSource > maxDisplayNameSource) {
maxDisplayNameSource = nameSource;
}
}
// Find an appropriate display name for the joined contact:
// if should have a higher DisplayNameSource or be the name
// of the original contact that we are joining with another.
if (writable) {
for (int i = 0; i < rawContactIds.length; i++) {
c.moveToPosition(i);
if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
if (nameSource == maxDisplayNameSource
&& (verifiedNameRawContactId == -1
|| c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
}
}
}
}
} finally {
c.close();
}
// For each pair of raw contacts, insert an aggregation exception
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
for (int i = 0; i < rawContactIds.length; i++) {
for (int j = 0; j < rawContactIds.length; j++) {
if (i != j) {
buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
}
}
}
// Mark the original contact as "name verified" to make sure that the contact
// display name does not change as a result of the join
if (verifiedNameRawContactId != -1) {
Builder builder = ContentProviderOperation.newUpdate(
ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
builder.withValue(RawContacts.NAME_VERIFIED, 1);
operations.add(builder.build());
}
boolean success = false;
// Apply all aggregation exceptions as one batch
try {
resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Toast.makeText(this, R.string.contactsJoinedMessage, Toast.LENGTH_LONG).show();
success = true;
} catch (RemoteException e) {
Log.e(TAG, "Failed to apply aggregation exception batch", e);
Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
} catch (OperationApplicationException e) {
Log.e(TAG, "Failed to apply aggregation exception batch", e);
Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
}
Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
if (success) {
Uri uri = RawContacts.getContactLookupUri(resolver,
ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
callbackIntent.setData(uri);
}
startActivity(callbackIntent);
}
/**
* Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
*/
private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
long rawContactId1, long rawContactId2) {
Builder builder =
ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
operations.add(builder.build());
}
}