blob: 33da756ae38029e66d7b731b0edb24a8b0e83986 [file] [log] [blame]
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001/*
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
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080017package com.android.contacts;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070018
Jay Shrauner615ed9c2015-07-29 11:27:56 -070019import static android.Manifest.permission.WRITE_CONTACTS;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080020import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070021import android.app.IntentService;
22import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080023import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070024import android.content.ContentProviderResult;
25import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080026import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070027import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080028import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070029import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080030import android.content.OperationApplicationException;
31import android.database.Cursor;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070032import android.database.DatabaseUtils;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070033import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080034import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080035import android.os.Handler;
36import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080037import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080038import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070039import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080040import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080041import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080042import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080043import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070044import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080045import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070046import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070047import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070048import android.provider.ContactsContract.RawContactsEntity;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070049import android.support.v4.content.LocalBroadcastManager;
Gary Mai7efa9942016-05-12 11:26:49 -070050import android.support.v4.os.ResultReceiver;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070051import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080052import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070053
Wenyi Wangdd7d4562015-12-08 13:33:43 -080054import com.android.contacts.activities.ContactEditorBaseActivity;
Wenyi Wang67addcc2015-11-23 10:07:48 -080055import com.android.contacts.common.compat.CompatUtils;
Chiao Chengd7ca03e2012-10-24 15:14:08 -070056import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080057import com.android.contacts.common.model.AccountTypeManager;
Wenyi Wang67addcc2015-11-23 10:07:48 -080058import com.android.contacts.common.model.CPOWrapper;
Yorke Leecd321f62013-10-28 15:20:15 -070059import com.android.contacts.common.model.RawContactDelta;
60import com.android.contacts.common.model.RawContactDeltaList;
61import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080062import com.android.contacts.common.model.account.AccountWithDataSet;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070063import com.android.contacts.common.testing.NeededForTesting;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070064import com.android.contacts.common.util.PermissionsUtil;
Wenyi Wangaac0e662015-12-18 17:17:33 -080065import com.android.contacts.compat.PinnedPositionsCompat;
Yorke Lee637a38e2013-09-14 08:36:33 -070066import com.android.contacts.util.ContactPhotoUtils;
67
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070068import com.google.common.collect.Lists;
69import com.google.common.collect.Sets;
70
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080071import java.util.ArrayList;
72import java.util.HashSet;
73import java.util.List;
74import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070075
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080076/**
77 * A service responsible for saving changes to the content provider.
78 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070079public class ContactSaveService extends IntentService {
80 private static final String TAG = "ContactSaveService";
81
Katherine Kuana007e442011-07-07 09:25:34 -070082 /** Set to true in order to view logs on content provider operations */
83 private static final boolean DEBUG = false;
84
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070085 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
86
87 public static final String EXTRA_ACCOUNT_NAME = "accountName";
88 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070089 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070090 public static final String EXTRA_CONTENT_VALUES = "contentValues";
91 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
Gary Mai7efa9942016-05-12 11:26:49 -070092 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
93 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070094
Dmitri Plotnikova0114142011-02-15 13:53:21 -080095 public static final String ACTION_SAVE_CONTACT = "saveContact";
96 public static final String EXTRA_CONTACT_STATE = "state";
97 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070098 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070099 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -0800100 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700101
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800102 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800103 public static final String ACTION_RENAME_GROUP = "renameGroup";
104 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700105 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800106 public static final String EXTRA_GROUP_ID = "groupId";
107 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700108 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
109 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800110
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800111 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800112 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800113 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800114 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800115 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800116 public static final String EXTRA_STARRED_FLAG = "starred";
Marcus Hagerott3bb85142016-07-29 10:46:36 -0700117 public static final String EXTRA_DISPLAY_NAME = "extraDisplayName";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800118
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800119 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
120 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
121 public static final String EXTRA_DATA_ID = "dataId";
122
Gary Mai7efa9942016-05-12 11:26:49 -0700123 public static final String ACTION_SPLIT_CONTACT = "splitContact";
124
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800125 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800126 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800127 public static final String EXTRA_CONTACT_ID1 = "contactId1";
128 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800129
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700130 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
131 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
132
133 public static final String ACTION_SET_RINGTONE = "setRingtone";
134 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
135
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700136 public static final String ACTION_UNDO = "undo";
137 public static final String EXTRA_UNDO_ACTION = "undoAction";
138 public static final String EXTRA_UNDO_DATA = "undoData";
139
140 public static final String BROADCAST_ACTION_GROUP_DELETED = "groupDeleted";
141
Gary Mai7efa9942016-05-12 11:26:49 -0700142 public static final int CP2_ERROR = 0;
143 public static final int CONTACTS_LINKED = 1;
144 public static final int CONTACTS_SPLIT = 2;
Gary Mai31d572e2016-06-03 14:04:32 -0700145 public static final int BAD_ARGUMENTS = 3;
Gary Mai7efa9942016-05-12 11:26:49 -0700146
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700147 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
148 Data.MIMETYPE,
149 Data.IS_PRIMARY,
150 Data.DATA1,
151 Data.DATA2,
152 Data.DATA3,
153 Data.DATA4,
154 Data.DATA5,
155 Data.DATA6,
156 Data.DATA7,
157 Data.DATA8,
158 Data.DATA9,
159 Data.DATA10,
160 Data.DATA11,
161 Data.DATA12,
162 Data.DATA13,
163 Data.DATA14,
164 Data.DATA15
165 );
166
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800167 private static final int PERSIST_TRIES = 3;
168
Walter Jang0653de32015-07-24 12:12:40 -0700169 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
170
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800171 public interface Listener {
172 public void onServiceCompleted(Intent callbackIntent);
173 }
174
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100175 private static final CopyOnWriteArrayList<Listener> sListeners =
176 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800177
178 private Handler mMainHandler;
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700179 private GroupsDao mGroupsDao;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800180
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700181 public ContactSaveService() {
182 super(TAG);
183 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800184 mMainHandler = new Handler(Looper.getMainLooper());
185 }
186
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700187 @Override
188 public void onCreate() {
189 super.onCreate();
190 mGroupsDao = new GroupsDaoImpl(this);
191 }
192
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800193 public static void registerListener(Listener listener) {
194 if (!(listener instanceof Activity)) {
195 throw new ClassCastException("Only activities can be registered to"
196 + " receive callback from " + ContactSaveService.class.getName());
197 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100198 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800199 }
200
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700201 public static boolean canUndo(Intent resultIntent) {
202 return resultIntent.hasExtra(EXTRA_UNDO_DATA);
203 }
204
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800205 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100206 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700207 }
208
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800209 /**
210 * Returns true if the ContactSaveService was started successfully and false if an exception
211 * was thrown and a Toast error message was displayed.
212 */
213 public static boolean startService(Context context, Intent intent, int saveMode) {
214 try {
215 context.startService(intent);
216 } catch (Exception exception) {
217 final int resId;
218 switch (saveMode) {
219 case ContactEditorBaseActivity.ContactEditor.SaveMode.SPLIT:
220 resId = R.string.contactUnlinkErrorToast;
221 break;
222 case ContactEditorBaseActivity.ContactEditor.SaveMode.RELOAD:
223 resId = R.string.contactJoinErrorToast;
224 break;
225 case ContactEditorBaseActivity.ContactEditor.SaveMode.CLOSE:
226 resId = R.string.contactSavedErrorToast;
227 break;
228 default:
229 resId = R.string.contactGenericErrorToast;
230 }
231 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
232 return false;
233 }
234 return true;
235 }
236
237 /**
238 * Utility method that starts service and handles exception.
239 */
240 public static void startService(Context context, Intent intent) {
241 try {
242 context.startService(intent);
243 } catch (Exception exception) {
244 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
245 }
246 }
247
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700248 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800249 public Object getSystemService(String name) {
250 Object service = super.getSystemService(name);
251 if (service != null) {
252 return service;
253 }
254
255 return getApplicationContext().getSystemService(name);
256 }
257
258 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700259 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800260 if (intent == null) {
261 Log.d(TAG, "onHandleIntent: could not handle null intent");
262 return;
263 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700264 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
265 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
266 // TODO: add more specific error string such as "Turn on Contacts
267 // permission to update your contacts"
268 showToast(R.string.contactSavedErrorToast);
269 return;
270 }
271
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700272 // Call an appropriate method. If we're sure it affects how incoming phone calls are
273 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700274 String action = intent.getAction();
275 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
276 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800277 } else if (ACTION_SAVE_CONTACT.equals(action)) {
278 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800279 } else if (ACTION_CREATE_GROUP.equals(action)) {
280 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800281 } else if (ACTION_RENAME_GROUP.equals(action)) {
282 renameGroup(intent);
283 } else if (ACTION_DELETE_GROUP.equals(action)) {
284 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700285 } else if (ACTION_UPDATE_GROUP.equals(action)) {
286 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800287 } else if (ACTION_SET_STARRED.equals(action)) {
288 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800289 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
290 setSuperPrimary(intent);
291 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
292 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800293 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
294 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800295 } else if (ACTION_DELETE_CONTACT.equals(action)) {
296 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700297 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
298 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800299 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
300 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800301 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
302 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700303 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
304 setSendToVoicemail(intent);
305 } else if (ACTION_SET_RINGTONE.equals(action)) {
306 setRingtone(intent);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700307 } else if (ACTION_UNDO.equals(action)) {
308 undo(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700309 }
310 }
311
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800312 /**
313 * Creates an intent that can be sent to this service to create a new raw contact
314 * using data presented as a set of ContentValues.
315 */
316 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700317 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700318 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800319 Intent serviceIntent = new Intent(
320 context, ContactSaveService.class);
321 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
322 if (account != null) {
323 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
324 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700325 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800326 }
327 serviceIntent.putParcelableArrayListExtra(
328 ContactSaveService.EXTRA_CONTENT_VALUES, values);
329
330 // Callback intent will be invoked by the service once the new contact is
331 // created. The service will put the URI of the new contact as "data" on
332 // the callback intent.
333 Intent callbackIntent = new Intent(context, callbackActivity);
334 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800335 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
336 return serviceIntent;
337 }
338
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700339 private void createRawContact(Intent intent) {
340 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
341 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700342 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700343 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
344 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
345
346 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
347 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
348 .withValue(RawContacts.ACCOUNT_NAME, accountName)
349 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700350 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700351 .build());
352
353 int size = valueList.size();
354 for (int i = 0; i < size; i++) {
355 ContentValues values = valueList.get(i);
356 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
357 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
358 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
359 .withValues(values)
360 .build());
361 }
362
363 ContentResolver resolver = getContentResolver();
364 ContentProviderResult[] results;
365 try {
366 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
367 } catch (Exception e) {
368 throw new RuntimeException("Failed to store new contact", e);
369 }
370
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700371 Uri rawContactUri = results[0].uri;
372 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
373
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800374 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700375 }
376
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700377 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800378 * Creates an intent that can be sent to this service to create a new raw contact
379 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800380 * This variant is more convenient to use when there is only one photo that can
381 * possibly be updated, as in the Contact Details screen.
382 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
383 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800384 */
Maurice Chu851222a2012-06-21 11:43:08 -0700385 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700386 String saveModeExtraKey, int saveMode, boolean isProfile,
387 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700388 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800389 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700390 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800391 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700392 callbackActivity, callbackAction, bundle,
393 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800394 }
395
396 /**
397 * Creates an intent that can be sent to this service to create a new raw contact
398 * using data presented as a set of ContentValues.
399 * This variant is used when multiple contacts' photos may be updated, as in the
400 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700401 *
Josh Garguse692e012012-01-18 14:53:11 -0800402 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700403 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
404 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800405 */
Maurice Chu851222a2012-06-21 11:43:08 -0700406 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700407 String saveModeExtraKey, int saveMode, boolean isProfile,
408 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700409 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000410 Intent serviceIntent = new Intent(
411 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800412 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
413 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700414 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800415 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
416
Josh Garguse692e012012-01-18 14:53:11 -0800417 if (updatedPhotos != null) {
418 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
419 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800420
Josh Garguse5d3f892012-04-11 11:56:15 -0700421 if (callbackActivity != null) {
422 // Callback intent will be invoked by the service once the contact is
423 // saved. The service will put the URI of the new contact as "data" on
424 // the callback intent.
425 Intent callbackIntent = new Intent(context, callbackActivity);
426 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700427 if (joinContactIdExtraKey != null && joinContactId != null) {
428 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
429 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700430 callbackIntent.setAction(callbackAction);
431 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
432 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800433 return serviceIntent;
434 }
435
436 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700437 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700438 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800439 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800440
Jay Shrauner08099782015-03-25 14:17:11 -0700441 if (state == null) {
442 Log.e(TAG, "Invalid arguments for saveContact request");
443 return;
444 }
445
benny.lin3a4e7a22014-01-08 10:58:08 +0800446 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800447 // Trim any empty fields, and RawContacts, before persisting
448 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700449 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800450
451 Uri lookupUri = null;
452
453 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700454
Josh Garguse692e012012-01-18 14:53:11 -0800455 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800456
Josh Gargusef15c8e2012-01-30 16:42:02 -0800457 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
458 long insertedRawContactId = -1;
459
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800460 // Attempt to persist changes
461 int tries = 0;
462 while (tries++ < PERSIST_TRIES) {
463 try {
464 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800465 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
466
467 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
468
469 for (CPOWrapper cpoWrapper : diffWrapper) {
470 diff.add(cpoWrapper.getOperation());
471 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700472
Katherine Kuana007e442011-07-07 09:25:34 -0700473 if (DEBUG) {
474 Log.v(TAG, "Content Provider Operations:");
475 for (ContentProviderOperation operation : diff) {
476 Log.v(TAG, operation.toString());
477 }
478 }
479
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700480 int numberProcessed = 0;
481 boolean batchFailed = false;
482 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
483 while (numberProcessed < diff.size()) {
484 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
485 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700486 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700487 batchFailed = true;
488 break;
489 } else {
490 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700491 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800492 }
493
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700494 if (batchFailed) {
495 // Retry save
496 continue;
497 }
498
Wenyi Wang67addcc2015-11-23 10:07:48 -0800499 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800500 if (rawContactId == -1) {
501 throw new IllegalStateException("Could not determine RawContact ID after save");
502 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800503 // We don't have to check to see if the value is still -1. If we reach here,
504 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800505 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700506 if (isProfile) {
507 // Since the profile supports local raw contacts, which may have been completely
508 // removed if all information was removed, we need to do a special query to
509 // get the lookup URI for the profile contact (if it still exists).
510 Cursor c = resolver.query(Profile.CONTENT_URI,
511 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
512 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800513 if (c == null) {
514 continue;
515 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700516 try {
Erik162b7e32011-09-20 15:23:55 -0700517 if (c.moveToFirst()) {
518 final long contactId = c.getLong(0);
519 final String lookupKey = c.getString(1);
520 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
521 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700522 } finally {
523 c.close();
524 }
525 } else {
526 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
527 rawContactId);
528 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
529 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800530 if (lookupUri != null) {
531 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
532 }
Josh Garguse692e012012-01-18 14:53:11 -0800533
534 // We can change this back to false later, if we fail to save the contact photo.
535 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800536 break;
537
538 } catch (RemoteException e) {
539 // Something went wrong, bail without success
540 Log.e(TAG, "Problem persisting user edits", e);
541 break;
542
Jay Shrauner57fca182014-01-17 14:20:50 -0800543 } catch (IllegalArgumentException e) {
544 // This is thrown by applyBatch on malformed requests
545 Log.e(TAG, "Problem persisting user edits", e);
546 showToast(R.string.contactSavedErrorToast);
547 break;
548
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800549 } catch (OperationApplicationException e) {
550 // Version consistency failed, re-parent change and try again
551 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
552 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
553 boolean first = true;
554 final int count = state.size();
555 for (int i = 0; i < count; i++) {
556 Long rawContactId = state.getRawContactId(i);
557 if (rawContactId != null && rawContactId != -1) {
558 if (!first) {
559 sb.append(',');
560 }
561 sb.append(rawContactId);
562 first = false;
563 }
564 }
565 sb.append(")");
566
567 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800568 throw new IllegalStateException(
569 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800570 }
571
Maurice Chu851222a2012-06-21 11:43:08 -0700572 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700573 isProfile
574 ? RawContactsEntity.PROFILE_CONTENT_URI
575 : RawContactsEntity.CONTENT_URI,
576 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700577 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700578
579 // Update the new state to use profile URIs if appropriate.
580 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700581 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700582 delta.setProfileQueryUri();
583 }
584 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800585 }
586 }
587
Josh Garguse692e012012-01-18 14:53:11 -0800588 // Now save any updated photos. We do this at the end to ensure that
589 // the ContactProvider already knows about newly-created contacts.
590 if (updatedPhotos != null) {
591 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700592 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800593 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800594
595 // If the raw-contact ID is negative, we are saving a new raw-contact;
596 // replace the bogus ID with the new one that we actually saved the contact at.
597 if (rawContactId < 0) {
598 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800599 }
600
Jay Shrauner511561d2015-04-02 10:35:33 -0700601 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700602 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700603 succeeded = false;
604 }
Josh Garguse692e012012-01-18 14:53:11 -0800605 }
606 }
607
Josh Garguse5d3f892012-04-11 11:56:15 -0700608 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
609 if (callbackIntent != null) {
610 if (succeeded) {
611 // Mark the intent to indicate that the save was successful (even if the lookup URI
612 // is now null). For local contacts or the local profile, it's possible that the
613 // save triggered removal of the contact, so no lookup URI would exist..
614 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
615 }
616 callbackIntent.setData(lookupUri);
617 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800618 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800619 }
620
Josh Garguse692e012012-01-18 14:53:11 -0800621 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700622 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
623 * subsets, adds the returned array to "results".
624 *
625 * @return the size of the array, if not null; -1 when the array is null.
626 */
627 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
628 ContentProviderResult[] results, ContentResolver resolver)
629 throws RemoteException, OperationApplicationException {
630 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
631 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
632 subset.addAll(diff.subList(offset, offset + subsetCount));
633 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
634 .AUTHORITY, subset);
635 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
636 return -1;
637 }
638 for (ContentProviderResult c : subsetResult) {
639 results[offset++] = c;
640 }
641 return subsetResult.length;
642 }
643
644 /**
Josh Garguse692e012012-01-18 14:53:11 -0800645 * Save updated photo for the specified raw-contact.
646 * @return true for success, false for failure
647 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800648 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800649 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800650 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
651 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
652
benny.lin3a4e7a22014-01-08 10:58:08 +0800653 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800654 }
655
Josh Gargusef15c8e2012-01-30 16:42:02 -0800656 /**
657 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
658 */
Maurice Chu851222a2012-06-21 11:43:08 -0700659 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800660 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800661 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800662 long existingRawContactId = state.findRawContactId();
663 if (existingRawContactId != -1) {
664 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800665 }
666
Wenyi Wang67addcc2015-11-23 10:07:48 -0800667 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800668 }
669
670 /**
671 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
672 */
673 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800674 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800675 if (results == null) {
676 return -1;
677 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800678 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800679 final int numResults = results.length;
680 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800681 final CPOWrapper cpoWrapper = diffWrapper.get(i);
682 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
683 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
684 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800685 return ContentUris.parseId(results[i].uri);
686 }
687 }
688 return -1;
689 }
690
691 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700692 * Creates an intent that can be sent to this service to create a new group as
693 * well as add new members at the same time.
694 *
695 * @param context of the application
696 * @param account in which the group should be created
697 * @param label is the name of the group (cannot be null)
698 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
699 * should be added to the group
700 * @param callbackActivity is the activity to send the callback intent to
701 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700702 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700703 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700704 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700705 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800706 Intent serviceIntent = new Intent(context, ContactSaveService.class);
707 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
708 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
709 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700710 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800711 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700712 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700713
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800714 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700715 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800716 Intent callbackIntent = new Intent(context, callbackActivity);
717 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700718 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800719
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700720 return serviceIntent;
721 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800722
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800723 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700724 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
725 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
726 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
727 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700728 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800729
Katherine Kuan717e3432011-07-13 17:03:24 -0700730 // Create the new group
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700731 final Uri groupUri = mGroupsDao.create(label,
732 new AccountWithDataSet(accountName, accountType, dataSet));
733 final ContentResolver resolver = getContentResolver();
Katherine Kuan717e3432011-07-13 17:03:24 -0700734
735 // If there's no URI, then the insertion failed. Abort early because group members can't be
736 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800737 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700738 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800739 return;
740 }
741
Katherine Kuan717e3432011-07-13 17:03:24 -0700742 // Add new group members
743 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
744
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700745 ContentValues values = new ContentValues();
Katherine Kuan717e3432011-07-13 17:03:24 -0700746 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
747 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800748 values.clear();
749 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
750 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
751
752 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700753 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700754 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800755 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800756 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800757 }
758
759 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800760 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800761 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700762 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700763 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800764 Intent serviceIntent = new Intent(context, ContactSaveService.class);
765 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
766 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
767 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700768
769 // Callback intent will be invoked by the service once the group is renamed.
770 Intent callbackIntent = new Intent(context, callbackActivity);
771 callbackIntent.setAction(callbackAction);
772 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
773
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800774 return serviceIntent;
775 }
776
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800777 private void renameGroup(Intent intent) {
778 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
779 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
780
781 if (groupId == -1) {
782 Log.e(TAG, "Invalid arguments for renameGroup request");
783 return;
784 }
785
786 ContentValues values = new ContentValues();
787 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700788 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
789 getContentResolver().update(groupUri, values, null, null);
790
791 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
792 callbackIntent.setData(groupUri);
793 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800794 }
795
796 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800797 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800798 */
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700799 public static Intent createGroupDeletionIntent(Context context, long groupId) {
800 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800801 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800802 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700803
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800804 return serviceIntent;
805 }
806
807 private void deleteGroup(Intent intent) {
808 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
809 if (groupId == -1) {
810 Log.e(TAG, "Invalid arguments for deleteGroup request");
811 return;
812 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700813 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800814
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700815 final Intent callbackIntent = new Intent(BROADCAST_ACTION_GROUP_DELETED);
816 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
817 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
818 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
Walter Jang72f99882016-05-26 09:01:31 -0700819
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700820 mGroupsDao.delete(groupUri);
821
822 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
823 }
824
825 public static Intent createUndoIntent(Context context, Intent resultIntent) {
826 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
827 serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
828 serviceIntent.putExtras(resultIntent);
829 return serviceIntent;
830 }
831
832 private void undo(Intent intent) {
833 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
834 if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
835 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
Walter Jang72f99882016-05-26 09:01:31 -0700836 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800837 }
838
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700839
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800840 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700841 * Creates an intent that can be sent to this service to rename a group as
842 * well as add and remove members from the group.
843 *
844 * @param context of the application
845 * @param groupId of the group that should be modified
846 * @param newLabel is the updated name of the group (can be null if the name
847 * should not be updated)
848 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
849 * should be added to the group
850 * @param rawContactsToRemove is an array of raw contact IDs for contacts
851 * that should be removed from the group
852 * @param callbackActivity is the activity to send the callback intent to
853 * @param callbackAction is the intent action for the callback intent
854 */
855 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
856 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700857 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700858 Intent serviceIntent = new Intent(context, ContactSaveService.class);
859 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
860 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
861 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
862 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
863 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
864 rawContactsToRemove);
865
866 // Callback intent will be invoked by the service once the group is updated
867 Intent callbackIntent = new Intent(context, callbackActivity);
868 callbackIntent.setAction(callbackAction);
869 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
870
871 return serviceIntent;
872 }
873
874 private void updateGroup(Intent intent) {
875 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
876 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
877 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
878 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
879
880 if (groupId == -1) {
881 Log.e(TAG, "Invalid arguments for updateGroup request");
882 return;
883 }
884
885 final ContentResolver resolver = getContentResolver();
886 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
887
888 // Update group name if necessary
889 if (label != null) {
890 ContentValues values = new ContentValues();
891 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700892 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700893 }
894
Katherine Kuan717e3432011-07-13 17:03:24 -0700895 // Add and remove members if necessary
896 addMembersToGroup(resolver, rawContactsToAdd, groupId);
897 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
898
899 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
900 callbackIntent.setData(groupUri);
901 deliverCallback(callbackIntent);
902 }
903
Daniel Lehmann18958a22012-02-28 17:45:25 -0800904 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700905 long groupId) {
906 if (rawContactsToAdd == null) {
907 return;
908 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700909 for (long rawContactId : rawContactsToAdd) {
910 try {
911 final ArrayList<ContentProviderOperation> rawContactOperations =
912 new ArrayList<ContentProviderOperation>();
913
914 // Build an assert operation to ensure the contact is not already in the group
915 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
916 .newAssertQuery(Data.CONTENT_URI);
917 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
918 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
919 new String[] { String.valueOf(rawContactId),
920 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
921 assertBuilder.withExpectedCount(0);
922 rawContactOperations.add(assertBuilder.build());
923
924 // Build an insert operation to add the contact to the group
925 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
926 .newInsert(Data.CONTENT_URI);
927 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
928 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
929 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
930 rawContactOperations.add(insertBuilder.build());
931
932 if (DEBUG) {
933 for (ContentProviderOperation operation : rawContactOperations) {
934 Log.v(TAG, operation.toString());
935 }
936 }
937
938 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700939 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800940 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700941 }
942 } catch (RemoteException e) {
943 // Something went wrong, bail without success
944 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
945 String.valueOf(rawContactId), e);
946 } catch (OperationApplicationException e) {
947 // The assert could have failed because the contact is already in the group,
948 // just continue to the next contact
949 Log.w(TAG, "Assert failed in adding raw contact ID " +
950 String.valueOf(rawContactId) + ". Already exists in group " +
951 String.valueOf(groupId), e);
952 }
953 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700954 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700955
Daniel Lehmann18958a22012-02-28 17:45:25 -0800956 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700957 long groupId) {
958 if (rawContactsToRemove == null) {
959 return;
960 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700961 for (long rawContactId : rawContactsToRemove) {
962 // Apply the delete operation on the data row for the given raw contact's
963 // membership in the given group. If no contact matches the provided selection, then
964 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800965 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700966 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
967 new String[] { String.valueOf(rawContactId),
968 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
969 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700970 }
971
972 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800973 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800974 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800975 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
976 Intent serviceIntent = new Intent(context, ContactSaveService.class);
977 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
978 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
979 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
980
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800981 return serviceIntent;
982 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800983
984 private void setStarred(Intent intent) {
985 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
986 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
987 if (contactUri == null) {
988 Log.e(TAG, "Invalid arguments for setStarred request");
989 return;
990 }
991
992 final ContentValues values = new ContentValues(1);
993 values.put(Contacts.STARRED, value);
994 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700995
996 // Undemote the contact if necessary
997 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
998 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800999 if (c == null) {
1000 return;
1001 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001002 try {
1003 if (c.moveToFirst()) {
1004 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -07001005
1006 // Don't bother undemoting if this contact is the user's profile.
1007 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -08001008 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -07001009 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001010 }
1011 } finally {
1012 c.close();
1013 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001014 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001015
1016 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -07001017 * Creates an intent that can be sent to this service to set the redirect to voicemail.
1018 */
1019 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
1020 boolean value) {
1021 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1022 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1023 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1024 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1025
1026 return serviceIntent;
1027 }
1028
1029 private void setSendToVoicemail(Intent intent) {
1030 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1031 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1032 if (contactUri == null) {
1033 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1034 return;
1035 }
1036
1037 final ContentValues values = new ContentValues(1);
1038 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1039 getContentResolver().update(contactUri, values, null, null);
1040 }
1041
1042 /**
1043 * Creates an intent that can be sent to this service to save the contact's ringtone.
1044 */
1045 public static Intent createSetRingtone(Context context, Uri contactUri,
1046 String value) {
1047 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1048 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1049 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1050 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1051
1052 return serviceIntent;
1053 }
1054
1055 private void setRingtone(Intent intent) {
1056 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1057 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1058 if (contactUri == null) {
1059 Log.e(TAG, "Invalid arguments for setRingtone");
1060 return;
1061 }
1062 ContentValues values = new ContentValues(1);
1063 values.put(Contacts.CUSTOM_RINGTONE, value);
1064 getContentResolver().update(contactUri, values, null, null);
1065 }
1066
1067 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001068 * Creates an intent that sets the selected data item as super primary (default)
1069 */
1070 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1071 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1072 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1073 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1074 return serviceIntent;
1075 }
1076
1077 private void setSuperPrimary(Intent intent) {
1078 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1079 if (dataId == -1) {
1080 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1081 return;
1082 }
1083
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001084 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001085 }
1086
1087 /**
1088 * Creates an intent that clears the primary flag of all data items that belong to the same
1089 * raw_contact as the given data item. Will only clear, if the data item was primary before
1090 * this call
1091 */
1092 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1093 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1094 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1095 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1096 return serviceIntent;
1097 }
1098
1099 private void clearPrimary(Intent intent) {
1100 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1101 if (dataId == -1) {
1102 Log.e(TAG, "Invalid arguments for clearPrimary request");
1103 return;
1104 }
1105
1106 // Update the primary values in the data record.
1107 ContentValues values = new ContentValues(1);
1108 values.put(Data.IS_SUPER_PRIMARY, 0);
1109 values.put(Data.IS_PRIMARY, 0);
1110
1111 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1112 values, null, null);
1113 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001114
1115 /**
1116 * Creates an intent that can be sent to this service to delete a contact.
1117 */
1118 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1119 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1120 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1121 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1122 return serviceIntent;
1123 }
1124
Brian Attwelld2962a32015-03-02 14:48:50 -08001125 /**
1126 * Creates an intent that can be sent to this service to delete multiple contacts.
1127 */
1128 public static Intent createDeleteMultipleContactsIntent(Context context,
1129 long[] contactIds) {
1130 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1131 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1132 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1133 return serviceIntent;
1134 }
1135
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001136 private void deleteContact(Intent intent) {
1137 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1138 if (contactUri == null) {
1139 Log.e(TAG, "Invalid arguments for deleteContact request");
1140 return;
1141 }
1142
1143 getContentResolver().delete(contactUri, null, null);
1144 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001145
Brian Attwelld2962a32015-03-02 14:48:50 -08001146 private void deleteMultipleContacts(Intent intent) {
1147 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1148 if (contactIds == null) {
1149 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1150 return;
1151 }
1152 for (long contactId : contactIds) {
1153 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1154 getContentResolver().delete(contactUri, null, null);
1155 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001156 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1157 .contacts_deleted_toast, contactIds.length);
1158 mMainHandler.post(new Runnable() {
1159 @Override
1160 public void run() {
1161 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1162 .show();
1163 }
1164 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001165 }
1166
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001167 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001168 * Creates an intent that can be sent to this service to split a contact into it's constituent
Gary Mai53fe0d22016-07-26 17:23:53 -07001169 * pieces. This will set the raw contact ids to TYPE_AUTOMATIC for AggregationExceptions so
1170 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001171 */
1172 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1173 ResultReceiver receiver) {
1174 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1175 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1176 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1177 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1178 return serviceIntent;
1179 }
1180
1181 private void splitContact(Intent intent) {
1182 final long rawContactIds[][] = (long[][]) intent
1183 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001184 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Mai7efa9942016-05-12 11:26:49 -07001185 if (rawContactIds == null) {
1186 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001187 if (receiver != null) {
1188 receiver.send(BAD_ARGUMENTS, new Bundle());
1189 }
Gary Mai7efa9942016-05-12 11:26:49 -07001190 return;
1191 }
1192 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1193 final ContentResolver resolver = getContentResolver();
1194 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001195 for (int i = 0; i < rawContactIds.length; i++) {
1196 for (int j = 0; j < rawContactIds.length; j++) {
1197 if (i != j) {
1198 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
1199 if (receiver != null) {
1200 receiver.send(CP2_ERROR, new Bundle());
1201 return;
1202 }
1203 }
1204 }
1205 }
1206 }
1207 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1208 if (receiver != null) {
1209 receiver.send(CP2_ERROR, new Bundle());
1210 }
1211 return;
1212 }
1213 if (receiver != null) {
1214 receiver.send(CONTACTS_SPLIT, new Bundle());
1215 } else {
1216 showToast(R.string.contactUnlinkedToast);
1217 }
1218 }
1219
1220 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001221 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001222 * and {@param rawContactIds2} to {@param operations}.
1223 * @return false if an error occurred, true otherwise.
1224 */
1225 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
1226 long[] rawContactIds1, long[] rawContactIds2) {
1227 if (rawContactIds1 == null || rawContactIds2 == null) {
1228 Log.e(TAG, "Invalid arguments for splitContact request");
1229 return false;
1230 }
1231 // For each pair of raw contacts, insert an aggregation exception
1232 final ContentResolver resolver = getContentResolver();
1233 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1234 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1235 for (int i = 0; i < rawContactIds1.length; i++) {
1236 for (int j = 0; j < rawContactIds2.length; j++) {
1237 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
1238 // Before we get to 500 we need to flush the operations list
1239 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1240 if (!applyOperations(resolver, operations)) {
1241 return false;
1242 }
1243 operations.clear();
1244 }
1245 }
1246 }
1247 return true;
1248 }
1249
1250 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001251 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001252 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001253 */
1254 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001255 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001256 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1257 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1258 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1259 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001260
1261 // Callback intent will be invoked by the service once the contacts are joined.
1262 Intent callbackIntent = new Intent(context, callbackActivity);
1263 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001264 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1265
1266 return serviceIntent;
1267 }
1268
Brian Attwelld3946ca2015-03-03 11:13:49 -08001269 /**
1270 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1271 * No special attention is paid to where the resulting contact's name is taken from.
1272 */
Gary Mai7efa9942016-05-12 11:26:49 -07001273 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1274 ResultReceiver receiver) {
1275 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001276 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1277 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001278 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001279 return serviceIntent;
1280 }
1281
Gary Mai7efa9942016-05-12 11:26:49 -07001282 /**
1283 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1284 * No special attention is paid to where the resulting contact's name is taken from.
1285 */
1286 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1287 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1288 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001289
1290 private interface JoinContactQuery {
1291 String[] PROJECTION = {
1292 RawContacts._ID,
1293 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001294 RawContacts.DISPLAY_NAME_SOURCE,
1295 };
1296
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001297 int _ID = 0;
1298 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001299 int DISPLAY_NAME_SOURCE = 2;
1300 }
1301
1302 private interface ContactEntityQuery {
1303 String[] PROJECTION = {
1304 Contacts.Entity.DATA_ID,
1305 Contacts.Entity.CONTACT_ID,
1306 Contacts.Entity.IS_SUPER_PRIMARY,
1307 };
1308 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1309 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1310 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1311 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1312
1313 int DATA_ID = 0;
1314 int CONTACT_ID = 1;
1315 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001316 }
1317
Brian Attwelld3946ca2015-03-03 11:13:49 -08001318 private void joinSeveralContacts(Intent intent) {
1319 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001320
Gary Mai7efa9942016-05-12 11:26:49 -07001321 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001322
Brian Attwelld3946ca2015-03-03 11:13:49 -08001323 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001324 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1325 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001326 if (rawContactIds == null) {
1327 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001328 if (receiver != null) {
1329 receiver.send(BAD_ARGUMENTS, new Bundle());
1330 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001331 return;
1332 }
1333
Brian Attwelld3946ca2015-03-03 11:13:49 -08001334 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001335 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001336 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1337 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1338 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001339 for (int i = 0; i < rawContactIds.length; i++) {
1340 for (int j = 0; j < rawContactIds.length; j++) {
1341 if (i != j) {
1342 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1343 }
Walter Jang0653de32015-07-24 12:12:40 -07001344 // Before we get to 500 we need to flush the operations list
1345 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001346 if (!applyOperations(resolver, operations)) {
1347 if (receiver != null) {
1348 receiver.send(CP2_ERROR, new Bundle());
1349 }
Walter Jang0653de32015-07-24 12:12:40 -07001350 return;
1351 }
1352 operations.clear();
1353 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001354 }
1355 }
Gary Mai7efa9942016-05-12 11:26:49 -07001356 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1357 if (receiver != null) {
1358 receiver.send(CP2_ERROR, new Bundle());
1359 }
Walter Jang0653de32015-07-24 12:12:40 -07001360 return;
1361 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001362
Gary Mai7efa9942016-05-12 11:26:49 -07001363 if (receiver != null) {
1364 final Bundle result = new Bundle();
1365 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001366 result.putString(EXTRA_DISPLAY_NAME, queryNameOfLinkedContacts(contactIds));
Gary Mai7efa9942016-05-12 11:26:49 -07001367 receiver.send(CONTACTS_LINKED, result);
1368 } else {
1369 showToast(R.string.contactsJoinedMessage);
1370 }
Walter Jang0653de32015-07-24 12:12:40 -07001371 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001372
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001373 // Get the display name of the top-level contact after the contacts have been linked.
1374 private String queryNameOfLinkedContacts(long[] contactIds) {
1375 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1376 final String[] whereArgs = new String[contactIds.length];
1377 for (int i = 0; i < contactIds.length; i++) {
1378 whereArgs[i] = String.valueOf(contactIds[i]);
1379 whereBuilder.append("?,");
1380 }
1381 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1382 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
1383 new String[]{Contacts.DISPLAY_NAME}, whereBuilder.toString(), whereArgs, null);
1384 try {
1385 if (cursor.moveToFirst()) {
1386 return cursor.getString(0);
1387 }
1388 return null;
1389 } finally {
1390 cursor.close();
1391 }
1392 }
1393
1394
Walter Jang0653de32015-07-24 12:12:40 -07001395 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001396 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001397 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001398 try {
1399 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001400 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001401 } catch (RemoteException | OperationApplicationException e) {
1402 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1403 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001404 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001405 }
1406 }
1407
Brian Attwelld3946ca2015-03-03 11:13:49 -08001408 private void joinContacts(Intent intent) {
1409 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1410 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001411
1412 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001413 // in the join UIs.
1414 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1415 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001416 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001417 return;
1418 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001419
Brian Attwell548f5c62015-01-27 17:46:46 -08001420 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001421
1422 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001423 for (int i = 0; i < rawContactIds.length; i++) {
1424 for (int j = 0; j < rawContactIds.length; j++) {
1425 if (i != j) {
1426 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1427 }
1428 }
1429 }
1430
Brian Attwelld3946ca2015-03-03 11:13:49 -08001431 final ContentResolver resolver = getContentResolver();
1432
Brian Attwell548f5c62015-01-27 17:46:46 -08001433 // Use the name for contactId1 as the name for the newly aggregated contact.
1434 final Uri contactId1Uri = ContentUris.withAppendedId(
1435 Contacts.CONTENT_URI, contactId1);
1436 final Uri entityUri = Uri.withAppendedPath(
1437 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1438 Cursor c = resolver.query(entityUri,
1439 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1440 if (c == null) {
1441 Log.e(TAG, "Unable to open Contacts DB cursor");
1442 showToast(R.string.contactSavedErrorToast);
1443 return;
1444 }
1445 long dataIdToAddSuperPrimary = -1;
1446 try {
1447 if (c.moveToFirst()) {
1448 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1449 }
1450 } finally {
1451 c.close();
1452 }
1453
1454 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1455 // display name does not change as a result of the join.
1456 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001457 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001458 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1459 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1460 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001461 operations.add(builder.build());
1462 }
1463
1464 boolean success = false;
1465 // Apply all aggregation exceptions as one batch
1466 try {
1467 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001468 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001469 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001470 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001471 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001472 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001473 }
1474
1475 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1476 if (success) {
1477 Uri uri = RawContacts.getContactLookupUri(resolver,
1478 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1479 callbackIntent.setData(uri);
1480 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001481 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001482 }
1483
Gary Mai7efa9942016-05-12 11:26:49 -07001484 /**
1485 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1486 * array of the return value holds an array of raw contact ids for one contactId.
1487 * @param contactIds
1488 * @return
1489 */
1490 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1491 final long[][] rawContactIds = new long[contactIds.length][];
1492 for (int i = 0; i < contactIds.length; i++) {
1493 rawContactIds[i] = getRawContactIds(contactIds[i]);
1494 }
1495 return rawContactIds;
1496 }
1497
1498 /**
1499 * Gets the raw contact ids associated with {@param contactId}.
1500 * @param contactId
1501 * @return Array of raw contact ids.
1502 */
1503 private long[] getRawContactIds(long contactId) {
1504 final ContentResolver resolver = getContentResolver();
1505 long rawContactIds[];
1506
1507 final StringBuilder queryBuilder = new StringBuilder();
1508 queryBuilder.append(RawContacts.CONTACT_ID)
1509 .append("=")
1510 .append(String.valueOf(contactId));
1511
1512 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1513 JoinContactQuery.PROJECTION,
1514 queryBuilder.toString(),
1515 null, null);
1516 if (c == null) {
1517 Log.e(TAG, "Unable to open Contacts DB cursor");
1518 return null;
1519 }
1520 try {
1521 rawContactIds = new long[c.getCount()];
1522 for (int i = 0; i < rawContactIds.length; i++) {
1523 c.moveToPosition(i);
1524 final long rawContactId = c.getLong(JoinContactQuery._ID);
1525 rawContactIds[i] = rawContactId;
1526 }
1527 } finally {
1528 c.close();
1529 }
1530 return rawContactIds;
1531 }
1532
Brian Attwelld3946ca2015-03-03 11:13:49 -08001533 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1534 if (contactIds == null) {
1535 return null;
1536 }
1537
Brian Attwell548f5c62015-01-27 17:46:46 -08001538 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001539
1540 final StringBuilder queryBuilder = new StringBuilder();
1541 final String stringContactIds[] = new String[contactIds.length];
1542 for (int i = 0; i < contactIds.length; i++) {
1543 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1544 stringContactIds[i] = String.valueOf(contactIds[i]);
1545 if (contactIds[i] == -1) {
1546 return null;
1547 }
1548 if (i == contactIds.length -1) {
1549 break;
1550 }
1551 queryBuilder.append(" OR ");
1552 }
1553
Brian Attwell548f5c62015-01-27 17:46:46 -08001554 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1555 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001556 queryBuilder.toString(),
1557 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001558 if (c == null) {
1559 Log.e(TAG, "Unable to open Contacts DB cursor");
1560 showToast(R.string.contactSavedErrorToast);
1561 return null;
1562 }
Gary Mai7efa9942016-05-12 11:26:49 -07001563 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001564 try {
1565 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001566 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001567 return null;
1568 }
1569 rawContactIds = new long[c.getCount()];
1570 for (int i = 0; i < rawContactIds.length; i++) {
1571 c.moveToPosition(i);
1572 long rawContactId = c.getLong(JoinContactQuery._ID);
1573 rawContactIds[i] = rawContactId;
1574 }
1575 } finally {
1576 c.close();
1577 }
1578 return rawContactIds;
1579 }
1580
Brian Attwelld3946ca2015-03-03 11:13:49 -08001581 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1582 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1583 }
1584
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001585 /**
1586 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1587 */
1588 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1589 long rawContactId1, long rawContactId2) {
1590 Builder builder =
1591 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1592 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1593 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1594 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1595 operations.add(builder.build());
1596 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001597
1598 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001599 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} ContentProviderOperation.
Gary Mai7efa9942016-05-12 11:26:49 -07001600 */
1601 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
1602 long rawContactId1, long rawContactId2) {
1603 final Builder builder =
1604 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Mai53fe0d22016-07-26 17:23:53 -07001605 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001606 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1607 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1608 operations.add(builder.build());
1609 }
1610
1611 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001612 * Shows a toast on the UI thread.
1613 */
1614 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001615 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001616
1617 @Override
1618 public void run() {
1619 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1620 }
1621 });
1622 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001623
1624 private void deliverCallback(final Intent callbackIntent) {
1625 mMainHandler.post(new Runnable() {
1626
1627 @Override
1628 public void run() {
1629 deliverCallbackOnUiThread(callbackIntent);
1630 }
1631 });
1632 }
1633
1634 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1635 // TODO: this assumes that if there are multiple instances of the same
1636 // activity registered, the last one registered is the one waiting for
1637 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001638 for (Listener listener : sListeners) {
1639 if (callbackIntent.getComponent().equals(
1640 ((Activity) listener).getIntent().getComponent())) {
1641 listener.onServiceCompleted(callbackIntent);
1642 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001643 }
1644 }
1645 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001646
1647 public interface GroupsDao {
1648 Uri create(String title, AccountWithDataSet account);
1649 int delete(Uri groupUri);
1650 Bundle captureDeletionUndoData(Uri groupUri);
1651 Uri undoDeletion(Bundle undoData);
1652 }
1653
1654 @NeededForTesting
1655 public static class GroupsDaoImpl implements GroupsDao {
1656 @NeededForTesting
1657 public static final String KEY_GROUP_DATA = "groupData";
1658 @NeededForTesting
1659 public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
1660
1661 private static final String TAG = "GroupsDao";
1662 private final Context context;
1663 private final ContentResolver contentResolver;
1664
1665 public GroupsDaoImpl(Context context) {
1666 this(context, context.getContentResolver());
1667 }
1668
1669 public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
1670 this.context = context;
1671 this.contentResolver = contentResolver;
1672 }
1673
1674 public Bundle captureDeletionUndoData(Uri groupUri) {
1675 final long groupId = ContentUris.parseId(groupUri);
1676 final Bundle result = new Bundle();
1677
1678 final Cursor cursor = contentResolver.query(groupUri,
1679 new String[]{
1680 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
1681 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
1682 Groups.SHOULD_SYNC
1683 },
1684 Groups.DELETED + "=?", new String[] { "0" }, null);
1685 try {
1686 if (cursor.moveToFirst()) {
1687 final ContentValues groupValues = new ContentValues();
1688 DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
1689 result.putParcelable(KEY_GROUP_DATA, groupValues);
1690 } else {
1691 // Group doesn't exist.
1692 return result;
1693 }
1694 } finally {
1695 cursor.close();
1696 }
1697
1698 final Cursor membersCursor = contentResolver.query(
1699 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
1700 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1701 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
1702 final long[] memberIds = new long[membersCursor.getCount()];
1703 int i = 0;
1704 while (membersCursor.moveToNext()) {
1705 memberIds[i++] = membersCursor.getLong(0);
1706 }
1707 result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
1708 return result;
1709 }
1710
1711 public Uri undoDeletion(Bundle deletedGroupData) {
1712 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
1713 if (groupData == null) {
1714 return null;
1715 }
1716 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
1717 final long groupId = ContentUris.parseId(groupUri);
1718
1719 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
1720 if (memberIds == null) {
1721 return groupUri;
1722 }
1723 final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
1724 for (int i = 0; i < memberIds.length; i++) {
1725 memberInsertions[i] = new ContentValues();
1726 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
1727 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1728 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
1729 }
1730 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
1731 if (inserted != memberIds.length) {
1732 Log.e(TAG, "Could not recover some members for group deletion undo");
1733 }
1734
1735 return groupUri;
1736 }
1737
1738 public Uri create(String title, AccountWithDataSet account) {
1739 final ContentValues values = new ContentValues();
1740 values.put(Groups.TITLE, title);
1741 values.put(Groups.ACCOUNT_NAME, account.name);
1742 values.put(Groups.ACCOUNT_TYPE, account.type);
1743 values.put(Groups.DATA_SET, account.dataSet);
1744 return contentResolver.insert(Groups.CONTENT_URI, values);
1745 }
1746
1747 public int delete(Uri groupUri) {
1748 return contentResolver.delete(groupUri, null, null);
1749 }
1750 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001751}