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