Batch deletion

Also: when re-opening the Activity from the launcher, clear the
mIsInSelectionMode variable in memory.

Bug: 19549465
Change-Id: If589983d3d84c9c18066da08f9879c22db1a75ed
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index c43941f..1668521 100644
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -99,7 +99,9 @@
 
     public static final String ACTION_SET_STARRED = "setStarred";
     public static final String ACTION_DELETE_CONTACT = "delete";
+    public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
     public static final String EXTRA_CONTACT_URI = "contactUri";
+    public static final String EXTRA_CONTACT_IDS = "contactIds";
     public static final String EXTRA_STARRED_FLAG = "starred";
 
     public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
@@ -203,6 +205,8 @@
             setSuperPrimary(intent);
         } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
             clearPrimary(intent);
+        } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
+            deleteMultipleContacts(intent);
         } else if (ACTION_DELETE_CONTACT.equals(action)) {
             deleteContact(intent);
         } else if (ACTION_JOIN_CONTACTS.equals(action)) {
@@ -945,6 +949,17 @@
         return serviceIntent;
     }
 
+    /**
+     * Creates an intent that can be sent to this service to delete multiple contacts.
+     */
+    public static Intent createDeleteMultipleContactsIntent(Context context,
+            long[] contactIds) {
+        Intent serviceIntent = new Intent(context, ContactSaveService.class);
+        serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
+        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
+        return serviceIntent;
+    }
+
     private void deleteContact(Intent intent) {
         Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
         if (contactUri == null) {
@@ -955,6 +970,19 @@
         getContentResolver().delete(contactUri, null, null);
     }
 
+    private void deleteMultipleContacts(Intent intent) {
+        final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
+        if (contactIds == null) {
+            Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
+            return;
+        }
+        for (long contactId : contactIds) {
+            final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+            getContentResolver().delete(contactUri, null, null);
+        }
+
+    }
+
     /**
      * Creates an intent that can be sent to this service to join two contacts.
      */
diff --git a/src/com/android/contacts/activities/ActionBarAdapter.java b/src/com/android/contacts/activities/ActionBarAdapter.java
index f6cb921..dc9fcf2 100644
--- a/src/com/android/contacts/activities/ActionBarAdapter.java
+++ b/src/com/android/contacts/activities/ActionBarAdapter.java
@@ -192,6 +192,7 @@
             mSearchMode = request.isSearchMode();
             mQueryString = request.getQueryString();
             mCurrentTab = loadLastTabPreference();
+            mSelectionMode = false;
         } else {
             mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
             mSelectionMode = savedState.getBoolean(EXTRA_KEY_SELECTED_MODE);
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index a3af101..577bf57 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -62,6 +62,8 @@
 import com.android.contacts.common.list.ContactListFilter;
 import com.android.contacts.common.list.ContactListFilterController;
 import com.android.contacts.common.list.ContactTileAdapter.DisplayType;
+import com.android.contacts.interactions.ContactMultiDeletionInteraction;
+import com.android.contacts.interactions.ContactMultiDeletionInteraction.MultiContactDeleteListener;
 import com.android.contacts.list.MultiSelectContactsListFragment;
 import com.android.contacts.list.MultiSelectContactsListFragment.OnCheckBoxListActionListener;
 import com.android.contacts.list.ContactTileListFragment;
@@ -97,7 +99,8 @@
         ActionBarAdapter.Listener,
         DialogManager.DialogShowingViewActivity,
         ContactListFilterController.ContactListFilterListener,
-        ProviderStatusListener {
+        ProviderStatusListener,
+        MultiContactDeleteListener {
 
     private static final String TAG = "PeopleActivity";
 
@@ -569,6 +572,9 @@
                 mTabPager.setCurrentItem(tab, !wereTabsHidden);
             }
         }
+        if (!mActionBarAdapter.isSelectionMode()) {
+            mAllFragment.displayCheckBoxes(false);
+        }
         invalidateOptionsMenu();
         showEmptyStateForTab(tab);
     }
@@ -1124,6 +1130,9 @@
             case R.id.menu_share:
                 shareSelectedContacts();
                 return true;
+            case R.id.menu_delete:
+                deleteSelectedContacts();
+                return true;
             case R.id.menu_import_export: {
                 ImportExportDialogFragment.show(getFragmentManager(), areContactsAvailable(),
                         PeopleActivity.class);
@@ -1188,6 +1197,16 @@
         ImplicitIntentsUtil.startActivityOutsideApp(this, intent);
     }
 
+    private void deleteSelectedContacts() {
+        ContactMultiDeletionInteraction.start(PeopleActivity.this,
+                mAllFragment.getSelectedContactIds());
+    }
+
+    @Override
+    public void onDeletionFinished() {
+        mAllFragment.clearCheckBoxes();
+    }
+
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         switch (requestCode) {
diff --git a/src/com/android/contacts/interactions/ContactMultiDeletionInteraction.java b/src/com/android/contacts/interactions/ContactMultiDeletionInteraction.java
new file mode 100644
index 0000000..7c13178
--- /dev/null
+++ b/src/com/android/contacts/interactions/ContactMultiDeletionInteraction.java
@@ -0,0 +1,297 @@
+/*
+ * 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.contacts.interactions;
+
+import com.google.common.collect.Sets;
+
+import com.android.contacts.ContactSaveService;
+import com.android.contacts.R;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+import java.util.HashSet;
+import java.util.TreeSet;
+
+/**
+ * An interaction invoked to delete multiple contacts.
+ *
+ * This class is very similar to {@link ContactDeletionInteraction}.
+ */
+public class ContactMultiDeletionInteraction extends Fragment
+        implements LoaderCallbacks<Cursor> {
+
+    public interface MultiContactDeleteListener {
+        void onDeletionFinished();
+    }
+
+    private static final String FRAGMENT_TAG = "deleteMultipleContacts";
+    private static final String TAG = "ContactMultiDeletionInteraction";
+    private static final String KEY_ACTIVE = "active";
+    private static final String KEY_CONTACTS_IDS = "contactIds";
+    public static final String ARG_CONTACT_IDS = "contactIds";
+
+    private static final String[] RAW_CONTACT_PROJECTION = new String[] {
+            RawContacts._ID,
+            RawContacts.ACCOUNT_TYPE,
+            RawContacts.DATA_SET,
+            RawContacts.CONTACT_ID,
+    };
+
+    private static final int COLUMN_INDEX_RAW_CONTACT_ID = 0;
+    private static final int COLUMN_INDEX_ACCOUNT_TYPE = 1;
+    private static final int COLUMN_INDEX_DATA_SET = 2;
+    private static final int COLUMN_INDEX_CONTACT_ID = 3;
+
+    private boolean mIsLoaderActive;
+    private TreeSet<Long> mContactIds;
+    private Context mContext;
+    private AlertDialog mDialog;
+
+    /**
+     * Starts the interaction.
+     *
+     * @param activity the activity within which to start the interaction
+     * @param contactIds the IDs of contacts to be deleted
+     * @return the newly created interaction
+     */
+    public static ContactMultiDeletionInteraction start(
+            Activity activity, TreeSet<Long> contactIds) {
+        if (contactIds == null) {
+            return null;
+        }
+
+        final FragmentManager fragmentManager = activity.getFragmentManager();
+        ContactMultiDeletionInteraction fragment =
+                (ContactMultiDeletionInteraction) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
+        if (fragment == null) {
+            fragment = new ContactMultiDeletionInteraction();
+            fragment.setContactIds(contactIds);
+            fragmentManager.beginTransaction().add(fragment, FRAGMENT_TAG)
+                    .commitAllowingStateLoss();
+        } else {
+            fragment.setContactIds(contactIds);
+        }
+        return fragment;
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        mContext = activity;
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        if (mDialog != null && mDialog.isShowing()) {
+            mDialog.setOnDismissListener(null);
+            mDialog.dismiss();
+            mDialog = null;
+        }
+    }
+
+    public void setContactIds(TreeSet<Long> contactIds) {
+        mContactIds = contactIds;
+        mIsLoaderActive = true;
+        if (isStarted()) {
+            Bundle args = new Bundle();
+            args.putSerializable(ARG_CONTACT_IDS, mContactIds);
+            getLoaderManager().restartLoader(R.id.dialog_delete_multiple_contact_loader_id,
+                    args, this);
+        }
+    }
+
+    private boolean isStarted() {
+        return isAdded();
+    }
+
+    @Override
+    public void onStart() {
+        if (mIsLoaderActive) {
+            Bundle args = new Bundle();
+            args.putSerializable(ARG_CONTACT_IDS, mContactIds);
+            getLoaderManager().initLoader(
+                    R.id.dialog_delete_multiple_contact_loader_id, args, this);
+        }
+        super.onStart();
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        if (mDialog != null) {
+            mDialog.hide();
+        }
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        final TreeSet<Long> contactIds = (TreeSet<Long>) args.getSerializable(ARG_CONTACT_IDS);
+        final Object[] parameterObject = contactIds.toArray();
+        final String[] parameters = new String[contactIds.size()];
+
+        final StringBuilder builder = new StringBuilder();
+        for (int i = 0; i < contactIds.size(); i++) {
+            parameters[i] = String.valueOf(parameterObject[i]);
+            builder.append(RawContacts.CONTACT_ID + " =?");
+            if (i == contactIds.size() -1) {
+                break;
+            }
+            builder.append(" OR ");
+        }
+        return new CursorLoader(mContext, RawContacts.CONTENT_URI, RAW_CONTACT_PROJECTION,
+                builder.toString(), parameters, null);
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+        if (mDialog != null) {
+            mDialog.dismiss();
+            mDialog = null;
+        }
+
+        if (!mIsLoaderActive) {
+            return;
+        }
+
+        if (cursor == null || cursor.isClosed()) {
+            Log.e(TAG, "Failed to load contacts");
+            return;
+        }
+
+        // This cursor may contain duplicate raw contacts, so we need to de-dupe them first
+        final HashSet<Long> readOnlyRawContacts = Sets.newHashSet();
+        final HashSet<Long> writableRawContacts = Sets.newHashSet();
+        final HashSet<Long> contactIds = Sets.newHashSet();
+
+        AccountTypeManager accountTypes = AccountTypeManager.getInstance(getActivity());
+        cursor.moveToPosition(-1);
+        while (cursor.moveToNext()) {
+            final long rawContactId = cursor.getLong(COLUMN_INDEX_RAW_CONTACT_ID);
+            final String accountType = cursor.getString(COLUMN_INDEX_ACCOUNT_TYPE);
+            final String dataSet = cursor.getString(COLUMN_INDEX_DATA_SET);
+            final long contactId = cursor.getLong(COLUMN_INDEX_CONTACT_ID);
+            contactIds.add(contactId);
+            final AccountType type = accountTypes.getAccountType(accountType, dataSet);
+            boolean writable = type == null || type.areContactsWritable();
+            if (writable) {
+                writableRawContacts.add(rawContactId);
+            } else {
+                readOnlyRawContacts.add(rawContactId);
+            }
+        }
+
+        final int readOnlyCount = readOnlyRawContacts.size();
+        final int writableCount = writableRawContacts.size();
+
+        final int messageId;
+        if (readOnlyCount > 0 && writableCount > 0) {
+            messageId = R.string.batch_delete_multiple_accounts_confirmation;
+        } else if (readOnlyCount > 0 && writableCount == 0) {
+            messageId = R.string.batch_delete_read_only_contact_confirmation;
+        } else {
+            messageId = R.string.batch_delete_confirmation;
+        }
+
+        // Convert set of contact ids into a format that is easily parcellable and iterated upon
+        // for the sake of ContactSaveService.
+        final Long[] contactIdObjectArray = contactIds.toArray(new Long[contactIds.size()]);
+        final long[] contactIdArray = new long[contactIds.size()];
+        for (int i = 0; i < contactIds.size(); i++) {
+            contactIdArray[i] = contactIdObjectArray[i];
+        }
+
+        showDialog(messageId, contactIdArray);
+
+        // We don't want onLoadFinished() calls any more, which may come when the database is
+        // updating.
+        getLoaderManager().destroyLoader(R.id.dialog_delete_multiple_contact_loader_id);
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+    }
+
+    private void showDialog(int messageId, final long[] contactIds) {
+        mDialog = new AlertDialog.Builder(getActivity())
+                .setIconAttribute(android.R.attr.alertDialogIcon)
+                .setMessage(messageId)
+                .setNegativeButton(android.R.string.cancel, null)
+                .setPositiveButton(android.R.string.ok,
+                    new DialogInterface.OnClickListener() {
+                        @Override
+                        public void onClick(DialogInterface dialog, int whichButton) {
+                            doDeleteContact(contactIds);
+                        }
+                    }
+                )
+                .create();
+
+        mDialog.setOnDismissListener(new OnDismissListener() {
+            @Override
+            public void onDismiss(DialogInterface dialog) {
+                mIsLoaderActive = false;
+                mDialog = null;
+            }
+        });
+        mDialog.show();
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putBoolean(KEY_ACTIVE, mIsLoaderActive);
+        outState.putSerializable(KEY_CONTACTS_IDS, mContactIds);
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        if (savedInstanceState != null) {
+            mIsLoaderActive = savedInstanceState.getBoolean(KEY_ACTIVE);
+            mContactIds = (TreeSet<Long>) savedInstanceState.getSerializable(KEY_CONTACTS_IDS);
+        }
+    }
+
+    protected void doDeleteContact(long[] contactIds) {
+        mContext.startService(ContactSaveService.createDeleteMultipleContactsIntent(mContext,
+                contactIds));
+        notifyListenerActivity();
+    }
+
+    private void notifyListenerActivity() {
+        if (getActivity() instanceof MultiContactDeleteListener) {
+            final MultiContactDeleteListener listener = (MultiContactDeleteListener) getActivity();
+            listener.onDeletionFinished();
+        }
+    }
+}
diff --git a/src/com/android/contacts/list/MultiSelectContactsListFragment.java b/src/com/android/contacts/list/MultiSelectContactsListFragment.java
index 5e17aee..9716ae0 100644
--- a/src/com/android/contacts/list/MultiSelectContactsListFragment.java
+++ b/src/com/android/contacts/list/MultiSelectContactsListFragment.java
@@ -92,9 +92,14 @@
     public void displayCheckBoxes(boolean displayCheckBoxes) {
         getAdapter().setDisplayCheckBoxes(displayCheckBoxes);
         if (!displayCheckBoxes) {
-            getAdapter().setSelectedContactIds(new TreeSet<Long>());
+            clearCheckBoxes();
         }
     }
+
+    public void clearCheckBoxes() {
+        getAdapter().setSelectedContactIds(new TreeSet<Long>());
+    }
+
     @Override
     protected boolean onItemLongClick(int position, long id) {
         final MultiSelectEntryContactListAdapter adapter = getAdapter();