Renaming a couple of packages

Change-Id: I2c0f86b51baa622df629206f8b79ef1d0df09119
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
new file mode 100644
index 0000000..a926c78
--- /dev/null
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -0,0 +1,539 @@
+/*
+ * 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());
+    }
+}