1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.contacts.interactions;
18
19import android.app.Activity;
20import android.app.AlertDialog;
21import android.app.Fragment;
22import android.app.FragmentManager;
23import android.app.LoaderManager.LoaderCallbacks;
24import android.content.Context;
25import android.content.CursorLoader;
26import android.content.DialogInterface;
27import android.content.DialogInterface.OnDismissListener;
28import android.content.Loader;
29import android.database.Cursor;
30import android.os.Bundle;
31import android.provider.ContactsContract.RawContacts;
32import android.text.TextUtils;
33import android.util.Log;
34
35import com.android.contacts.ContactSaveService;
36import com.android.contacts.R;
37import com.android.contacts.model.AccountTypeManager;
38import com.android.contacts.model.account.AccountType;
39import com.android.contacts.preference.ContactsPreferences;
40import com.android.contacts.util.ContactDisplayUtils;
41
42import com.google.common.collect.Sets;
43
44import java.util.HashSet;
45import java.util.TreeSet;
46
47/**
48 * An interaction invoked to delete multiple contacts.
49 *
50 * This class is very similar to {@link ContactDeletionInteraction}.
51 */
52public class ContactMultiDeletionInteraction extends Fragment
53        implements LoaderCallbacks<Cursor> {
54
55    public interface MultiContactDeleteListener {
56        void onDeletionFinished();
57    }
58
59    private static final String FRAGMENT_TAG = "deleteMultipleContacts";
60    private static final String TAG = "ContactMultiDeletion";
61    private static final String KEY_ACTIVE = "active";
62    private static final String KEY_CONTACTS_IDS = "contactIds";
63    public static final String ARG_CONTACT_IDS = "contactIds";
64
65    private static final String[] RAW_CONTACT_PROJECTION = new String[] {
66            RawContacts._ID,
67            RawContacts.ACCOUNT_TYPE,
68            RawContacts.DATA_SET,
69            RawContacts.CONTACT_ID,
70            RawContacts.DISPLAY_NAME_PRIMARY,
71            RawContacts.DISPLAY_NAME_ALTERNATIVE
72    };
73
74    private static final int COLUMN_INDEX_RAW_CONTACT_ID = 0;
75    private static final int COLUMN_INDEX_ACCOUNT_TYPE = 1;
76    private static final int COLUMN_INDEX_DATA_SET = 2;
77    private static final int COLUMN_INDEX_CONTACT_ID = 3;
78    private static final int COLUMN_INDEX_DISPLAY_NAME = 4;
79    private static final int COLUMN_INDEX_DISPLAY_NAME_ALT = 5;
80
81    private boolean mIsLoaderActive;
82    private TreeSet<Long> mContactIds;
83    private Context mContext;
84    private AlertDialog mDialog;
85    private MultiContactDeleteListener mListener;
86
87    /**
88     * Starts the interaction.
89     *
90     * @param hostFragment the fragment within which to start the interaction
91     * @param contactIds the IDs of contacts to be deleted
92     * @return the newly created interaction
93     */
94    public static ContactMultiDeletionInteraction start(
95            Fragment hostFragment, TreeSet<Long> contactIds) {
96        if (contactIds == null) {
97            return null;
98        }
99
100        final FragmentManager fragmentManager = hostFragment.getFragmentManager();
101        ContactMultiDeletionInteraction fragment =
102                (ContactMultiDeletionInteraction) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
103        if (fragment == null) {
104            fragment = new ContactMultiDeletionInteraction();
105            fragment.setContactIds(contactIds);
106            fragmentManager.beginTransaction().add(fragment, FRAGMENT_TAG)
107                    .commitAllowingStateLoss();
108        } else {
109            fragment.setContactIds(contactIds);
110        }
111        return fragment;
112    }
113
114    @Override
115    public void onAttach(Activity activity) {
116        super.onAttach(activity);
117        mContext = activity;
118    }
119
120    @Override
121    public void onDestroyView() {
122        super.onDestroyView();
123        if (mDialog != null && mDialog.isShowing()) {
124            mDialog.setOnDismissListener(null);
125            mDialog.dismiss();
126            mDialog = null;
127        }
128    }
129
130    public void setContactIds(TreeSet<Long> contactIds) {
131        mContactIds = contactIds;
132        mIsLoaderActive = true;
133        if (isStarted()) {
134            Bundle args = new Bundle();
135            args.putSerializable(ARG_CONTACT_IDS, mContactIds);
136            getLoaderManager().restartLoader(R.id.dialog_delete_multiple_contact_loader_id,
137                    args, this);
138        }
139    }
140
141    private boolean isStarted() {
142        return isAdded();
143    }
144
145    @Override
146    public void onStart() {
147        if (mIsLoaderActive) {
148            Bundle args = new Bundle();
149            args.putSerializable(ARG_CONTACT_IDS, mContactIds);
150            getLoaderManager().initLoader(
151                    R.id.dialog_delete_multiple_contact_loader_id, args, this);
152        }
153        super.onStart();
154    }
155
156    @Override
157    public void onStop() {
158        super.onStop();
159        if (mDialog != null) {
160            mDialog.hide();
161        }
162    }
163
164    @Override
165    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
166        final TreeSet<Long> contactIds = (TreeSet<Long>) args.getSerializable(ARG_CONTACT_IDS);
167        final Object[] parameterObject = contactIds.toArray();
168        final String[] parameters = new String[contactIds.size()];
169
170        final StringBuilder builder = new StringBuilder();
171        for (int i = 0; i < contactIds.size(); i++) {
172            parameters[i] = String.valueOf(parameterObject[i]);
173            builder.append(RawContacts.CONTACT_ID + " =?");
174            if (i == contactIds.size() -1) {
175                break;
176            }
177            builder.append(" OR ");
178        }
179        return new CursorLoader(mContext, RawContacts.CONTENT_URI, RAW_CONTACT_PROJECTION,
180                builder.toString(), parameters, null);
181    }
182
183    @Override
184    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
185        if (mDialog != null) {
186            mDialog.dismiss();
187            mDialog = null;
188        }
189
190        if (!mIsLoaderActive) {
191            return;
192        }
193
194        if (cursor == null || cursor.isClosed()) {
195            Log.e(TAG, "Failed to load contacts");
196            return;
197        }
198
199        // This cursor may contain duplicate raw contacts, so we need to de-dupe them first
200        final HashSet<Long> readOnlyRawContacts = Sets.newHashSet();
201        final HashSet<Long> writableRawContacts = Sets.newHashSet();
202        final HashSet<Long> contactIds = Sets.newHashSet();
203        final HashSet<String> names = Sets.newHashSet();
204
205        final ContactsPreferences contactsPreferences = new ContactsPreferences(mContext);
206
207        AccountTypeManager accountTypes = AccountTypeManager.getInstance(getActivity());
208        cursor.moveToPosition(-1);
209        while (cursor.moveToNext()) {
210            final long rawContactId = cursor.getLong(COLUMN_INDEX_RAW_CONTACT_ID);
211            final String accountType = cursor.getString(COLUMN_INDEX_ACCOUNT_TYPE);
212            final String dataSet = cursor.getString(COLUMN_INDEX_DATA_SET);
213            final long contactId = cursor.getLong(COLUMN_INDEX_CONTACT_ID);
214            final String displayName = cursor.getString(COLUMN_INDEX_DISPLAY_NAME);
215            final String displayNameAlt = cursor.getString(COLUMN_INDEX_DISPLAY_NAME_ALT);
216
217            final String name = ContactDisplayUtils.getPreferredDisplayName(displayName,
218                    displayNameAlt, contactsPreferences);
219            if (!TextUtils.isEmpty(name)) {
220                names.add(name);
221            }
222
223            contactIds.add(contactId);
224            final AccountType type = accountTypes.getAccountType(accountType, dataSet);
225            boolean writable = type == null || type.areContactsWritable();
226            if (writable) {
227                writableRawContacts.add(rawContactId);
228            } else {
229                readOnlyRawContacts.add(rawContactId);
230            }
231        }
232
233        final int readOnlyCount = readOnlyRawContacts.size();
234        final int writableCount = writableRawContacts.size();
235
236        final int messageId;
237        int positiveButtonId = android.R.string.ok;
238        if (readOnlyCount > 0 && writableCount > 0) {
239            messageId = R.string.batch_delete_multiple_accounts_confirmation;
240        } else if (readOnlyCount > 0 && writableCount == 0) {
241            messageId = R.string.batch_delete_read_only_contact_confirmation;
242            positiveButtonId = R.string.readOnlyContactWarning_positive_button;
243        } else if (writableCount == 1) {
244            messageId = R.string.single_delete_confirmation;
245            positiveButtonId = R.string.deleteConfirmation_positive_button;
246        } else {
247            messageId = R.string.batch_delete_confirmation;
248            positiveButtonId = R.string.deleteConfirmation_positive_button;
249        }
250
251        // Convert set of contact ids into a format that is easily parcellable and iterated upon
252        // for the sake of ContactSaveService.
253        final Long[] contactIdObjectArray = contactIds.toArray(new Long[contactIds.size()]);
254        final long[] contactIdArray = new long[contactIds.size()];
255        for (int i = 0; i < contactIds.size(); i++) {
256            contactIdArray[i] = contactIdObjectArray[i];
257        }
258
259        final String[] namesArray = names.toArray(new String[names.size()]);
260        showDialog(messageId, positiveButtonId, contactIdArray, namesArray);
261
262        // We don't want onLoadFinished() calls any more, which may come when the database is
263        // updating.
264        getLoaderManager().destroyLoader(R.id.dialog_delete_multiple_contact_loader_id);
265    }
266
267    @Override
268    public void onLoaderReset(Loader<Cursor> loader) {
269    }
270
271    private void showDialog(int messageId, int positiveButtonId, final long[] contactIds,
272            final String[] namesArray) {
273        mDialog = new AlertDialog.Builder(getActivity())
274                .setIconAttribute(android.R.attr.alertDialogIcon)
275                .setMessage(messageId)
276                .setNegativeButton(android.R.string.cancel, null)
277                .setPositiveButton(positiveButtonId,
278                    new DialogInterface.OnClickListener() {
279                        @Override
280                        public void onClick(DialogInterface dialog, int whichButton) {
281                            doDeleteContact(contactIds, namesArray);
282                        }
283                    }
284                )
285                .create();
286
287        mDialog.setOnDismissListener(new OnDismissListener() {
288            @Override
289            public void onDismiss(DialogInterface dialog) {
290                mIsLoaderActive = false;
291                mDialog = null;
292            }
293        });
294        mDialog.show();
295    }
296
297    @Override
298    public void onSaveInstanceState(Bundle outState) {
299        super.onSaveInstanceState(outState);
300        outState.putBoolean(KEY_ACTIVE, mIsLoaderActive);
301        outState.putSerializable(KEY_CONTACTS_IDS, mContactIds);
302    }
303
304    @Override
305    public void onActivityCreated(Bundle savedInstanceState) {
306        super.onActivityCreated(savedInstanceState);
307        if (savedInstanceState != null) {
308            mIsLoaderActive = savedInstanceState.getBoolean(KEY_ACTIVE);
309            mContactIds = (TreeSet<Long>) savedInstanceState.getSerializable(KEY_CONTACTS_IDS);
310        }
311    }
312
313    protected void doDeleteContact(long[] contactIds, final String[] names) {
314        mContext.startService(ContactSaveService.createDeleteMultipleContactsIntent(mContext,
315                contactIds, names));
316        mListener.onDeletionFinished();
317    }
318
319    public void setListener(MultiContactDeleteListener listener) {
320        mListener = listener;
321    }
322}
323