1 /* 2 * Copyright (C) 2015 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.LoaderCallbacks; 24 import android.content.Context; 25 import android.content.CursorLoader; 26 import android.content.DialogInterface; 27 import android.content.DialogInterface.OnDismissListener; 28 import android.content.Loader; 29 import android.database.Cursor; 30 import android.os.Bundle; 31 import android.provider.ContactsContract.RawContacts; 32 import android.text.TextUtils; 33 import android.util.Log; 34 35 import com.android.contacts.ContactSaveService; 36 import com.android.contacts.R; 37 import com.android.contacts.model.AccountTypeManager; 38 import com.android.contacts.model.account.AccountType; 39 import com.android.contacts.preference.ContactsPreferences; 40 import com.android.contacts.util.ContactDisplayUtils; 41 42 import com.google.common.collect.Sets; 43 44 import java.util.HashSet; 45 import java.util.TreeSet; 46 47 /** 48 * An interaction invoked to delete multiple contacts. 49 * 50 * This class is very similar to {@link ContactDeletionInteraction}. 51 */ 52 public class ContactMultiDeletionInteraction extends Fragment 53 implements LoaderCallbacks<Cursor> { 54 55 public interface MultiContactDeleteListener { 56 void onDeletionFinished(); 57 } 58 59 private static final String FRAGMENT_TAG = "deleteMultipleContacts"; 60 private static final String TAG = "ContactMultiDeletion"; 61 private static final String KEY_ACTIVE = "active"; 62 private static final String KEY_CONTACTS_IDS = "contactIds"; 63 public static final String ARG_CONTACT_IDS = "contactIds"; 64 65 private static final String[] RAW_CONTACT_PROJECTION = new String[] { 66 RawContacts._ID, 67 RawContacts.ACCOUNT_TYPE, 68 RawContacts.DATA_SET, 69 RawContacts.CONTACT_ID, 70 RawContacts.DISPLAY_NAME_PRIMARY, 71 RawContacts.DISPLAY_NAME_ALTERNATIVE 72 }; 73 74 private static final int COLUMN_INDEX_RAW_CONTACT_ID = 0; 75 private static final int COLUMN_INDEX_ACCOUNT_TYPE = 1; 76 private static final int COLUMN_INDEX_DATA_SET = 2; 77 private static final int COLUMN_INDEX_CONTACT_ID = 3; 78 private static final int COLUMN_INDEX_DISPLAY_NAME = 4; 79 private static final int COLUMN_INDEX_DISPLAY_NAME_ALT = 5; 80 81 private boolean mIsLoaderActive; 82 private TreeSet<Long> mContactIds; 83 private Context mContext; 84 private AlertDialog mDialog; 85 private MultiContactDeleteListener mListener; 86 87 /** 88 * Starts the interaction. 89 * 90 * @param hostFragment the fragment within which to start the interaction 91 * @param contactIds the IDs of contacts to be deleted 92 * @return the newly created interaction 93 */ 94 public static ContactMultiDeletionInteraction start( 95 Fragment hostFragment, TreeSet<Long> contactIds) { 96 if (contactIds == null) { 97 return null; 98 } 99 100 final FragmentManager fragmentManager = hostFragment.getFragmentManager(); 101 ContactMultiDeletionInteraction fragment = 102 (ContactMultiDeletionInteraction) fragmentManager.findFragmentByTag(FRAGMENT_TAG); 103 if (fragment == null) { 104 fragment = new ContactMultiDeletionInteraction(); 105 fragment.setContactIds(contactIds); 106 fragmentManager.beginTransaction().add(fragment, FRAGMENT_TAG) 107 .commitAllowingStateLoss(); 108 } else { 109 fragment.setContactIds(contactIds); 110 } 111 return fragment; 112 } 113 114 @Override 115 public void onAttach(Activity activity) { 116 super.onAttach(activity); 117 mContext = activity; 118 } 119 120 @Override 121 public void onDestroyView() { 122 super.onDestroyView(); 123 if (mDialog != null && mDialog.isShowing()) { 124 mDialog.setOnDismissListener(null); 125 mDialog.dismiss(); 126 mDialog = null; 127 } 128 } 129 130 public void setContactIds(TreeSet<Long> contactIds) { 131 mContactIds = contactIds; 132 mIsLoaderActive = true; 133 if (isStarted()) { 134 Bundle args = new Bundle(); 135 args.putSerializable(ARG_CONTACT_IDS, mContactIds); 136 getLoaderManager().restartLoader(R.id.dialog_delete_multiple_contact_loader_id, 137 args, this); 138 } 139 } 140 141 private boolean isStarted() { 142 return isAdded(); 143 } 144 145 @Override 146 public void onStart() { 147 if (mIsLoaderActive) { 148 Bundle args = new Bundle(); 149 args.putSerializable(ARG_CONTACT_IDS, mContactIds); 150 getLoaderManager().initLoader( 151 R.id.dialog_delete_multiple_contact_loader_id, args, this); 152 } 153 super.onStart(); 154 } 155 156 @Override 157 public void onStop() { 158 super.onStop(); 159 if (mDialog != null) { 160 mDialog.hide(); 161 } 162 } 163 164 @Override 165 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 166 final TreeSet<Long> contactIds = (TreeSet<Long>) args.getSerializable(ARG_CONTACT_IDS); 167 final Object[] parameterObject = contactIds.toArray(); 168 final String[] parameters = new String[contactIds.size()]; 169 170 final StringBuilder builder = new StringBuilder(); 171 for (int i = 0; i < contactIds.size(); i++) { 172 parameters[i] = String.valueOf(parameterObject[i]); 173 builder.append(RawContacts.CONTACT_ID + " =?"); 174 if (i == contactIds.size() -1) { 175 break; 176 } 177 builder.append(" OR "); 178 } 179 return new CursorLoader(mContext, RawContacts.CONTENT_URI, RAW_CONTACT_PROJECTION, 180 builder.toString(), parameters, null); 181 } 182 183 @Override 184 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 185 if (mDialog != null) { 186 mDialog.dismiss(); 187 mDialog = null; 188 } 189 190 if (!mIsLoaderActive) { 191 return; 192 } 193 194 if (cursor == null || cursor.isClosed()) { 195 Log.e(TAG, "Failed to load contacts"); 196 return; 197 } 198 199 // This cursor may contain duplicate raw contacts, so we need to de-dupe them first 200 final HashSet<Long> readOnlyRawContacts = Sets.newHashSet(); 201 final HashSet<Long> writableRawContacts = Sets.newHashSet(); 202 final HashSet<Long> contactIds = Sets.newHashSet(); 203 final HashSet<String> names = Sets.newHashSet(); 204 205 final ContactsPreferences contactsPreferences = new ContactsPreferences(mContext); 206 207 AccountTypeManager accountTypes = AccountTypeManager.getInstance(getActivity()); 208 cursor.moveToPosition(-1); 209 while (cursor.moveToNext()) { 210 final long rawContactId = cursor.getLong(COLUMN_INDEX_RAW_CONTACT_ID); 211 final String accountType = cursor.getString(COLUMN_INDEX_ACCOUNT_TYPE); 212 final String dataSet = cursor.getString(COLUMN_INDEX_DATA_SET); 213 final long contactId = cursor.getLong(COLUMN_INDEX_CONTACT_ID); 214 final String displayName = cursor.getString(COLUMN_INDEX_DISPLAY_NAME); 215 final String displayNameAlt = cursor.getString(COLUMN_INDEX_DISPLAY_NAME_ALT); 216 217 final String name = ContactDisplayUtils.getPreferredDisplayName(displayName, 218 displayNameAlt, contactsPreferences); 219 if (!TextUtils.isEmpty(name)) { 220 names.add(name); 221 } 222 223 contactIds.add(contactId); 224 final AccountType type = accountTypes.getAccountType(accountType, dataSet); 225 boolean writable = type == null || type.areContactsWritable(); 226 if (writable) { 227 writableRawContacts.add(rawContactId); 228 } else { 229 readOnlyRawContacts.add(rawContactId); 230 } 231 } 232 233 final int readOnlyCount = readOnlyRawContacts.size(); 234 final int writableCount = writableRawContacts.size(); 235 236 final int messageId; 237 int positiveButtonId = android.R.string.ok; 238 if (readOnlyCount > 0 && writableCount > 0) { 239 messageId = R.string.batch_delete_multiple_accounts_confirmation; 240 } else if (readOnlyCount > 0 && writableCount == 0) { 241 messageId = R.string.batch_delete_read_only_contact_confirmation; 242 positiveButtonId = R.string.readOnlyContactWarning_positive_button; 243 } else if (writableCount == 1) { 244 messageId = R.string.single_delete_confirmation; 245 positiveButtonId = R.string.deleteConfirmation_positive_button; 246 } else { 247 messageId = R.string.batch_delete_confirmation; 248 positiveButtonId = R.string.deleteConfirmation_positive_button; 249 } 250 251 // Convert set of contact ids into a format that is easily parcellable and iterated upon 252 // for the sake of ContactSaveService. 253 final Long[] contactIdObjectArray = contactIds.toArray(new Long[contactIds.size()]); 254 final long[] contactIdArray = new long[contactIds.size()]; 255 for (int i = 0; i < contactIds.size(); i++) { 256 contactIdArray[i] = contactIdObjectArray[i]; 257 } 258 259 final String[] namesArray = names.toArray(new String[names.size()]); 260 showDialog(messageId, positiveButtonId, contactIdArray, namesArray); 261 262 // We don't want onLoadFinished() calls any more, which may come when the database is 263 // updating. 264 getLoaderManager().destroyLoader(R.id.dialog_delete_multiple_contact_loader_id); 265 } 266 267 @Override 268 public void onLoaderReset(Loader<Cursor> loader) { 269 } 270 271 private void showDialog(int messageId, int positiveButtonId, final long[] contactIds, 272 final String[] namesArray) { 273 mDialog = new AlertDialog.Builder(getActivity()) 274 .setIconAttribute(android.R.attr.alertDialogIcon) 275 .setMessage(messageId) 276 .setNegativeButton(android.R.string.cancel, null) 277 .setPositiveButton(positiveButtonId, 278 new DialogInterface.OnClickListener() { 279 @Override 280 public void onClick(DialogInterface dialog, int whichButton) { 281 doDeleteContact(contactIds, namesArray); 282 } 283 } 284 ) 285 .create(); 286 287 mDialog.setOnDismissListener(new OnDismissListener() { 288 @Override 289 public void onDismiss(DialogInterface dialog) { 290 mIsLoaderActive = false; 291 mDialog = null; 292 } 293 }); 294 mDialog.show(); 295 } 296 297 @Override 298 public void onSaveInstanceState(Bundle outState) { 299 super.onSaveInstanceState(outState); 300 outState.putBoolean(KEY_ACTIVE, mIsLoaderActive); 301 outState.putSerializable(KEY_CONTACTS_IDS, mContactIds); 302 } 303 304 @Override 305 public void onActivityCreated(Bundle savedInstanceState) { 306 super.onActivityCreated(savedInstanceState); 307 if (savedInstanceState != null) { 308 mIsLoaderActive = savedInstanceState.getBoolean(KEY_ACTIVE); 309 mContactIds = (TreeSet<Long>) savedInstanceState.getSerializable(KEY_CONTACTS_IDS); 310 } 311 } 312 313 protected void doDeleteContact(long[] contactIds, final String[] names) { 314 mContext.startService(ContactSaveService.createDeleteMultipleContactsIntent(mContext, 315 contactIds, names)); 316 mListener.onDeletionFinished(); 317 } 318 319 public void setListener(MultiContactDeleteListener listener) { 320 mListener = listener; 321 } 322 } 323