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