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