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