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 
17 package com.android.contacts.interactions;
18 
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.app.Fragment;
22 import android.app.FragmentManager;
23 import android.app.LoaderManager.LoaderCallbacks;
24 import android.content.Context;
25 import android.content.CursorLoader;
26 import android.content.DialogInterface;
27 import android.content.DialogInterface.OnDismissListener;
28 import android.content.Loader;
29 import android.database.Cursor;
30 import android.os.Bundle;
31 import android.provider.ContactsContract.RawContacts;
32 import android.text.TextUtils;
33 import android.util.Log;
34 
35 import com.android.contacts.ContactSaveService;
36 import com.android.contacts.R;
37 import com.android.contacts.model.AccountTypeManager;
38 import com.android.contacts.model.account.AccountType;
39 import com.android.contacts.preference.ContactsPreferences;
40 import com.android.contacts.util.ContactDisplayUtils;
41 
42 import com.google.common.collect.Sets;
43 
44 import java.util.HashSet;
45 import java.util.TreeSet;
46 
47 /**
48  * An interaction invoked to delete multiple contacts.
49  *
50  * This class is very similar to {@link ContactDeletionInteraction}.
51  */
52 public class ContactMultiDeletionInteraction extends Fragment
53         implements LoaderCallbacks<Cursor> {
54 
55     public interface MultiContactDeleteListener {
onDeletionFinished()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      */
start( Fragment hostFragment, TreeSet<Long> contactIds)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
onAttach(Activity activity)115     public void onAttach(Activity activity) {
116         super.onAttach(activity);
117         mContext = activity;
118     }
119 
120     @Override
onDestroyView()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 
setContactIds(TreeSet<Long> contactIds)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 
isStarted()141     private boolean isStarted() {
142         return isAdded();
143     }
144 
145     @Override
onStart()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
onStop()157     public void onStop() {
158         super.onStop();
159         if (mDialog != null) {
160             mDialog.hide();
161         }
162     }
163 
164     @Override
onCreateLoader(int id, Bundle args)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
onLoadFinished(Loader<Cursor> loader, Cursor cursor)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
onLoaderReset(Loader<Cursor> loader)268     public void onLoaderReset(Loader<Cursor> loader) {
269     }
270 
showDialog(int messageId, int positiveButtonId, final long[] contactIds, final String[] namesArray)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
onSaveInstanceState(Bundle outState)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
onActivityCreated(Bundle savedInstanceState)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 
doDeleteContact(long[] contactIds, final String[] names)313     protected void doDeleteContact(long[] contactIds, final String[] names) {
314         mContext.startService(ContactSaveService.createDeleteMultipleContactsIntent(mContext,
315                 contactIds, names));
316         mListener.onDeletionFinished();
317     }
318 
setListener(MultiContactDeleteListener listener)319     public void setListener(MultiContactDeleteListener listener) {
320         mListener = listener;
321     }
322 }
323