Adding "make personal copy" feature

The is the first in a series of CLs.
For now we create the copy, but we don't
actually select it in the UI.

Change-Id: Ie2719bf4e91915992f0e785b7a9827b3c934a6a2
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 105b502..982e8db 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1304,4 +1304,7 @@
 
     <!-- The description of the directory where the contact was found [CHAR LIMIT=100]-->
     <string name="contact_directory_account_description">from <xliff:g id="type" example="Corporate Directory">%1$s</xliff:g> (<xliff:g id="name" example="me@acme.com">%2$s</xliff:g>)</string>
+
+    <!-- Toast shown when creating a personal copy of a contact [CHAR LIMIT=100] -->
+    <string name="toast_making_personal_copy">Creating a personal copy</string>
 </resources>
diff --git a/src/com/android/contacts/activities/ContactBrowserActivity.java b/src/com/android/contacts/activities/ContactBrowserActivity.java
index db353e8..6a68547 100644
--- a/src/com/android/contacts/activities/ContactBrowserActivity.java
+++ b/src/com/android/contacts/activities/ContactBrowserActivity.java
@@ -30,11 +30,13 @@
 import com.android.contacts.list.StrequentContactListFragment;
 import com.android.contacts.ui.ContactsPreferencesActivity;
 import com.android.contacts.util.DialogManager;
+import com.android.contacts.views.ContactSaveService;
 import com.android.contacts.views.detail.ContactDetailFragment;
 import com.android.contacts.views.detail.ContactNoneFragment;
 import com.android.contacts.views.editor.ContactEditorFragment;
 import com.android.contacts.widget.ContextMenuAdapter;
 
+import android.accounts.Account;
 import android.app.ActionBar;
 import android.app.Activity;
 import android.app.Dialog;
@@ -57,6 +59,8 @@
 import android.view.Window;
 import android.widget.Toast;
 
+import java.util.ArrayList;
+
 /**
  * Displays a list to browse contacts. For xlarge screens, this also displays a detail-pane on
  * the right
@@ -169,6 +173,26 @@
         }
     }
 
+    @Override
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+        if (Intent.ACTION_VIEW.equals(intent.getAction())) {
+            Uri uri = intent.getData();
+            if (uri == null) {
+                return;
+            }
+
+            if (mHasActionBar) {
+                if (mActionBarAdapter.getMode() != ContactBrowserMode.MODE_CONTACTS) {
+                    mActionBarAdapter.clearSavedState(ContactBrowserMode.MODE_CONTACTS);
+                    mActionBarAdapter.setMode(ContactBrowserMode.MODE_CONTACTS);
+                }
+            }
+            mListFragment.setSelectedContactUri(uri);
+            setupContactDetailFragment(uri);
+        }
+    }
+
     private void configureListFragment() {
         int mode = -1;
         if (mHasActionBar) {
@@ -503,6 +527,15 @@
         public void onDeleteRequested(Uri contactLookupUri) {
             getContactDeletionInteraction().deleteContact(contactLookupUri);
         }
+
+        @Override
+        public void onCreateRawContactRequested(ArrayList<ContentValues> values, Account account) {
+            Toast.makeText(ContactBrowserActivity.this, R.string.toast_making_personal_copy,
+                    Toast.LENGTH_LONG).show();
+            Intent serviceIntent = ContactSaveService.createNewRawContactIntent(
+                    ContactBrowserActivity.this, values, account);
+            startService(serviceIntent);
+        }
     }
 
     private class EditorFragmentListener implements ContactEditorFragment.Listener {
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index b07ee36..b26cbd0 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -19,17 +19,23 @@
 import com.android.contacts.ContactsSearchManager;
 import com.android.contacts.R;
 import com.android.contacts.interactions.ContactDeletionInteraction;
+import com.android.contacts.views.ContactSaveService;
 import com.android.contacts.views.detail.ContactDetailFragment;
 
+import android.accounts.Account;
 import android.app.Activity;
 import android.app.Dialog;
 import android.content.ActivityNotFoundException;
+import android.content.ContentValues;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.MenuItem;
+import android.widget.Toast;
+
+import java.util.ArrayList;
 
 public class ContactDetailActivity extends Activity {
     private static final String TAG = "ContactDetailActivity";
@@ -127,5 +133,16 @@
         public void onDeleteRequested(Uri lookupUri) {
             getContactDeletionInteraction().deleteContact(lookupUri);
         }
+
+        @Override
+        public void onCreateRawContactRequested(
+                ArrayList<ContentValues> values, Account account) {
+            Toast.makeText(ContactDetailActivity.this, R.string.toast_making_personal_copy,
+                    Toast.LENGTH_LONG).show();
+            Intent serviceIntent = ContactSaveService.createNewRawContactIntent(
+                    ContactDetailActivity.this, values, account);
+            startService(serviceIntent);
+
+        }
     };
 }
diff --git a/src/com/android/contacts/list/ContactBrowseListFragment.java b/src/com/android/contacts/list/ContactBrowseListFragment.java
index c09dc47..fe82129 100644
--- a/src/com/android/contacts/list/ContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/ContactBrowseListFragment.java
@@ -138,12 +138,14 @@
 
             parseSelectedContactUri();
 
-            // Configure the adapter to show the selection based on the lookup key extracted
-            // from the URI
-            configureAdapter();
+            if (isAdded()) {
+                // Configure the adapter to show the selection based on the lookup key extracted
+                // from the URI
+                configureAdapter();
 
-            // Also, launch a loader to pick up a new lookup key in case it has changed
-            startLoadingContactLookupKey();
+                // Also, launch a loader to pick up a new lookup key in case it has changed
+                startLoadingContactLookupKey();
+            }
         }
     }
 
diff --git a/src/com/android/contacts/views/ContactLoader.java b/src/com/android/contacts/views/ContactLoader.java
index 7a221a1..19a28b5 100644
--- a/src/com/android/contacts/views/ContactLoader.java
+++ b/src/com/android/contacts/views/ContactLoader.java
@@ -24,6 +24,7 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Entity;
+import android.content.Entity.NamedContentValues;
 import android.content.Loader;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -44,6 +45,7 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 
 /**
  * Loads a single Contact and all it constituent RawContacts.
@@ -97,6 +99,7 @@
 
         private String mDirectoryDisplayName;
         private String mDirectoryType;
+        private String mDirectoryAccountType;
         private String mDirectoryAccountName;
         private int mDirectoryExportSupport;
 
@@ -156,9 +159,10 @@
          * @param exportSupport See {@link Directory#EXPORT_SUPPORT}.
          */
         public void setDirectoryMetaData(String displayName, String directoryType,
-                String accountName, int exportSupport) {
+                String accountType, String accountName, int exportSupport) {
             mDirectoryDisplayName = displayName;
             mDirectoryType = directoryType;
+            mDirectoryAccountType = accountType;
             mDirectoryAccountName = accountName;
             mDirectoryExportSupport = exportSupport;
         }
@@ -236,10 +240,34 @@
             return mDirectoryType;
         }
 
+        public String getDirectoryAccountType() {
+            return mDirectoryAccountType;
+        }
+
         public String getDirectoryAccountName() {
             return mDirectoryAccountName;
         }
 
+        public ArrayList<ContentValues> getContentValues() {
+            if (mEntities.size() != 1) {
+                throw new IllegalStateException(
+                        "Cannot extract content values from an aggregated contact");
+            }
+
+            Entity entity = mEntities.get(0);
+            ArrayList<ContentValues> result = new ArrayList<ContentValues>();
+            ArrayList<NamedContentValues> subValues = entity.getSubValues();
+            if (subValues != null) {
+                int size = subValues.size();
+                for (int i = 0; i < size; i++) {
+                    NamedContentValues pair = subValues.get(i);
+                    if (Data.CONTENT_URI.equals(pair.uri)) {
+                        result.add(pair.values);
+                    }
+                }
+            }
+            return result;
+        }
     }
 
     private static class ContactQuery {
@@ -377,6 +405,7 @@
             Directory.DISPLAY_NAME,
             Directory.PACKAGE_NAME,
             Directory.TYPE_RESOURCE_ID,
+            Directory.ACCOUNT_TYPE,
             Directory.ACCOUNT_NAME,
             Directory.EXPORT_SUPPORT,
         };
@@ -384,8 +413,9 @@
         public final static int DISPLAY_NAME = 0;
         public final static int PACKAGE_NAME = 1;
         public final static int TYPE_RESOURCE_ID = 2;
-        public final static int ACCOUNT_NAME = 3;
-        public final static int EXPORT_SUPPORT = 4;
+        public final static int ACCOUNT_TYPE = 3;
+        public final static int ACCOUNT_NAME = 4;
+        public final static int EXPORT_SUPPORT = 5;
     }
 
     private final class LoadContactTask extends AsyncTask<Void, Void, Result> {
@@ -628,6 +658,7 @@
                     final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
                     final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
                     final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
+                    final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
                     final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
                     final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
                     String directoryType = null;
@@ -643,7 +674,7 @@
                     }
 
                     result.setDirectoryMetaData(
-                            displayName, directoryType, accountName, exportSupport);
+                            displayName, directoryType, accountType, accountName, exportSupport);
                 }
             } finally {
                 cursor.close();
diff --git a/src/com/android/contacts/views/ContactSaveService.java b/src/com/android/contacts/views/ContactSaveService.java
index 936b8a4..91d2413 100644
--- a/src/com/android/contacts/views/ContactSaveService.java
+++ b/src/com/android/contacts/views/ContactSaveService.java
@@ -16,22 +16,62 @@
 
 package com.android.contacts.views;
 
+import com.android.contacts.activities.ContactBrowserActivity;
+import com.google.android.collect.Sets;
+
+import android.accounts.Account;
+import android.app.Activity;
 import android.app.IntentService;
 import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentValues;
 import android.content.Intent;
 import android.content.OperationApplicationException;
+import android.net.Uri;
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.provider.ContactsContract;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
 import android.util.Log;
 
 import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
 
 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";
 
+    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);
@@ -39,6 +79,52 @@
 
     @Override
     protected void onHandleIntent(Intent intent) {
+        String action = intent.getAction();
+        if (ACTION_NEW_RAW_CONTACT.equals(action)) {
+            createRawContact(intent);
+        } else {
+            performContentProviderOperations(intent);
+        }
+    }
+
+    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 result = ContactsContract.Directory.CONTENT_URI;
+        Uri rawContactUri = results[0].uri;
+        callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
+
+        startActivity(callbackIntent);
+    }
+
+    private void performContentProviderOperations(Intent intent) {
         final Parcelable[] operationsArray = intent.getParcelableArrayExtra(EXTRA_OPERATIONS);
 
         // We have to cast each item individually here
@@ -56,4 +142,31 @@
             Log.e(TAG, "Error saving", e);
         }
     }
+
+    /**
+     * 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(
+            Activity activity, ArrayList<ContentValues> values, Account account) {
+        Intent serviceIntent = new Intent(
+                activity, 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(activity, activity.getClass());
+        callbackIntent.setAction(Intent.ACTION_VIEW);
+        callbackIntent.setFlags(
+                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
+        return serviceIntent;
+    }
 }
diff --git a/src/com/android/contacts/views/detail/ContactDetailFragment.java b/src/com/android/contacts/views/detail/ContactDetailFragment.java
index 436e9b0..c734f84 100644
--- a/src/com/android/contacts/views/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/views/detail/ContactDetailFragment.java
@@ -17,22 +17,24 @@
 package com.android.contacts.views.detail;
 
 import com.android.contacts.Collapser;
+import com.android.contacts.Collapser.Collapsible;
 import com.android.contacts.ContactOptionsActivity;
 import com.android.contacts.ContactPresenceIconUtil;
 import com.android.contacts.ContactsUtils;
 import com.android.contacts.ContactsUtils.ImActions;
 import com.android.contacts.R;
 import com.android.contacts.TypePrecedence;
-import com.android.contacts.Collapser.Collapsible;
 import com.android.contacts.model.ContactsSource;
-import com.android.contacts.model.Sources;
 import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.Sources;
 import com.android.contacts.util.Constants;
 import com.android.contacts.util.DataStatus;
 import com.android.contacts.util.PhoneCapabilityTester;
 import com.android.contacts.views.ContactLoader;
+import com.android.contacts.views.editor.SelectAccountDialogFragment;
 import com.android.internal.telephony.ITelephony;
 
+import android.accounts.Account;
 import android.app.Activity;
 import android.app.Fragment;
 import android.app.LoaderManager;
@@ -42,9 +44,9 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Entity;
+import android.content.Entity.NamedContentValues;
 import android.content.Intent;
 import android.content.Loader;
-import android.content.Entity.NamedContentValues;
 import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
 import android.net.ParseException;
@@ -54,12 +56,6 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.provider.ContactsContract.CommonDataKinds;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.Directory;
-import android.provider.ContactsContract.DisplayNameSources;
-import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.StatusUpdates;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Im;
 import android.provider.ContactsContract.CommonDataKinds.Nickname;
@@ -70,32 +66,38 @@
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StatusUpdates;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
-import android.view.ViewGroup;
-import android.view.ContextMenu.ContextMenuInfo;
 import android.view.View.OnClickListener;
 import android.view.View.OnCreateContextMenuListener;
+import android.view.ViewGroup;
 import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
 import android.widget.BaseAdapter;
 import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.TextView;
 import android.widget.Toast;
-import android.widget.AdapterView.OnItemClickListener;
 
 import java.util.ArrayList;
 
-public class ContactDetailFragment extends Fragment
-        implements OnCreateContextMenuListener, OnItemClickListener {
+public class ContactDetailFragment extends Fragment implements OnCreateContextMenuListener,
+        OnItemClickListener, SelectAccountDialogFragment.Listener {
     private static final String TAG = "ContactDetailFragment";
 
     private static final int MENU_ITEM_MAKE_DEFAULT = 3;
@@ -883,13 +885,62 @@
                 return true;
             }
             case R.id.menu_copy: {
-                Toast.makeText(mContext, "Not implemented yet", Toast.LENGTH_SHORT).show();
+                makePersonalCopy();
                 return true;
             }
         }
         return false;
     }
 
+    private void makePersonalCopy() {
+        if (mListener == null) {
+            return;
+        }
+
+        int exportSupport = mContactData.getDirectoryExportSupport();
+        switch (exportSupport) {
+            case Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY: {
+                createCopy(new Account(mContactData.getDirectoryAccountName(),
+                                mContactData.getDirectoryAccountType()));
+                break;
+            }
+            case Directory.EXPORT_SUPPORT_ANY_ACCOUNT: {
+                final ArrayList<Account> accounts = Sources.getInstance(mContext).getAccounts(true);
+                if (accounts.isEmpty()) {
+                    createCopy(null);
+                    return;  // Don't show a dialog.
+                }
+
+                // In the common case of a single writable account, auto-select
+                // it without showing a dialog.
+                if (accounts.size() == 1) {
+                    createCopy(accounts.get(0));
+                    return;  // Don't show a dialog.
+                }
+
+                final SelectAccountDialogFragment dialog =
+                        new SelectAccountDialogFragment(getId(), true);
+                dialog.show(getFragmentManager(), SelectAccountDialogFragment.TAG);
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void onAccountSelectorCancelled() {
+    }
+
+    @Override
+    public void onAccountChosen(Account account, boolean isNewContact) {
+        createCopy(account);
+    }
+
+    private void createCopy(Account account) {
+        if (mListener != null) {
+            mListener.onCreateRawContactRequested(mContactData.getContentValues(), account);
+        }
+    }
+
     @Override
     public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
         AdapterView.AdapterContextMenuInfo info;
@@ -1077,5 +1128,13 @@
          * User decided to delete the contact
          */
         public void onDeleteRequested(Uri lookupUri);
+
+        /**
+         * User requested creation of a new contact with the specified values.
+         *
+         * @param values ContentValues containing data rows for the new contact.
+         * @param account Account where the new contact should be created
+         */
+        public void onCreateRawContactRequested(ArrayList<ContentValues> values, Account account);
     }
 }
diff --git a/src/com/android/contacts/views/editor/SelectAccountDialogFragment.java b/src/com/android/contacts/views/editor/SelectAccountDialogFragment.java
index 9ea1955..3740fe3 100644
--- a/src/com/android/contacts/views/editor/SelectAccountDialogFragment.java
+++ b/src/com/android/contacts/views/editor/SelectAccountDialogFragment.java
@@ -43,7 +43,7 @@
  * Does not perform any action by itself.
  */
 public class SelectAccountDialogFragment extends TargetedDialogFragment {
-    public static final String TAG = "PickPhotoDialogFragment";
+    public static final String TAG = "SelectAccountDialogFragment";
     private static final String IS_NEW_CONTACT = "IS_NEW_CONTACT";
 
     private boolean mIsNewContact;
@@ -70,6 +70,7 @@
         outState.putBoolean(IS_NEW_CONTACT, mIsNewContact);
     }
 
+    @Override
     public Dialog onCreateDialog(Bundle savedInstanceState) {
         // Wrap our context to inflate list items using correct theme
         final Context dialogContext = new ContextThemeWrapper(getActivity(),