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