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