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