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