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