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 com.google.common.collect.Sets;
20 
21 import com.android.contacts.ContactSaveService;
22 import com.android.contacts.R;
23 import com.android.contacts.common.model.AccountTypeManager;
24 import com.android.contacts.common.model.account.AccountType;
25 
26 import android.app.Activity;
27 import android.app.AlertDialog;
28 import android.app.Fragment;
29 import android.app.FragmentManager;
30 import android.app.LoaderManager.LoaderCallbacks;
31 import android.content.Context;
32 import android.content.CursorLoader;
33 import android.content.DialogInterface;
34 import android.content.DialogInterface.OnDismissListener;
35 import android.content.Loader;
36 import android.database.Cursor;
37 import android.os.Bundle;
38 import android.provider.ContactsContract.RawContacts;
39 import android.util.Log;
40 
41 import java.util.HashSet;
42 import java.util.TreeSet;
43 
44 /**
45  * An interaction invoked to delete multiple contacts.
46  *
47  * This class is very similar to {@link ContactDeletionInteraction}.
48  */
49 public class ContactMultiDeletionInteraction extends Fragment
50         implements LoaderCallbacks<Cursor> {
51 
52     public interface MultiContactDeleteListener {
onDeletionFinished()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      */
start( Activity activity, TreeSet<Long> contactIds)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
onAttach(Activity activity)107     public void onAttach(Activity activity) {
108         super.onAttach(activity);
109         mContext = activity;
110     }
111 
112     @Override
onDestroyView()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 
setContactIds(TreeSet<Long> contactIds)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 
isStarted()133     private boolean isStarted() {
134         return isAdded();
135     }
136 
137     @Override
onStart()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
onStop()149     public void onStop() {
150         super.onStop();
151         if (mDialog != null) {
152             mDialog.hide();
153         }
154     }
155 
156     @Override
onCreateLoader(int id, Bundle args)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
onLoadFinished(Loader<Cursor> loader, Cursor cursor)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         int positiveButtonId = android.R.string.ok;
218         if (readOnlyCount > 0 && writableCount > 0) {
219             messageId = R.string.batch_delete_multiple_accounts_confirmation;
220         } else if (readOnlyCount > 0 && writableCount == 0) {
221             messageId = R.string.batch_delete_read_only_contact_confirmation;
222             positiveButtonId = R.string.readOnlyContactWarning_positive_button;
223         } else if (writableCount == 1) {
224             messageId = R.string.single_delete_confirmation;
225             positiveButtonId = R.string.deleteConfirmation_positive_button;
226         } else {
227             messageId = R.string.batch_delete_confirmation;
228             positiveButtonId = R.string.deleteConfirmation_positive_button;
229         }
230 
231         // Convert set of contact ids into a format that is easily parcellable and iterated upon
232         // for the sake of ContactSaveService.
233         final Long[] contactIdObjectArray = contactIds.toArray(new Long[contactIds.size()]);
234         final long[] contactIdArray = new long[contactIds.size()];
235         for (int i = 0; i < contactIds.size(); i++) {
236             contactIdArray[i] = contactIdObjectArray[i];
237         }
238 
239         showDialog(messageId, positiveButtonId, contactIdArray);
240 
241         // We don't want onLoadFinished() calls any more, which may come when the database is
242         // updating.
243         getLoaderManager().destroyLoader(R.id.dialog_delete_multiple_contact_loader_id);
244     }
245 
246     @Override
onLoaderReset(Loader<Cursor> loader)247     public void onLoaderReset(Loader<Cursor> loader) {
248     }
249 
showDialog(int messageId, int positiveButtonId, final long[] contactIds)250     private void showDialog(int messageId, int positiveButtonId, final long[] contactIds) {
251         mDialog = new AlertDialog.Builder(getActivity())
252                 .setIconAttribute(android.R.attr.alertDialogIcon)
253                 .setMessage(messageId)
254                 .setNegativeButton(android.R.string.cancel, null)
255                 .setPositiveButton(positiveButtonId,
256                     new DialogInterface.OnClickListener() {
257                         @Override
258                         public void onClick(DialogInterface dialog, int whichButton) {
259                             doDeleteContact(contactIds);
260                         }
261                     }
262                 )
263                 .create();
264 
265         mDialog.setOnDismissListener(new OnDismissListener() {
266             @Override
267             public void onDismiss(DialogInterface dialog) {
268                 mIsLoaderActive = false;
269                 mDialog = null;
270             }
271         });
272         mDialog.show();
273     }
274 
275     @Override
onSaveInstanceState(Bundle outState)276     public void onSaveInstanceState(Bundle outState) {
277         super.onSaveInstanceState(outState);
278         outState.putBoolean(KEY_ACTIVE, mIsLoaderActive);
279         outState.putSerializable(KEY_CONTACTS_IDS, mContactIds);
280     }
281 
282     @Override
onActivityCreated(Bundle savedInstanceState)283     public void onActivityCreated(Bundle savedInstanceState) {
284         super.onActivityCreated(savedInstanceState);
285         if (savedInstanceState != null) {
286             mIsLoaderActive = savedInstanceState.getBoolean(KEY_ACTIVE);
287             mContactIds = (TreeSet<Long>) savedInstanceState.getSerializable(KEY_CONTACTS_IDS);
288         }
289     }
290 
doDeleteContact(long[] contactIds)291     protected void doDeleteContact(long[] contactIds) {
292         mContext.startService(ContactSaveService.createDeleteMultipleContactsIntent(mContext,
293                 contactIds));
294         notifyListenerActivity();
295     }
296 
notifyListenerActivity()297     private void notifyListenerActivity() {
298         if (getActivity() instanceof MultiContactDeleteListener) {
299             final MultiContactDeleteListener listener = (MultiContactDeleteListener) getActivity();
300             listener.onDeletionFinished();
301         }
302     }
303 }
304