1 /*
2  * Copyright (C) 2010 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;
24 import android.app.LoaderManager.LoaderCallbacks;
25 import android.content.Context;
26 import android.content.CursorLoader;
27 import android.content.DialogInterface;
28 import android.content.DialogInterface.OnDismissListener;
29 import android.content.Loader;
30 import android.database.Cursor;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.provider.ContactsContract.Contacts;
34 import android.provider.ContactsContract.Contacts.Entity;
35 import android.text.TextUtils;
36 import android.util.Log;
37 import android.widget.Toast;
38 
39 import com.android.contacts.ContactSaveService;
40 import com.android.contacts.R;
41 import com.android.contacts.common.model.AccountTypeManager;
42 import com.android.contacts.common.model.account.AccountType;
43 import com.google.common.annotations.VisibleForTesting;
44 import com.google.common.collect.Sets;
45 
46 import java.util.HashSet;
47 
48 /**
49  * An interaction invoked to delete a contact.
50  */
51 public class ContactDeletionInteraction extends Fragment
52         implements LoaderCallbacks<Cursor>, OnDismissListener {
53 
54     private static final String TAG = "ContactDeletionInteraction";
55     private static final String FRAGMENT_TAG = "deleteContact";
56 
57     private static final String KEY_ACTIVE = "active";
58     private static final String KEY_CONTACT_URI = "contactUri";
59     private static final String KEY_FINISH_WHEN_DONE = "finishWhenDone";
60     public static final String ARG_CONTACT_URI = "contactUri";
61     public static final int RESULT_CODE_DELETED = 3;
62 
63     private static final String[] ENTITY_PROJECTION = new String[] {
64         Entity.RAW_CONTACT_ID, //0
65         Entity.ACCOUNT_TYPE, //1
66         Entity.DATA_SET, // 2
67         Entity.CONTACT_ID, // 3
68         Entity.LOOKUP_KEY, // 4
69     };
70 
71     private static final int COLUMN_INDEX_RAW_CONTACT_ID = 0;
72     private static final int COLUMN_INDEX_ACCOUNT_TYPE = 1;
73     private static final int COLUMN_INDEX_DATA_SET = 2;
74     private static final int COLUMN_INDEX_CONTACT_ID = 3;
75     private static final int COLUMN_INDEX_LOOKUP_KEY = 4;
76 
77     private boolean mActive;
78     private Uri mContactUri;
79     private boolean mFinishActivityWhenDone;
80     private Context mContext;
81     private AlertDialog mDialog;
82 
83     /** This is a wrapper around the fragment's loader manager to be used only during testing. */
84     private TestLoaderManagerBase mTestLoaderManager;
85 
86     @VisibleForTesting
87     int mMessageId;
88 
89     /**
90      * Starts the interaction.
91      *
92      * @param activity the activity within which to start the interaction
93      * @param contactUri the URI of the contact to delete
94      * @param finishActivityWhenDone whether to finish the activity upon completion of the
95      *        interaction
96      * @return the newly created interaction
97      */
start( Activity activity, Uri contactUri, boolean finishActivityWhenDone)98     public static ContactDeletionInteraction start(
99             Activity activity, Uri contactUri, boolean finishActivityWhenDone) {
100         return startWithTestLoaderManager(activity, contactUri, finishActivityWhenDone, null);
101     }
102 
103     /**
104      * Starts the interaction and optionally set up a {@link TestLoaderManagerBase}.
105      *
106      * @param activity the activity within which to start the interaction
107      * @param contactUri the URI of the contact to delete
108      * @param finishActivityWhenDone whether to finish the activity upon completion of the
109      *        interaction
110      * @param testLoaderManager the {@link TestLoaderManagerBase} to use to load the data, may be null
111      *        in which case the default {@link LoaderManager} is used
112      * @return the newly created interaction
113      */
114     @VisibleForTesting
startWithTestLoaderManager( Activity activity, Uri contactUri, boolean finishActivityWhenDone, TestLoaderManagerBase testLoaderManager)115     static ContactDeletionInteraction startWithTestLoaderManager(
116             Activity activity, Uri contactUri, boolean finishActivityWhenDone,
117             TestLoaderManagerBase testLoaderManager) {
118         if (contactUri == null || activity.isDestroyed()) {
119             return null;
120         }
121 
122         FragmentManager fragmentManager = activity.getFragmentManager();
123         ContactDeletionInteraction fragment =
124                 (ContactDeletionInteraction) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
125         if (fragment == null) {
126             fragment = new ContactDeletionInteraction();
127             fragment.setTestLoaderManager(testLoaderManager);
128             fragment.setContactUri(contactUri);
129             fragment.setFinishActivityWhenDone(finishActivityWhenDone);
130             fragmentManager.beginTransaction().add(fragment, FRAGMENT_TAG)
131                     .commitAllowingStateLoss();
132         } else {
133             fragment.setTestLoaderManager(testLoaderManager);
134             fragment.setContactUri(contactUri);
135             fragment.setFinishActivityWhenDone(finishActivityWhenDone);
136         }
137         return fragment;
138     }
139 
140     @Override
getLoaderManager()141     public LoaderManager getLoaderManager() {
142         // Return the TestLoaderManager if one is set up.
143         LoaderManager loaderManager = super.getLoaderManager();
144         if (mTestLoaderManager != null) {
145             // Set the delegate: this operation is idempotent, so let's just do it every time.
146             mTestLoaderManager.setDelegate(loaderManager);
147             return mTestLoaderManager;
148         } else {
149             return loaderManager;
150         }
151     }
152 
153     /** Sets the TestLoaderManager that is used to wrap the actual LoaderManager in tests. */
setTestLoaderManager(TestLoaderManagerBase mockLoaderManager)154     private void setTestLoaderManager(TestLoaderManagerBase mockLoaderManager) {
155         mTestLoaderManager = mockLoaderManager;
156     }
157 
158     @Override
onAttach(Activity activity)159     public void onAttach(Activity activity) {
160         super.onAttach(activity);
161         mContext = activity;
162     }
163 
164     @Override
onDestroyView()165     public void onDestroyView() {
166         super.onDestroyView();
167         if (mDialog != null && mDialog.isShowing()) {
168             mDialog.setOnDismissListener(null);
169             mDialog.dismiss();
170             mDialog = null;
171         }
172     }
173 
setContactUri(Uri contactUri)174     public void setContactUri(Uri contactUri) {
175         mContactUri = contactUri;
176         mActive = true;
177         if (isStarted()) {
178             Bundle args = new Bundle();
179             args.putParcelable(ARG_CONTACT_URI, mContactUri);
180             getLoaderManager().restartLoader(R.id.dialog_delete_contact_loader_id, args, this);
181         }
182     }
183 
setFinishActivityWhenDone(boolean finishActivityWhenDone)184     private void setFinishActivityWhenDone(boolean finishActivityWhenDone) {
185         this.mFinishActivityWhenDone = finishActivityWhenDone;
186 
187     }
188 
189     /* Visible for testing */
isStarted()190     boolean isStarted() {
191         return isAdded();
192     }
193 
194     @Override
onStart()195     public void onStart() {
196         if (mActive) {
197             Bundle args = new Bundle();
198             args.putParcelable(ARG_CONTACT_URI, mContactUri);
199             getLoaderManager().initLoader(R.id.dialog_delete_contact_loader_id, args, this);
200         }
201         super.onStart();
202     }
203 
204     @Override
onStop()205     public void onStop() {
206         super.onStop();
207         if (mDialog != null) {
208             mDialog.hide();
209         }
210     }
211 
212     @Override
onCreateLoader(int id, Bundle args)213     public Loader<Cursor> onCreateLoader(int id, Bundle args) {
214         Uri contactUri = args.getParcelable(ARG_CONTACT_URI);
215         return new CursorLoader(mContext,
216                 Uri.withAppendedPath(contactUri, Entity.CONTENT_DIRECTORY), ENTITY_PROJECTION,
217                 null, null, null);
218     }
219 
220     @Override
onLoadFinished(Loader<Cursor> loader, Cursor cursor)221     public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
222         if (mDialog != null) {
223             mDialog.dismiss();
224             mDialog = null;
225         }
226 
227         if (!mActive) {
228             return;
229         }
230 
231         if (cursor == null || cursor.isClosed()) {
232             Log.e(TAG, "Failed to load contacts");
233             return;
234         }
235 
236         long contactId = 0;
237         String lookupKey = null;
238 
239         // This cursor may contain duplicate raw contacts, so we need to de-dupe them first
240         HashSet<Long>  readOnlyRawContacts = Sets.newHashSet();
241         HashSet<Long>  writableRawContacts = Sets.newHashSet();
242 
243         AccountTypeManager accountTypes = AccountTypeManager.getInstance(getActivity());
244         cursor.moveToPosition(-1);
245         while (cursor.moveToNext()) {
246             final long rawContactId = cursor.getLong(COLUMN_INDEX_RAW_CONTACT_ID);
247             final String accountType = cursor.getString(COLUMN_INDEX_ACCOUNT_TYPE);
248             final String dataSet = cursor.getString(COLUMN_INDEX_DATA_SET);
249             contactId = cursor.getLong(COLUMN_INDEX_CONTACT_ID);
250             lookupKey = cursor.getString(COLUMN_INDEX_LOOKUP_KEY);
251             AccountType type = accountTypes.getAccountType(accountType, dataSet);
252             boolean writable = type == null || type.areContactsWritable();
253             if (writable) {
254                 writableRawContacts.add(rawContactId);
255             } else {
256                 readOnlyRawContacts.add(rawContactId);
257             }
258         }
259         if (TextUtils.isEmpty(lookupKey)) {
260             Log.e(TAG, "Failed to find contact lookup key");
261             getActivity().finish();
262             return;
263         }
264 
265         int readOnlyCount = readOnlyRawContacts.size();
266         int writableCount = writableRawContacts.size();
267         int positiveButtonId = android.R.string.ok;
268         if (readOnlyCount > 0 && writableCount > 0) {
269             mMessageId = R.string.readOnlyContactDeleteConfirmation;
270         } else if (readOnlyCount > 0 && writableCount == 0) {
271             mMessageId = R.string.readOnlyContactWarning;
272             positiveButtonId = R.string.readOnlyContactWarning_positive_button;
273         } else if (readOnlyCount == 0 && writableCount > 1) {
274             mMessageId = R.string.multipleContactDeleteConfirmation;
275             positiveButtonId = R.string.deleteConfirmation_positive_button;
276         } else {
277             mMessageId = R.string.deleteConfirmation;
278             positiveButtonId = R.string.deleteConfirmation_positive_button;
279         }
280 
281         final Uri contactUri = Contacts.getLookupUri(contactId, lookupKey);
282         showDialog(mMessageId, positiveButtonId, contactUri);
283 
284         // We don't want onLoadFinished() calls any more, which may come when the database is
285         // updating.
286         getLoaderManager().destroyLoader(R.id.dialog_delete_contact_loader_id);
287     }
288 
289     @Override
onLoaderReset(Loader<Cursor> loader)290     public void onLoaderReset(Loader<Cursor> loader) {
291     }
292 
showDialog(int messageId, int positiveButtonId, final Uri contactUri)293     private void showDialog(int messageId, int positiveButtonId, final Uri contactUri) {
294         mDialog = new AlertDialog.Builder(getActivity())
295                 .setIconAttribute(android.R.attr.alertDialogIcon)
296                 .setMessage(messageId)
297                 .setNegativeButton(android.R.string.cancel, null)
298                 .setPositiveButton(positiveButtonId,
299                     new DialogInterface.OnClickListener() {
300                         @Override
301                         public void onClick(DialogInterface dialog, int whichButton) {
302                             doDeleteContact(contactUri);
303                         }
304                     }
305                 )
306                 .create();
307 
308         mDialog.setOnDismissListener(this);
309         mDialog.show();
310     }
311 
312     @Override
onDismiss(DialogInterface dialog)313     public void onDismiss(DialogInterface dialog) {
314         mActive = false;
315         mDialog = null;
316     }
317 
318     @Override
onSaveInstanceState(Bundle outState)319     public void onSaveInstanceState(Bundle outState) {
320         super.onSaveInstanceState(outState);
321         outState.putBoolean(KEY_ACTIVE, mActive);
322         outState.putParcelable(KEY_CONTACT_URI, mContactUri);
323         outState.putBoolean(KEY_FINISH_WHEN_DONE, mFinishActivityWhenDone);
324     }
325 
326     @Override
onActivityCreated(Bundle savedInstanceState)327     public void onActivityCreated(Bundle savedInstanceState) {
328         super.onActivityCreated(savedInstanceState);
329         if (savedInstanceState != null) {
330             mActive = savedInstanceState.getBoolean(KEY_ACTIVE);
331             mContactUri = savedInstanceState.getParcelable(KEY_CONTACT_URI);
332             mFinishActivityWhenDone = savedInstanceState.getBoolean(KEY_FINISH_WHEN_DONE);
333         }
334     }
335 
doDeleteContact(Uri contactUri)336     protected void doDeleteContact(Uri contactUri) {
337         mContext.startService(ContactSaveService.createDeleteContactIntent(mContext, contactUri));
338         if (isAdded() && mFinishActivityWhenDone) {
339             getActivity().setResult(RESULT_CODE_DELETED);
340             getActivity().finish();
341             final String deleteToastMessage = getResources().getQuantityString(R.plurals
342                     .contacts_deleted_toast, /* quantity */ 1);
343             Toast.makeText(mContext, deleteToastMessage, Toast.LENGTH_LONG).show();
344         }
345     }
346 }
347