blob: 54be5a0da2bc34fac021d6b81c3c4477855f6deb [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;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070032import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080033import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080034import android.os.Handler;
35import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080036import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080037import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070038import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080039import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080040import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080041import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080042import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070043import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080044import android.provider.ContactsContract.Groups;
Yorke Leee8e3fb82013-09-12 17:53:31 -070045import android.provider.ContactsContract.PinnedPositions;
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;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070049import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080050import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070051
Wenyi Wang67addcc2015-11-23 10:07:48 -080052import com.android.contacts.common.compat.CompatUtils;
Chiao Chengd7ca03e2012-10-24 15:14:08 -070053import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080054import com.android.contacts.common.model.AccountTypeManager;
Wenyi Wang67addcc2015-11-23 10:07:48 -080055import com.android.contacts.common.model.CPOWrapper;
Yorke Leecd321f62013-10-28 15:20:15 -070056import com.android.contacts.common.model.RawContactDelta;
57import com.android.contacts.common.model.RawContactDeltaList;
58import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080059import com.android.contacts.common.model.account.AccountWithDataSet;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070060import com.android.contacts.common.util.PermissionsUtil;
Yorke Lee637a38e2013-09-14 08:36:33 -070061import com.android.contacts.util.ContactPhotoUtils;
62
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070063import com.google.common.collect.Lists;
64import com.google.common.collect.Sets;
65
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080066import java.util.ArrayList;
67import java.util.HashSet;
68import java.util.List;
69import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070070
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080071/**
72 * A service responsible for saving changes to the content provider.
73 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070074public class ContactSaveService extends IntentService {
75 private static final String TAG = "ContactSaveService";
76
Katherine Kuana007e442011-07-07 09:25:34 -070077 /** Set to true in order to view logs on content provider operations */
78 private static final boolean DEBUG = false;
79
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070080 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
81
82 public static final String EXTRA_ACCOUNT_NAME = "accountName";
83 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070084 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070085 public static final String EXTRA_CONTENT_VALUES = "contentValues";
86 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
87
Dmitri Plotnikova0114142011-02-15 13:53:21 -080088 public static final String ACTION_SAVE_CONTACT = "saveContact";
89 public static final String EXTRA_CONTACT_STATE = "state";
90 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070091 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070092 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080093 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070094
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080095 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080096 public static final String ACTION_RENAME_GROUP = "renameGroup";
97 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070098 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080099 public static final String EXTRA_GROUP_ID = "groupId";
100 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700101 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
102 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800103
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800104 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800105 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800106 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800107 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800108 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800109 public static final String EXTRA_STARRED_FLAG = "starred";
110
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800111 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
112 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
113 public static final String EXTRA_DATA_ID = "dataId";
114
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800115 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800116 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800117 public static final String EXTRA_CONTACT_ID1 = "contactId1";
118 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800119
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700120 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
121 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
122
123 public static final String ACTION_SET_RINGTONE = "setRingtone";
124 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
125
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700126 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
127 Data.MIMETYPE,
128 Data.IS_PRIMARY,
129 Data.DATA1,
130 Data.DATA2,
131 Data.DATA3,
132 Data.DATA4,
133 Data.DATA5,
134 Data.DATA6,
135 Data.DATA7,
136 Data.DATA8,
137 Data.DATA9,
138 Data.DATA10,
139 Data.DATA11,
140 Data.DATA12,
141 Data.DATA13,
142 Data.DATA14,
143 Data.DATA15
144 );
145
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800146 private static final int PERSIST_TRIES = 3;
147
Walter Jang0653de32015-07-24 12:12:40 -0700148 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
149
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800150 public interface Listener {
151 public void onServiceCompleted(Intent callbackIntent);
152 }
153
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100154 private static final CopyOnWriteArrayList<Listener> sListeners =
155 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800156
157 private Handler mMainHandler;
158
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700159 public ContactSaveService() {
160 super(TAG);
161 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800162 mMainHandler = new Handler(Looper.getMainLooper());
163 }
164
165 public static void registerListener(Listener listener) {
166 if (!(listener instanceof Activity)) {
167 throw new ClassCastException("Only activities can be registered to"
168 + " receive callback from " + ContactSaveService.class.getName());
169 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100170 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800171 }
172
173 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100174 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700175 }
176
177 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800178 public Object getSystemService(String name) {
179 Object service = super.getSystemService(name);
180 if (service != null) {
181 return service;
182 }
183
184 return getApplicationContext().getSystemService(name);
185 }
186
187 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700188 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800189 if (intent == null) {
190 Log.d(TAG, "onHandleIntent: could not handle null intent");
191 return;
192 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700193 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
194 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
195 // TODO: add more specific error string such as "Turn on Contacts
196 // permission to update your contacts"
197 showToast(R.string.contactSavedErrorToast);
198 return;
199 }
200
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700201 // Call an appropriate method. If we're sure it affects how incoming phone calls are
202 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700203 String action = intent.getAction();
204 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
205 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800206 } else if (ACTION_SAVE_CONTACT.equals(action)) {
207 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800208 } else if (ACTION_CREATE_GROUP.equals(action)) {
209 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800210 } else if (ACTION_RENAME_GROUP.equals(action)) {
211 renameGroup(intent);
212 } else if (ACTION_DELETE_GROUP.equals(action)) {
213 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700214 } else if (ACTION_UPDATE_GROUP.equals(action)) {
215 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800216 } else if (ACTION_SET_STARRED.equals(action)) {
217 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800218 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
219 setSuperPrimary(intent);
220 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
221 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800222 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
223 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800224 } else if (ACTION_DELETE_CONTACT.equals(action)) {
225 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800226 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
227 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800228 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
229 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700230 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
231 setSendToVoicemail(intent);
232 } else if (ACTION_SET_RINGTONE.equals(action)) {
233 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700234 }
235 }
236
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800237 /**
238 * Creates an intent that can be sent to this service to create a new raw contact
239 * using data presented as a set of ContentValues.
240 */
241 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700242 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700243 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800244 Intent serviceIntent = new Intent(
245 context, ContactSaveService.class);
246 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
247 if (account != null) {
248 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
249 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700250 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800251 }
252 serviceIntent.putParcelableArrayListExtra(
253 ContactSaveService.EXTRA_CONTENT_VALUES, values);
254
255 // Callback intent will be invoked by the service once the new contact is
256 // created. The service will put the URI of the new contact as "data" on
257 // the callback intent.
258 Intent callbackIntent = new Intent(context, callbackActivity);
259 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800260 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
261 return serviceIntent;
262 }
263
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700264 private void createRawContact(Intent intent) {
265 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
266 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700267 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700268 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
269 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
270
271 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
272 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
273 .withValue(RawContacts.ACCOUNT_NAME, accountName)
274 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700275 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700276 .build());
277
278 int size = valueList.size();
279 for (int i = 0; i < size; i++) {
280 ContentValues values = valueList.get(i);
281 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
282 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
283 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
284 .withValues(values)
285 .build());
286 }
287
288 ContentResolver resolver = getContentResolver();
289 ContentProviderResult[] results;
290 try {
291 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
292 } catch (Exception e) {
293 throw new RuntimeException("Failed to store new contact", e);
294 }
295
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700296 Uri rawContactUri = results[0].uri;
297 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
298
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800299 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700300 }
301
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700302 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800303 * Creates an intent that can be sent to this service to create a new raw contact
304 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800305 * This variant is more convenient to use when there is only one photo that can
306 * possibly be updated, as in the Contact Details screen.
307 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
308 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800309 */
Maurice Chu851222a2012-06-21 11:43:08 -0700310 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700311 String saveModeExtraKey, int saveMode, boolean isProfile,
312 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700313 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800314 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700315 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800316 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700317 callbackActivity, callbackAction, bundle,
318 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800319 }
320
321 /**
322 * Creates an intent that can be sent to this service to create a new raw contact
323 * using data presented as a set of ContentValues.
324 * This variant is used when multiple contacts' photos may be updated, as in the
325 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700326 *
Josh Garguse692e012012-01-18 14:53:11 -0800327 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700328 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
329 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800330 */
Maurice Chu851222a2012-06-21 11:43:08 -0700331 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700332 String saveModeExtraKey, int saveMode, boolean isProfile,
333 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700334 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800335 Intent serviceIntent = new Intent(
336 context, ContactSaveService.class);
337 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
338 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700339 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800340 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
341
Josh Garguse692e012012-01-18 14:53:11 -0800342 if (updatedPhotos != null) {
343 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
344 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800345
Josh Garguse5d3f892012-04-11 11:56:15 -0700346 if (callbackActivity != null) {
347 // Callback intent will be invoked by the service once the contact is
348 // saved. The service will put the URI of the new contact as "data" on
349 // the callback intent.
350 Intent callbackIntent = new Intent(context, callbackActivity);
351 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700352 if (joinContactIdExtraKey != null && joinContactId != null) {
353 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
354 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700355 callbackIntent.setAction(callbackAction);
356 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
357 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800358 return serviceIntent;
359 }
360
361 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700362 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700363 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800364 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800365
Jay Shrauner08099782015-03-25 14:17:11 -0700366 if (state == null) {
367 Log.e(TAG, "Invalid arguments for saveContact request");
368 return;
369 }
370
benny.lin3a4e7a22014-01-08 10:58:08 +0800371 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800372 // Trim any empty fields, and RawContacts, before persisting
373 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700374 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800375
376 Uri lookupUri = null;
377
378 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700379
Josh Garguse692e012012-01-18 14:53:11 -0800380 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800381
Josh Gargusef15c8e2012-01-30 16:42:02 -0800382 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
383 long insertedRawContactId = -1;
384
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800385 // Attempt to persist changes
386 int tries = 0;
387 while (tries++ < PERSIST_TRIES) {
388 try {
389 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800390 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
391
392 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
393
394 for (CPOWrapper cpoWrapper : diffWrapper) {
395 diff.add(cpoWrapper.getOperation());
396 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700397
Katherine Kuana007e442011-07-07 09:25:34 -0700398 if (DEBUG) {
399 Log.v(TAG, "Content Provider Operations:");
400 for (ContentProviderOperation operation : diff) {
401 Log.v(TAG, operation.toString());
402 }
403 }
404
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700405 int numberProcessed = 0;
406 boolean batchFailed = false;
407 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
408 while (numberProcessed < diff.size()) {
409 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
410 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700411 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700412 batchFailed = true;
413 break;
414 } else {
415 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700416 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800417 }
418
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700419 if (batchFailed) {
420 // Retry save
421 continue;
422 }
423
Wenyi Wang67addcc2015-11-23 10:07:48 -0800424 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800425 if (rawContactId == -1) {
426 throw new IllegalStateException("Could not determine RawContact ID after save");
427 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800428 // We don't have to check to see if the value is still -1. If we reach here,
429 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800430 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700431 if (isProfile) {
432 // Since the profile supports local raw contacts, which may have been completely
433 // removed if all information was removed, we need to do a special query to
434 // get the lookup URI for the profile contact (if it still exists).
435 Cursor c = resolver.query(Profile.CONTENT_URI,
436 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
437 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800438 if (c == null) {
439 continue;
440 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700441 try {
Erik162b7e32011-09-20 15:23:55 -0700442 if (c.moveToFirst()) {
443 final long contactId = c.getLong(0);
444 final String lookupKey = c.getString(1);
445 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
446 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700447 } finally {
448 c.close();
449 }
450 } else {
451 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
452 rawContactId);
453 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
454 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800455 if (lookupUri != null) {
456 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
457 }
Josh Garguse692e012012-01-18 14:53:11 -0800458
459 // We can change this back to false later, if we fail to save the contact photo.
460 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800461 break;
462
463 } catch (RemoteException e) {
464 // Something went wrong, bail without success
465 Log.e(TAG, "Problem persisting user edits", e);
466 break;
467
Jay Shrauner57fca182014-01-17 14:20:50 -0800468 } catch (IllegalArgumentException e) {
469 // This is thrown by applyBatch on malformed requests
470 Log.e(TAG, "Problem persisting user edits", e);
471 showToast(R.string.contactSavedErrorToast);
472 break;
473
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800474 } catch (OperationApplicationException e) {
475 // Version consistency failed, re-parent change and try again
476 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
477 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
478 boolean first = true;
479 final int count = state.size();
480 for (int i = 0; i < count; i++) {
481 Long rawContactId = state.getRawContactId(i);
482 if (rawContactId != null && rawContactId != -1) {
483 if (!first) {
484 sb.append(',');
485 }
486 sb.append(rawContactId);
487 first = false;
488 }
489 }
490 sb.append(")");
491
492 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800493 throw new IllegalStateException(
494 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800495 }
496
Maurice Chu851222a2012-06-21 11:43:08 -0700497 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700498 isProfile
499 ? RawContactsEntity.PROFILE_CONTENT_URI
500 : RawContactsEntity.CONTENT_URI,
501 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700502 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700503
504 // Update the new state to use profile URIs if appropriate.
505 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700506 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700507 delta.setProfileQueryUri();
508 }
509 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800510 }
511 }
512
Josh Garguse692e012012-01-18 14:53:11 -0800513 // Now save any updated photos. We do this at the end to ensure that
514 // the ContactProvider already knows about newly-created contacts.
515 if (updatedPhotos != null) {
516 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700517 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800518 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800519
520 // If the raw-contact ID is negative, we are saving a new raw-contact;
521 // replace the bogus ID with the new one that we actually saved the contact at.
522 if (rawContactId < 0) {
523 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800524 }
525
Jay Shrauner511561d2015-04-02 10:35:33 -0700526 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700527 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700528 succeeded = false;
529 }
Josh Garguse692e012012-01-18 14:53:11 -0800530 }
531 }
532
Josh Garguse5d3f892012-04-11 11:56:15 -0700533 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
534 if (callbackIntent != null) {
535 if (succeeded) {
536 // Mark the intent to indicate that the save was successful (even if the lookup URI
537 // is now null). For local contacts or the local profile, it's possible that the
538 // save triggered removal of the contact, so no lookup URI would exist..
539 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
540 }
541 callbackIntent.setData(lookupUri);
542 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800543 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800544 }
545
Josh Garguse692e012012-01-18 14:53:11 -0800546 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700547 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
548 * subsets, adds the returned array to "results".
549 *
550 * @return the size of the array, if not null; -1 when the array is null.
551 */
552 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
553 ContentProviderResult[] results, ContentResolver resolver)
554 throws RemoteException, OperationApplicationException {
555 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
556 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
557 subset.addAll(diff.subList(offset, offset + subsetCount));
558 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
559 .AUTHORITY, subset);
560 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
561 return -1;
562 }
563 for (ContentProviderResult c : subsetResult) {
564 results[offset++] = c;
565 }
566 return subsetResult.length;
567 }
568
569 /**
Josh Garguse692e012012-01-18 14:53:11 -0800570 * Save updated photo for the specified raw-contact.
571 * @return true for success, false for failure
572 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800573 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800574 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800575 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
576 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
577
benny.lin3a4e7a22014-01-08 10:58:08 +0800578 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800579 }
580
Josh Gargusef15c8e2012-01-30 16:42:02 -0800581 /**
582 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
583 */
Maurice Chu851222a2012-06-21 11:43:08 -0700584 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800585 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800586 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800587 long existingRawContactId = state.findRawContactId();
588 if (existingRawContactId != -1) {
589 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800590 }
591
Wenyi Wang67addcc2015-11-23 10:07:48 -0800592 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800593 }
594
595 /**
596 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
597 */
598 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800599 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800600 if (results == null) {
601 return -1;
602 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800603 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800604 final int numResults = results.length;
605 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800606 final CPOWrapper cpoWrapper = diffWrapper.get(i);
607 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
608 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
609 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800610 return ContentUris.parseId(results[i].uri);
611 }
612 }
613 return -1;
614 }
615
616 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700617 * Creates an intent that can be sent to this service to create a new group as
618 * well as add new members at the same time.
619 *
620 * @param context of the application
621 * @param account in which the group should be created
622 * @param label is the name of the group (cannot be null)
623 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
624 * should be added to the group
625 * @param callbackActivity is the activity to send the callback intent to
626 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700627 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700628 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700629 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700630 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800631 Intent serviceIntent = new Intent(context, ContactSaveService.class);
632 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
633 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
634 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700635 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800636 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700637 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700638
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800639 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700640 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800641 Intent callbackIntent = new Intent(context, callbackActivity);
642 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700643 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800644
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700645 return serviceIntent;
646 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800647
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800648 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700649 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
650 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
651 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
652 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700653 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800654
655 ContentValues values = new ContentValues();
656 values.put(Groups.ACCOUNT_TYPE, accountType);
657 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700658 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800659 values.put(Groups.TITLE, label);
660
Katherine Kuan717e3432011-07-13 17:03:24 -0700661 final ContentResolver resolver = getContentResolver();
662
663 // Create the new group
664 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
665
666 // If there's no URI, then the insertion failed. Abort early because group members can't be
667 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800668 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700669 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800670 return;
671 }
672
Katherine Kuan717e3432011-07-13 17:03:24 -0700673 // Add new group members
674 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
675
676 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
677 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800678 values.clear();
679 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
680 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
681
682 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700683 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700684 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800685 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800686 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800687 }
688
689 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800690 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800691 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700692 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700693 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800694 Intent serviceIntent = new Intent(context, ContactSaveService.class);
695 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
696 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
697 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700698
699 // Callback intent will be invoked by the service once the group is renamed.
700 Intent callbackIntent = new Intent(context, callbackActivity);
701 callbackIntent.setAction(callbackAction);
702 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
703
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800704 return serviceIntent;
705 }
706
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800707 private void renameGroup(Intent intent) {
708 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
709 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
710
711 if (groupId == -1) {
712 Log.e(TAG, "Invalid arguments for renameGroup request");
713 return;
714 }
715
716 ContentValues values = new ContentValues();
717 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700718 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
719 getContentResolver().update(groupUri, values, null, null);
720
721 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
722 callbackIntent.setData(groupUri);
723 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800724 }
725
726 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800727 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800728 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800729 public static Intent createGroupDeletionIntent(Context context, long groupId) {
730 Intent serviceIntent = new Intent(context, ContactSaveService.class);
731 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800732 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800733 return serviceIntent;
734 }
735
736 private void deleteGroup(Intent intent) {
737 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
738 if (groupId == -1) {
739 Log.e(TAG, "Invalid arguments for deleteGroup request");
740 return;
741 }
742
743 getContentResolver().delete(
744 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
745 }
746
747 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700748 * Creates an intent that can be sent to this service to rename a group as
749 * well as add and remove members from the group.
750 *
751 * @param context of the application
752 * @param groupId of the group that should be modified
753 * @param newLabel is the updated name of the group (can be null if the name
754 * should not be updated)
755 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
756 * should be added to the group
757 * @param rawContactsToRemove is an array of raw contact IDs for contacts
758 * that should be removed from the group
759 * @param callbackActivity is the activity to send the callback intent to
760 * @param callbackAction is the intent action for the callback intent
761 */
762 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
763 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700764 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700765 Intent serviceIntent = new Intent(context, ContactSaveService.class);
766 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
767 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
768 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
769 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
770 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
771 rawContactsToRemove);
772
773 // Callback intent will be invoked by the service once the group is updated
774 Intent callbackIntent = new Intent(context, callbackActivity);
775 callbackIntent.setAction(callbackAction);
776 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
777
778 return serviceIntent;
779 }
780
781 private void updateGroup(Intent intent) {
782 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
783 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
784 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
785 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
786
787 if (groupId == -1) {
788 Log.e(TAG, "Invalid arguments for updateGroup request");
789 return;
790 }
791
792 final ContentResolver resolver = getContentResolver();
793 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
794
795 // Update group name if necessary
796 if (label != null) {
797 ContentValues values = new ContentValues();
798 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700799 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700800 }
801
Katherine Kuan717e3432011-07-13 17:03:24 -0700802 // Add and remove members if necessary
803 addMembersToGroup(resolver, rawContactsToAdd, groupId);
804 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
805
806 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
807 callbackIntent.setData(groupUri);
808 deliverCallback(callbackIntent);
809 }
810
Daniel Lehmann18958a22012-02-28 17:45:25 -0800811 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700812 long groupId) {
813 if (rawContactsToAdd == null) {
814 return;
815 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700816 for (long rawContactId : rawContactsToAdd) {
817 try {
818 final ArrayList<ContentProviderOperation> rawContactOperations =
819 new ArrayList<ContentProviderOperation>();
820
821 // Build an assert operation to ensure the contact is not already in the group
822 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
823 .newAssertQuery(Data.CONTENT_URI);
824 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
825 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
826 new String[] { String.valueOf(rawContactId),
827 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
828 assertBuilder.withExpectedCount(0);
829 rawContactOperations.add(assertBuilder.build());
830
831 // Build an insert operation to add the contact to the group
832 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
833 .newInsert(Data.CONTENT_URI);
834 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
835 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
836 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
837 rawContactOperations.add(insertBuilder.build());
838
839 if (DEBUG) {
840 for (ContentProviderOperation operation : rawContactOperations) {
841 Log.v(TAG, operation.toString());
842 }
843 }
844
845 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700846 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800847 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700848 }
849 } catch (RemoteException e) {
850 // Something went wrong, bail without success
851 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
852 String.valueOf(rawContactId), e);
853 } catch (OperationApplicationException e) {
854 // The assert could have failed because the contact is already in the group,
855 // just continue to the next contact
856 Log.w(TAG, "Assert failed in adding raw contact ID " +
857 String.valueOf(rawContactId) + ". Already exists in group " +
858 String.valueOf(groupId), e);
859 }
860 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700861 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700862
Daniel Lehmann18958a22012-02-28 17:45:25 -0800863 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700864 long groupId) {
865 if (rawContactsToRemove == null) {
866 return;
867 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700868 for (long rawContactId : rawContactsToRemove) {
869 // Apply the delete operation on the data row for the given raw contact's
870 // membership in the given group. If no contact matches the provided selection, then
871 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800872 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700873 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
874 new String[] { String.valueOf(rawContactId),
875 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
876 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700877 }
878
879 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800880 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800881 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800882 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
883 Intent serviceIntent = new Intent(context, ContactSaveService.class);
884 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
885 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
886 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
887
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800888 return serviceIntent;
889 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800890
891 private void setStarred(Intent intent) {
892 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
893 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
894 if (contactUri == null) {
895 Log.e(TAG, "Invalid arguments for setStarred request");
896 return;
897 }
898
899 final ContentValues values = new ContentValues(1);
900 values.put(Contacts.STARRED, value);
901 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700902
903 // Undemote the contact if necessary
904 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
905 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800906 if (c == null) {
907 return;
908 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700909 try {
910 if (c.moveToFirst()) {
911 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700912
913 // Don't bother undemoting if this contact is the user's profile.
914 if (id < Profile.MIN_ID) {
Brian Attwell2d88efa2014-12-17 21:49:56 -0800915 PinnedPositions.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700916 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700917 }
918 } finally {
919 c.close();
920 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800921 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800922
923 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700924 * Creates an intent that can be sent to this service to set the redirect to voicemail.
925 */
926 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
927 boolean value) {
928 Intent serviceIntent = new Intent(context, ContactSaveService.class);
929 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
930 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
931 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
932
933 return serviceIntent;
934 }
935
936 private void setSendToVoicemail(Intent intent) {
937 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
938 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
939 if (contactUri == null) {
940 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
941 return;
942 }
943
944 final ContentValues values = new ContentValues(1);
945 values.put(Contacts.SEND_TO_VOICEMAIL, value);
946 getContentResolver().update(contactUri, values, null, null);
947 }
948
949 /**
950 * Creates an intent that can be sent to this service to save the contact's ringtone.
951 */
952 public static Intent createSetRingtone(Context context, Uri contactUri,
953 String value) {
954 Intent serviceIntent = new Intent(context, ContactSaveService.class);
955 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
956 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
957 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
958
959 return serviceIntent;
960 }
961
962 private void setRingtone(Intent intent) {
963 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
964 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
965 if (contactUri == null) {
966 Log.e(TAG, "Invalid arguments for setRingtone");
967 return;
968 }
969 ContentValues values = new ContentValues(1);
970 values.put(Contacts.CUSTOM_RINGTONE, value);
971 getContentResolver().update(contactUri, values, null, null);
972 }
973
974 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800975 * Creates an intent that sets the selected data item as super primary (default)
976 */
977 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
978 Intent serviceIntent = new Intent(context, ContactSaveService.class);
979 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
980 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
981 return serviceIntent;
982 }
983
984 private void setSuperPrimary(Intent intent) {
985 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
986 if (dataId == -1) {
987 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
988 return;
989 }
990
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700991 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800992 }
993
994 /**
995 * Creates an intent that clears the primary flag of all data items that belong to the same
996 * raw_contact as the given data item. Will only clear, if the data item was primary before
997 * this call
998 */
999 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1000 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1001 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1002 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1003 return serviceIntent;
1004 }
1005
1006 private void clearPrimary(Intent intent) {
1007 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1008 if (dataId == -1) {
1009 Log.e(TAG, "Invalid arguments for clearPrimary request");
1010 return;
1011 }
1012
1013 // Update the primary values in the data record.
1014 ContentValues values = new ContentValues(1);
1015 values.put(Data.IS_SUPER_PRIMARY, 0);
1016 values.put(Data.IS_PRIMARY, 0);
1017
1018 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1019 values, null, null);
1020 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001021
1022 /**
1023 * Creates an intent that can be sent to this service to delete a contact.
1024 */
1025 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1026 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1027 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1028 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1029 return serviceIntent;
1030 }
1031
Brian Attwelld2962a32015-03-02 14:48:50 -08001032 /**
1033 * Creates an intent that can be sent to this service to delete multiple contacts.
1034 */
1035 public static Intent createDeleteMultipleContactsIntent(Context context,
1036 long[] contactIds) {
1037 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1038 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1039 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1040 return serviceIntent;
1041 }
1042
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001043 private void deleteContact(Intent intent) {
1044 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1045 if (contactUri == null) {
1046 Log.e(TAG, "Invalid arguments for deleteContact request");
1047 return;
1048 }
1049
1050 getContentResolver().delete(contactUri, null, null);
1051 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001052
Brian Attwelld2962a32015-03-02 14:48:50 -08001053 private void deleteMultipleContacts(Intent intent) {
1054 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1055 if (contactIds == null) {
1056 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1057 return;
1058 }
1059 for (long contactId : contactIds) {
1060 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1061 getContentResolver().delete(contactUri, null, null);
1062 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001063 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1064 .contacts_deleted_toast, contactIds.length);
1065 mMainHandler.post(new Runnable() {
1066 @Override
1067 public void run() {
1068 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1069 .show();
1070 }
1071 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001072 }
1073
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001074 /**
1075 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001076 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001077 */
1078 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001079 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001080 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1081 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1082 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1083 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001084
1085 // Callback intent will be invoked by the service once the contacts are joined.
1086 Intent callbackIntent = new Intent(context, callbackActivity);
1087 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001088 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1089
1090 return serviceIntent;
1091 }
1092
Brian Attwelld3946ca2015-03-03 11:13:49 -08001093 /**
1094 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1095 * No special attention is paid to where the resulting contact's name is taken from.
1096 */
1097 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1098 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1099 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1100 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1101 return serviceIntent;
1102 }
1103
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001104
1105 private interface JoinContactQuery {
1106 String[] PROJECTION = {
1107 RawContacts._ID,
1108 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001109 RawContacts.DISPLAY_NAME_SOURCE,
1110 };
1111
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001112 int _ID = 0;
1113 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001114 int DISPLAY_NAME_SOURCE = 2;
1115 }
1116
1117 private interface ContactEntityQuery {
1118 String[] PROJECTION = {
1119 Contacts.Entity.DATA_ID,
1120 Contacts.Entity.CONTACT_ID,
1121 Contacts.Entity.IS_SUPER_PRIMARY,
1122 };
1123 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1124 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1125 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1126 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1127
1128 int DATA_ID = 0;
1129 int CONTACT_ID = 1;
1130 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001131 }
1132
Brian Attwelld3946ca2015-03-03 11:13:49 -08001133 private void joinSeveralContacts(Intent intent) {
1134 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001135
Brian Attwelld3946ca2015-03-03 11:13:49 -08001136 // Load raw contact IDs for all contacts involved.
1137 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1138 if (rawContactIds == null) {
1139 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001140 return;
1141 }
1142
Brian Attwelld3946ca2015-03-03 11:13:49 -08001143 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001144 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001145 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1146 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1147 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001148 for (int i = 0; i < rawContactIds.length; i++) {
1149 for (int j = 0; j < rawContactIds.length; j++) {
1150 if (i != j) {
1151 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1152 }
Walter Jang0653de32015-07-24 12:12:40 -07001153 // Before we get to 500 we need to flush the operations list
1154 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1155 if (!applyJoinOperations(resolver, operations)) {
1156 return;
1157 }
1158 operations.clear();
1159 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001160 }
1161 }
Walter Jang0653de32015-07-24 12:12:40 -07001162 if (operations.size() > 0 && !applyJoinOperations(resolver, operations)) {
1163 return;
1164 }
1165 showToast(R.string.contactsJoinedMessage);
1166 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001167
Walter Jang0653de32015-07-24 12:12:40 -07001168 /** Returns true if the batch was successfully applied and false otherwise. */
1169 private boolean applyJoinOperations(ContentResolver resolver,
1170 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001171 try {
1172 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001173 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001174 } catch (RemoteException | OperationApplicationException e) {
1175 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1176 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001177 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001178 }
1179 }
1180
1181
1182 private void joinContacts(Intent intent) {
1183 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1184 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001185
1186 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001187 // in the join UIs.
1188 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1189 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001190 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001191 return;
1192 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001193
Brian Attwell548f5c62015-01-27 17:46:46 -08001194 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001195
1196 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001197 for (int i = 0; i < rawContactIds.length; i++) {
1198 for (int j = 0; j < rawContactIds.length; j++) {
1199 if (i != j) {
1200 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1201 }
1202 }
1203 }
1204
Brian Attwelld3946ca2015-03-03 11:13:49 -08001205 final ContentResolver resolver = getContentResolver();
1206
Brian Attwell548f5c62015-01-27 17:46:46 -08001207 // Use the name for contactId1 as the name for the newly aggregated contact.
1208 final Uri contactId1Uri = ContentUris.withAppendedId(
1209 Contacts.CONTENT_URI, contactId1);
1210 final Uri entityUri = Uri.withAppendedPath(
1211 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1212 Cursor c = resolver.query(entityUri,
1213 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1214 if (c == null) {
1215 Log.e(TAG, "Unable to open Contacts DB cursor");
1216 showToast(R.string.contactSavedErrorToast);
1217 return;
1218 }
1219 long dataIdToAddSuperPrimary = -1;
1220 try {
1221 if (c.moveToFirst()) {
1222 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1223 }
1224 } finally {
1225 c.close();
1226 }
1227
1228 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1229 // display name does not change as a result of the join.
1230 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001231 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001232 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1233 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1234 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001235 operations.add(builder.build());
1236 }
1237
1238 boolean success = false;
1239 // Apply all aggregation exceptions as one batch
1240 try {
1241 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001242 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001243 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001244 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001245 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001246 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001247 }
1248
1249 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1250 if (success) {
1251 Uri uri = RawContacts.getContactLookupUri(resolver,
1252 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1253 callbackIntent.setData(uri);
1254 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001255 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001256 }
1257
Brian Attwelld3946ca2015-03-03 11:13:49 -08001258 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1259 if (contactIds == null) {
1260 return null;
1261 }
1262
Brian Attwell548f5c62015-01-27 17:46:46 -08001263 final ContentResolver resolver = getContentResolver();
1264 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001265
1266 final StringBuilder queryBuilder = new StringBuilder();
1267 final String stringContactIds[] = new String[contactIds.length];
1268 for (int i = 0; i < contactIds.length; i++) {
1269 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1270 stringContactIds[i] = String.valueOf(contactIds[i]);
1271 if (contactIds[i] == -1) {
1272 return null;
1273 }
1274 if (i == contactIds.length -1) {
1275 break;
1276 }
1277 queryBuilder.append(" OR ");
1278 }
1279
Brian Attwell548f5c62015-01-27 17:46:46 -08001280 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1281 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001282 queryBuilder.toString(),
1283 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001284 if (c == null) {
1285 Log.e(TAG, "Unable to open Contacts DB cursor");
1286 showToast(R.string.contactSavedErrorToast);
1287 return null;
1288 }
1289 try {
1290 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001291 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001292 return null;
1293 }
1294 rawContactIds = new long[c.getCount()];
1295 for (int i = 0; i < rawContactIds.length; i++) {
1296 c.moveToPosition(i);
1297 long rawContactId = c.getLong(JoinContactQuery._ID);
1298 rawContactIds[i] = rawContactId;
1299 }
1300 } finally {
1301 c.close();
1302 }
1303 return rawContactIds;
1304 }
1305
Brian Attwelld3946ca2015-03-03 11:13:49 -08001306 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1307 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1308 }
1309
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001310 /**
1311 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1312 */
1313 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1314 long rawContactId1, long rawContactId2) {
1315 Builder builder =
1316 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1317 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1318 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1319 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1320 operations.add(builder.build());
1321 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001322
1323 /**
1324 * Shows a toast on the UI thread.
1325 */
1326 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001327 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001328
1329 @Override
1330 public void run() {
1331 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1332 }
1333 });
1334 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001335
1336 private void deliverCallback(final Intent callbackIntent) {
1337 mMainHandler.post(new Runnable() {
1338
1339 @Override
1340 public void run() {
1341 deliverCallbackOnUiThread(callbackIntent);
1342 }
1343 });
1344 }
1345
1346 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1347 // TODO: this assumes that if there are multiple instances of the same
1348 // activity registered, the last one registered is the one waiting for
1349 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001350 for (Listener listener : sListeners) {
1351 if (callbackIntent.getComponent().equals(
1352 ((Activity) listener).getIntent().getComponent())) {
1353 listener.onServiceCompleted(callbackIntent);
1354 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001355 }
1356 }
1357 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001358}