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
17package com.android.contacts.interactions;
18
19import android.app.Activity;
20import android.app.AlertDialog;
21import android.app.Fragment;
22import android.app.FragmentManager;
23import android.app.LoaderManager;
24import android.app.LoaderManager.LoaderCallbacks;
25import android.content.Context;
26import android.content.CursorLoader;
27import android.content.DialogInterface;
28import android.content.DialogInterface.OnDismissListener;
29import android.content.Loader;
30import android.database.Cursor;
31import android.net.Uri;
32import android.os.Bundle;
33import android.provider.ContactsContract.Contacts;
34import android.provider.ContactsContract.Contacts.Entity;
35import android.text.TextUtils;
36import android.util.Log;
37import android.widget.Toast;
38
39import com.android.contacts.ContactSaveService;
40import com.android.contacts.R;
41import com.android.contacts.common.model.AccountTypeManager;
42import com.android.contacts.common.model.account.AccountType;
43import com.google.common.annotations.VisibleForTesting;
44import com.google.common.collect.Sets;
45
46import java.util.HashSet;
47
48/**
49 * An interaction invoked to delete a contact.
50 */
51public 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     */
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
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
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. */
154    private void setTestLoaderManager(TestLoaderManagerBase mockLoaderManager) {
155        mTestLoaderManager = mockLoaderManager;
156    }
157
158    @Override
159    public void onAttach(Activity activity) {
160        super.onAttach(activity);
161        mContext = activity;
162    }
163
164    @Override
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
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
184    private void setFinishActivityWhenDone(boolean finishActivityWhenDone) {
185        this.mFinishActivityWhenDone = finishActivityWhenDone;
186
187    }
188
189    /* Visible for testing */
190    boolean isStarted() {
191        return isAdded();
192    }
193
194    @Override
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
205    public void onStop() {
206        super.onStop();
207        if (mDialog != null) {
208            mDialog.hide();
209        }
210    }
211
212    @Override
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
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
290    public void onLoaderReset(Loader<Cursor> loader) {
291    }
292
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
313    public void onDismiss(DialogInterface dialog) {
314        mActive = false;
315        mDialog = null;
316    }
317
318    @Override
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
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
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