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