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