blob: d014087e58101fb46773c38925ad97f066fd302d [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
Chiao Chengd7ca03e2012-10-24 15:14:08 -070052import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080053import com.android.contacts.common.model.AccountTypeManager;
Yorke Leecd321f62013-10-28 15:20:15 -070054import com.android.contacts.common.model.RawContactDelta;
55import com.android.contacts.common.model.RawContactDeltaList;
56import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080057import com.android.contacts.common.model.account.AccountWithDataSet;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070058import com.android.contacts.common.util.PermissionsUtil;
Yorke Lee637a38e2013-09-14 08:36:33 -070059import com.android.contacts.util.ContactPhotoUtils;
60
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070061import com.google.common.collect.Lists;
62import com.google.common.collect.Sets;
63
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080064import java.util.ArrayList;
65import java.util.HashSet;
66import java.util.List;
67import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070068
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080069/**
70 * A service responsible for saving changes to the content provider.
71 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070072public class ContactSaveService extends IntentService {
73 private static final String TAG = "ContactSaveService";
74
Katherine Kuana007e442011-07-07 09:25:34 -070075 /** Set to true in order to view logs on content provider operations */
76 private static final boolean DEBUG = false;
77
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070078 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
79
80 public static final String EXTRA_ACCOUNT_NAME = "accountName";
81 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070082 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070083 public static final String EXTRA_CONTENT_VALUES = "contentValues";
84 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
85
Dmitri Plotnikova0114142011-02-15 13:53:21 -080086 public static final String ACTION_SAVE_CONTACT = "saveContact";
87 public static final String EXTRA_CONTACT_STATE = "state";
88 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070089 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070090 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080091 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070092
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080093 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080094 public static final String ACTION_RENAME_GROUP = "renameGroup";
95 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070096 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080097 public static final String EXTRA_GROUP_ID = "groupId";
98 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070099 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
100 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800101
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800102 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800103 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800104 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800105 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800106 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800107 public static final String EXTRA_STARRED_FLAG = "starred";
108
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800109 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
110 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
111 public static final String EXTRA_DATA_ID = "dataId";
112
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800113 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800114 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800115 public static final String EXTRA_CONTACT_ID1 = "contactId1";
116 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800117
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700118 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
119 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
120
121 public static final String ACTION_SET_RINGTONE = "setRingtone";
122 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
123
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700124 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
125 Data.MIMETYPE,
126 Data.IS_PRIMARY,
127 Data.DATA1,
128 Data.DATA2,
129 Data.DATA3,
130 Data.DATA4,
131 Data.DATA5,
132 Data.DATA6,
133 Data.DATA7,
134 Data.DATA8,
135 Data.DATA9,
136 Data.DATA10,
137 Data.DATA11,
138 Data.DATA12,
139 Data.DATA13,
140 Data.DATA14,
141 Data.DATA15
142 );
143
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800144 private static final int PERSIST_TRIES = 3;
145
Walter Jang0653de32015-07-24 12:12:40 -0700146 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
147
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800148 public interface Listener {
149 public void onServiceCompleted(Intent callbackIntent);
150 }
151
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100152 private static final CopyOnWriteArrayList<Listener> sListeners =
153 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800154
155 private Handler mMainHandler;
156
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700157 public ContactSaveService() {
158 super(TAG);
159 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800160 mMainHandler = new Handler(Looper.getMainLooper());
161 }
162
163 public static void registerListener(Listener listener) {
164 if (!(listener instanceof Activity)) {
165 throw new ClassCastException("Only activities can be registered to"
166 + " receive callback from " + ContactSaveService.class.getName());
167 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100168 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800169 }
170
171 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100172 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700173 }
174
175 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800176 public Object getSystemService(String name) {
177 Object service = super.getSystemService(name);
178 if (service != null) {
179 return service;
180 }
181
182 return getApplicationContext().getSystemService(name);
183 }
184
185 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700186 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800187 if (intent == null) {
188 Log.d(TAG, "onHandleIntent: could not handle null intent");
189 return;
190 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700191 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
192 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
193 // TODO: add more specific error string such as "Turn on Contacts
194 // permission to update your contacts"
195 showToast(R.string.contactSavedErrorToast);
196 return;
197 }
198
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700199 // Call an appropriate method. If we're sure it affects how incoming phone calls are
200 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700201 String action = intent.getAction();
202 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
203 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800204 } else if (ACTION_SAVE_CONTACT.equals(action)) {
205 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800206 } else if (ACTION_CREATE_GROUP.equals(action)) {
207 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800208 } else if (ACTION_RENAME_GROUP.equals(action)) {
209 renameGroup(intent);
210 } else if (ACTION_DELETE_GROUP.equals(action)) {
211 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700212 } else if (ACTION_UPDATE_GROUP.equals(action)) {
213 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800214 } else if (ACTION_SET_STARRED.equals(action)) {
215 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800216 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
217 setSuperPrimary(intent);
218 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
219 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800220 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
221 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800222 } else if (ACTION_DELETE_CONTACT.equals(action)) {
223 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800224 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
225 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800226 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
227 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700228 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
229 setSendToVoicemail(intent);
230 } else if (ACTION_SET_RINGTONE.equals(action)) {
231 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700232 }
233 }
234
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800235 /**
236 * Creates an intent that can be sent to this service to create a new raw contact
237 * using data presented as a set of ContentValues.
238 */
239 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700240 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700241 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800242 Intent serviceIntent = new Intent(
243 context, ContactSaveService.class);
244 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
245 if (account != null) {
246 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
247 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700248 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800249 }
250 serviceIntent.putParcelableArrayListExtra(
251 ContactSaveService.EXTRA_CONTENT_VALUES, values);
252
253 // Callback intent will be invoked by the service once the new contact is
254 // created. The service will put the URI of the new contact as "data" on
255 // the callback intent.
256 Intent callbackIntent = new Intent(context, callbackActivity);
257 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800258 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
259 return serviceIntent;
260 }
261
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700262 private void createRawContact(Intent intent) {
263 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
264 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700265 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700266 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
267 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
268
269 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
270 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
271 .withValue(RawContacts.ACCOUNT_NAME, accountName)
272 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700273 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700274 .build());
275
276 int size = valueList.size();
277 for (int i = 0; i < size; i++) {
278 ContentValues values = valueList.get(i);
279 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
280 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
281 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
282 .withValues(values)
283 .build());
284 }
285
286 ContentResolver resolver = getContentResolver();
287 ContentProviderResult[] results;
288 try {
289 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
290 } catch (Exception e) {
291 throw new RuntimeException("Failed to store new contact", e);
292 }
293
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700294 Uri rawContactUri = results[0].uri;
295 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
296
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800297 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700298 }
299
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700300 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800301 * Creates an intent that can be sent to this service to create a new raw contact
302 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800303 * This variant is more convenient to use when there is only one photo that can
304 * possibly be updated, as in the Contact Details screen.
305 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
306 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800307 */
Maurice Chu851222a2012-06-21 11:43:08 -0700308 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700309 String saveModeExtraKey, int saveMode, boolean isProfile,
310 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700311 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800312 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700313 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800314 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700315 callbackActivity, callbackAction, bundle,
316 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800317 }
318
319 /**
320 * Creates an intent that can be sent to this service to create a new raw contact
321 * using data presented as a set of ContentValues.
322 * This variant is used when multiple contacts' photos may be updated, as in the
323 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700324 *
Josh Garguse692e012012-01-18 14:53:11 -0800325 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700326 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
327 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800328 */
Maurice Chu851222a2012-06-21 11:43:08 -0700329 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700330 String saveModeExtraKey, int saveMode, boolean isProfile,
331 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700332 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800333 Intent serviceIntent = new Intent(
334 context, ContactSaveService.class);
335 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
336 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700337 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800338 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
339
Josh Garguse692e012012-01-18 14:53:11 -0800340 if (updatedPhotos != null) {
341 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
342 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800343
Josh Garguse5d3f892012-04-11 11:56:15 -0700344 if (callbackActivity != null) {
345 // Callback intent will be invoked by the service once the contact is
346 // saved. The service will put the URI of the new contact as "data" on
347 // the callback intent.
348 Intent callbackIntent = new Intent(context, callbackActivity);
349 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700350 if (joinContactIdExtraKey != null && joinContactId != null) {
351 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
352 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700353 callbackIntent.setAction(callbackAction);
354 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
355 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800356 return serviceIntent;
357 }
358
359 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700360 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700361 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800362 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800363
Jay Shrauner08099782015-03-25 14:17:11 -0700364 if (state == null) {
365 Log.e(TAG, "Invalid arguments for saveContact request");
366 return;
367 }
368
benny.lin3a4e7a22014-01-08 10:58:08 +0800369 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800370 // Trim any empty fields, and RawContacts, before persisting
371 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700372 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800373
374 Uri lookupUri = null;
375
376 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700377
Josh Garguse692e012012-01-18 14:53:11 -0800378 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800379
Josh Gargusef15c8e2012-01-30 16:42:02 -0800380 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
381 long insertedRawContactId = -1;
382
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800383 // Attempt to persist changes
384 int tries = 0;
385 while (tries++ < PERSIST_TRIES) {
386 try {
387 // Build operations and try applying
388 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700389
Katherine Kuana007e442011-07-07 09:25:34 -0700390 if (DEBUG) {
391 Log.v(TAG, "Content Provider Operations:");
392 for (ContentProviderOperation operation : diff) {
393 Log.v(TAG, operation.toString());
394 }
395 }
396
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700397 int numberProcessed = 0;
398 boolean batchFailed = false;
399 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
400 while (numberProcessed < diff.size()) {
401 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
402 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700403 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700404 batchFailed = true;
405 break;
406 } else {
407 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700408 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800409 }
410
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700411 if (batchFailed) {
412 // Retry save
413 continue;
414 }
415
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800416 final long rawContactId = getRawContactId(state, diff, results);
417 if (rawContactId == -1) {
418 throw new IllegalStateException("Could not determine RawContact ID after save");
419 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800420 // We don't have to check to see if the value is still -1. If we reach here,
421 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
422 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700423 if (isProfile) {
424 // Since the profile supports local raw contacts, which may have been completely
425 // removed if all information was removed, we need to do a special query to
426 // get the lookup URI for the profile contact (if it still exists).
427 Cursor c = resolver.query(Profile.CONTENT_URI,
428 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
429 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800430 if (c == null) {
431 continue;
432 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700433 try {
Erik162b7e32011-09-20 15:23:55 -0700434 if (c.moveToFirst()) {
435 final long contactId = c.getLong(0);
436 final String lookupKey = c.getString(1);
437 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
438 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700439 } finally {
440 c.close();
441 }
442 } else {
443 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
444 rawContactId);
445 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
446 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800447 if (lookupUri != null) {
448 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
449 }
Josh Garguse692e012012-01-18 14:53:11 -0800450
451 // We can change this back to false later, if we fail to save the contact photo.
452 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800453 break;
454
455 } catch (RemoteException e) {
456 // Something went wrong, bail without success
457 Log.e(TAG, "Problem persisting user edits", e);
458 break;
459
Jay Shrauner57fca182014-01-17 14:20:50 -0800460 } catch (IllegalArgumentException e) {
461 // This is thrown by applyBatch on malformed requests
462 Log.e(TAG, "Problem persisting user edits", e);
463 showToast(R.string.contactSavedErrorToast);
464 break;
465
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800466 } catch (OperationApplicationException e) {
467 // Version consistency failed, re-parent change and try again
468 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
469 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
470 boolean first = true;
471 final int count = state.size();
472 for (int i = 0; i < count; i++) {
473 Long rawContactId = state.getRawContactId(i);
474 if (rawContactId != null && rawContactId != -1) {
475 if (!first) {
476 sb.append(',');
477 }
478 sb.append(rawContactId);
479 first = false;
480 }
481 }
482 sb.append(")");
483
484 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800485 throw new IllegalStateException(
486 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800487 }
488
Maurice Chu851222a2012-06-21 11:43:08 -0700489 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700490 isProfile
491 ? RawContactsEntity.PROFILE_CONTENT_URI
492 : RawContactsEntity.CONTENT_URI,
493 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700494 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700495
496 // Update the new state to use profile URIs if appropriate.
497 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700498 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700499 delta.setProfileQueryUri();
500 }
501 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800502 }
503 }
504
Josh Garguse692e012012-01-18 14:53:11 -0800505 // Now save any updated photos. We do this at the end to ensure that
506 // the ContactProvider already knows about newly-created contacts.
507 if (updatedPhotos != null) {
508 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700509 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800510 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800511
512 // If the raw-contact ID is negative, we are saving a new raw-contact;
513 // replace the bogus ID with the new one that we actually saved the contact at.
514 if (rawContactId < 0) {
515 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800516 }
517
Jay Shrauner511561d2015-04-02 10:35:33 -0700518 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700519 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700520 succeeded = false;
521 }
Josh Garguse692e012012-01-18 14:53:11 -0800522 }
523 }
524
Josh Garguse5d3f892012-04-11 11:56:15 -0700525 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
526 if (callbackIntent != null) {
527 if (succeeded) {
528 // Mark the intent to indicate that the save was successful (even if the lookup URI
529 // is now null). For local contacts or the local profile, it's possible that the
530 // save triggered removal of the contact, so no lookup URI would exist..
531 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
532 }
533 callbackIntent.setData(lookupUri);
534 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800535 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800536 }
537
Josh Garguse692e012012-01-18 14:53:11 -0800538 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700539 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
540 * subsets, adds the returned array to "results".
541 *
542 * @return the size of the array, if not null; -1 when the array is null.
543 */
544 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
545 ContentProviderResult[] results, ContentResolver resolver)
546 throws RemoteException, OperationApplicationException {
547 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
548 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
549 subset.addAll(diff.subList(offset, offset + subsetCount));
550 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
551 .AUTHORITY, subset);
552 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
553 return -1;
554 }
555 for (ContentProviderResult c : subsetResult) {
556 results[offset++] = c;
557 }
558 return subsetResult.length;
559 }
560
561 /**
Josh Garguse692e012012-01-18 14:53:11 -0800562 * Save updated photo for the specified raw-contact.
563 * @return true for success, false for failure
564 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800565 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800566 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800567 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
568 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
569
benny.lin3a4e7a22014-01-08 10:58:08 +0800570 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800571 }
572
Josh Gargusef15c8e2012-01-30 16:42:02 -0800573 /**
574 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
575 */
Maurice Chu851222a2012-06-21 11:43:08 -0700576 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800577 final ArrayList<ContentProviderOperation> diff,
578 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800579 long existingRawContactId = state.findRawContactId();
580 if (existingRawContactId != -1) {
581 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800582 }
583
Josh Gargusef15c8e2012-01-30 16:42:02 -0800584 return getInsertedRawContactId(diff, results);
585 }
586
587 /**
588 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
589 */
590 private long getInsertedRawContactId(
591 final ArrayList<ContentProviderOperation> diff,
592 final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800593 if (results == null) {
594 return -1;
595 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800596 final int diffSize = diff.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800597 final int numResults = results.length;
598 for (int i = 0; i < diffSize && i < numResults; i++) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800599 ContentProviderOperation operation = diff.get(i);
Brian Attwell13f94e12015-01-22 16:27:48 -0800600 if (operation.isInsert() && operation.getUri().getEncodedPath().contains(
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800601 RawContacts.CONTENT_URI.getEncodedPath())) {
602 return ContentUris.parseId(results[i].uri);
603 }
604 }
605 return -1;
606 }
607
608 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700609 * Creates an intent that can be sent to this service to create a new group as
610 * well as add new members at the same time.
611 *
612 * @param context of the application
613 * @param account in which the group should be created
614 * @param label is the name of the group (cannot be null)
615 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
616 * should be added to the group
617 * @param callbackActivity is the activity to send the callback intent to
618 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700619 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700620 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700621 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700622 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800623 Intent serviceIntent = new Intent(context, ContactSaveService.class);
624 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
625 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
626 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700627 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800628 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700629 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700630
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800631 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700632 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800633 Intent callbackIntent = new Intent(context, callbackActivity);
634 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700635 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800636
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700637 return serviceIntent;
638 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800639
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800640 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700641 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
642 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
643 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
644 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700645 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800646
647 ContentValues values = new ContentValues();
648 values.put(Groups.ACCOUNT_TYPE, accountType);
649 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700650 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800651 values.put(Groups.TITLE, label);
652
Katherine Kuan717e3432011-07-13 17:03:24 -0700653 final ContentResolver resolver = getContentResolver();
654
655 // Create the new group
656 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
657
658 // If there's no URI, then the insertion failed. Abort early because group members can't be
659 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800660 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700661 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800662 return;
663 }
664
Katherine Kuan717e3432011-07-13 17:03:24 -0700665 // Add new group members
666 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
667
668 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
669 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800670 values.clear();
671 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
672 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
673
674 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700675 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700676 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800677 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800678 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800679 }
680
681 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800682 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800683 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700684 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700685 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800686 Intent serviceIntent = new Intent(context, ContactSaveService.class);
687 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
688 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
689 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700690
691 // Callback intent will be invoked by the service once the group is renamed.
692 Intent callbackIntent = new Intent(context, callbackActivity);
693 callbackIntent.setAction(callbackAction);
694 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
695
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800696 return serviceIntent;
697 }
698
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800699 private void renameGroup(Intent intent) {
700 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
701 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
702
703 if (groupId == -1) {
704 Log.e(TAG, "Invalid arguments for renameGroup request");
705 return;
706 }
707
708 ContentValues values = new ContentValues();
709 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700710 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
711 getContentResolver().update(groupUri, values, null, null);
712
713 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
714 callbackIntent.setData(groupUri);
715 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800716 }
717
718 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800719 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800720 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800721 public static Intent createGroupDeletionIntent(Context context, long groupId) {
722 Intent serviceIntent = new Intent(context, ContactSaveService.class);
723 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800724 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800725 return serviceIntent;
726 }
727
728 private void deleteGroup(Intent intent) {
729 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
730 if (groupId == -1) {
731 Log.e(TAG, "Invalid arguments for deleteGroup request");
732 return;
733 }
734
735 getContentResolver().delete(
736 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
737 }
738
739 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700740 * Creates an intent that can be sent to this service to rename a group as
741 * well as add and remove members from the group.
742 *
743 * @param context of the application
744 * @param groupId of the group that should be modified
745 * @param newLabel is the updated name of the group (can be null if the name
746 * should not be updated)
747 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
748 * should be added to the group
749 * @param rawContactsToRemove is an array of raw contact IDs for contacts
750 * that should be removed from the group
751 * @param callbackActivity is the activity to send the callback intent to
752 * @param callbackAction is the intent action for the callback intent
753 */
754 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
755 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700756 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700757 Intent serviceIntent = new Intent(context, ContactSaveService.class);
758 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
759 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
760 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
761 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
762 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
763 rawContactsToRemove);
764
765 // Callback intent will be invoked by the service once the group is updated
766 Intent callbackIntent = new Intent(context, callbackActivity);
767 callbackIntent.setAction(callbackAction);
768 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
769
770 return serviceIntent;
771 }
772
773 private void updateGroup(Intent intent) {
774 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
775 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
776 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
777 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
778
779 if (groupId == -1) {
780 Log.e(TAG, "Invalid arguments for updateGroup request");
781 return;
782 }
783
784 final ContentResolver resolver = getContentResolver();
785 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
786
787 // Update group name if necessary
788 if (label != null) {
789 ContentValues values = new ContentValues();
790 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700791 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700792 }
793
Katherine Kuan717e3432011-07-13 17:03:24 -0700794 // Add and remove members if necessary
795 addMembersToGroup(resolver, rawContactsToAdd, groupId);
796 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
797
798 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
799 callbackIntent.setData(groupUri);
800 deliverCallback(callbackIntent);
801 }
802
Daniel Lehmann18958a22012-02-28 17:45:25 -0800803 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700804 long groupId) {
805 if (rawContactsToAdd == null) {
806 return;
807 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700808 for (long rawContactId : rawContactsToAdd) {
809 try {
810 final ArrayList<ContentProviderOperation> rawContactOperations =
811 new ArrayList<ContentProviderOperation>();
812
813 // Build an assert operation to ensure the contact is not already in the group
814 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
815 .newAssertQuery(Data.CONTENT_URI);
816 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
817 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
818 new String[] { String.valueOf(rawContactId),
819 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
820 assertBuilder.withExpectedCount(0);
821 rawContactOperations.add(assertBuilder.build());
822
823 // Build an insert operation to add the contact to the group
824 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
825 .newInsert(Data.CONTENT_URI);
826 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
827 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
828 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
829 rawContactOperations.add(insertBuilder.build());
830
831 if (DEBUG) {
832 for (ContentProviderOperation operation : rawContactOperations) {
833 Log.v(TAG, operation.toString());
834 }
835 }
836
837 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700838 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800839 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700840 }
841 } catch (RemoteException e) {
842 // Something went wrong, bail without success
843 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
844 String.valueOf(rawContactId), e);
845 } catch (OperationApplicationException e) {
846 // The assert could have failed because the contact is already in the group,
847 // just continue to the next contact
848 Log.w(TAG, "Assert failed in adding raw contact ID " +
849 String.valueOf(rawContactId) + ". Already exists in group " +
850 String.valueOf(groupId), e);
851 }
852 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700853 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700854
Daniel Lehmann18958a22012-02-28 17:45:25 -0800855 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700856 long groupId) {
857 if (rawContactsToRemove == null) {
858 return;
859 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700860 for (long rawContactId : rawContactsToRemove) {
861 // Apply the delete operation on the data row for the given raw contact's
862 // membership in the given group. If no contact matches the provided selection, then
863 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800864 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700865 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
866 new String[] { String.valueOf(rawContactId),
867 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
868 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700869 }
870
871 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800872 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800873 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800874 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
875 Intent serviceIntent = new Intent(context, ContactSaveService.class);
876 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
877 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
878 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
879
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800880 return serviceIntent;
881 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800882
883 private void setStarred(Intent intent) {
884 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
885 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
886 if (contactUri == null) {
887 Log.e(TAG, "Invalid arguments for setStarred request");
888 return;
889 }
890
891 final ContentValues values = new ContentValues(1);
892 values.put(Contacts.STARRED, value);
893 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700894
895 // Undemote the contact if necessary
896 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
897 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800898 if (c == null) {
899 return;
900 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700901 try {
902 if (c.moveToFirst()) {
903 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700904
905 // Don't bother undemoting if this contact is the user's profile.
906 if (id < Profile.MIN_ID) {
Brian Attwell2d88efa2014-12-17 21:49:56 -0800907 PinnedPositions.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700908 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700909 }
910 } finally {
911 c.close();
912 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800913 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800914
915 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700916 * Creates an intent that can be sent to this service to set the redirect to voicemail.
917 */
918 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
919 boolean value) {
920 Intent serviceIntent = new Intent(context, ContactSaveService.class);
921 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
922 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
923 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
924
925 return serviceIntent;
926 }
927
928 private void setSendToVoicemail(Intent intent) {
929 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
930 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
931 if (contactUri == null) {
932 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
933 return;
934 }
935
936 final ContentValues values = new ContentValues(1);
937 values.put(Contacts.SEND_TO_VOICEMAIL, value);
938 getContentResolver().update(contactUri, values, null, null);
939 }
940
941 /**
942 * Creates an intent that can be sent to this service to save the contact's ringtone.
943 */
944 public static Intent createSetRingtone(Context context, Uri contactUri,
945 String value) {
946 Intent serviceIntent = new Intent(context, ContactSaveService.class);
947 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
948 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
949 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
950
951 return serviceIntent;
952 }
953
954 private void setRingtone(Intent intent) {
955 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
956 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
957 if (contactUri == null) {
958 Log.e(TAG, "Invalid arguments for setRingtone");
959 return;
960 }
961 ContentValues values = new ContentValues(1);
962 values.put(Contacts.CUSTOM_RINGTONE, value);
963 getContentResolver().update(contactUri, values, null, null);
964 }
965
966 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800967 * Creates an intent that sets the selected data item as super primary (default)
968 */
969 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
970 Intent serviceIntent = new Intent(context, ContactSaveService.class);
971 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
972 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
973 return serviceIntent;
974 }
975
976 private void setSuperPrimary(Intent intent) {
977 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
978 if (dataId == -1) {
979 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
980 return;
981 }
982
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700983 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800984 }
985
986 /**
987 * Creates an intent that clears the primary flag of all data items that belong to the same
988 * raw_contact as the given data item. Will only clear, if the data item was primary before
989 * this call
990 */
991 public static Intent createClearPrimaryIntent(Context context, long dataId) {
992 Intent serviceIntent = new Intent(context, ContactSaveService.class);
993 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
994 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
995 return serviceIntent;
996 }
997
998 private void clearPrimary(Intent intent) {
999 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1000 if (dataId == -1) {
1001 Log.e(TAG, "Invalid arguments for clearPrimary request");
1002 return;
1003 }
1004
1005 // Update the primary values in the data record.
1006 ContentValues values = new ContentValues(1);
1007 values.put(Data.IS_SUPER_PRIMARY, 0);
1008 values.put(Data.IS_PRIMARY, 0);
1009
1010 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1011 values, null, null);
1012 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001013
1014 /**
1015 * Creates an intent that can be sent to this service to delete a contact.
1016 */
1017 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1018 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1019 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1020 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1021 return serviceIntent;
1022 }
1023
Brian Attwelld2962a32015-03-02 14:48:50 -08001024 /**
1025 * Creates an intent that can be sent to this service to delete multiple contacts.
1026 */
1027 public static Intent createDeleteMultipleContactsIntent(Context context,
1028 long[] contactIds) {
1029 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1030 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1031 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1032 return serviceIntent;
1033 }
1034
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001035 private void deleteContact(Intent intent) {
1036 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1037 if (contactUri == null) {
1038 Log.e(TAG, "Invalid arguments for deleteContact request");
1039 return;
1040 }
1041
1042 getContentResolver().delete(contactUri, null, null);
1043 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001044
Brian Attwelld2962a32015-03-02 14:48:50 -08001045 private void deleteMultipleContacts(Intent intent) {
1046 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1047 if (contactIds == null) {
1048 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1049 return;
1050 }
1051 for (long contactId : contactIds) {
1052 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1053 getContentResolver().delete(contactUri, null, null);
1054 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001055 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1056 .contacts_deleted_toast, contactIds.length);
1057 mMainHandler.post(new Runnable() {
1058 @Override
1059 public void run() {
1060 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1061 .show();
1062 }
1063 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001064 }
1065
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001066 /**
1067 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001068 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001069 */
1070 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001071 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001072 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1073 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1074 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1075 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001076
1077 // Callback intent will be invoked by the service once the contacts are joined.
1078 Intent callbackIntent = new Intent(context, callbackActivity);
1079 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001080 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1081
1082 return serviceIntent;
1083 }
1084
Brian Attwelld3946ca2015-03-03 11:13:49 -08001085 /**
1086 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1087 * No special attention is paid to where the resulting contact's name is taken from.
1088 */
1089 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1090 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1091 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1092 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1093 return serviceIntent;
1094 }
1095
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001096
1097 private interface JoinContactQuery {
1098 String[] PROJECTION = {
1099 RawContacts._ID,
1100 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001101 RawContacts.DISPLAY_NAME_SOURCE,
1102 };
1103
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001104 int _ID = 0;
1105 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001106 int DISPLAY_NAME_SOURCE = 2;
1107 }
1108
1109 private interface ContactEntityQuery {
1110 String[] PROJECTION = {
1111 Contacts.Entity.DATA_ID,
1112 Contacts.Entity.CONTACT_ID,
1113 Contacts.Entity.IS_SUPER_PRIMARY,
1114 };
1115 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1116 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1117 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1118 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1119
1120 int DATA_ID = 0;
1121 int CONTACT_ID = 1;
1122 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001123 }
1124
Brian Attwelld3946ca2015-03-03 11:13:49 -08001125 private void joinSeveralContacts(Intent intent) {
1126 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001127
Brian Attwelld3946ca2015-03-03 11:13:49 -08001128 // Load raw contact IDs for all contacts involved.
1129 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1130 if (rawContactIds == null) {
1131 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001132 return;
1133 }
1134
Brian Attwelld3946ca2015-03-03 11:13:49 -08001135 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001136 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001137 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1138 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1139 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001140 for (int i = 0; i < rawContactIds.length; i++) {
1141 for (int j = 0; j < rawContactIds.length; j++) {
1142 if (i != j) {
1143 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1144 }
Walter Jang0653de32015-07-24 12:12:40 -07001145 // Before we get to 500 we need to flush the operations list
1146 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1147 if (!applyJoinOperations(resolver, operations)) {
1148 return;
1149 }
1150 operations.clear();
1151 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001152 }
1153 }
Walter Jang0653de32015-07-24 12:12:40 -07001154 if (operations.size() > 0 && !applyJoinOperations(resolver, operations)) {
1155 return;
1156 }
1157 showToast(R.string.contactsJoinedMessage);
1158 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001159
Walter Jang0653de32015-07-24 12:12:40 -07001160 /** Returns true if the batch was successfully applied and false otherwise. */
1161 private boolean applyJoinOperations(ContentResolver resolver,
1162 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001163 try {
1164 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001165 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001166 } catch (RemoteException | OperationApplicationException e) {
1167 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1168 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001169 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001170 }
1171 }
1172
1173
1174 private void joinContacts(Intent intent) {
1175 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1176 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001177
1178 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001179 // in the join UIs.
1180 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1181 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001182 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001183 return;
1184 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001185
Brian Attwell548f5c62015-01-27 17:46:46 -08001186 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001187
1188 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001189 for (int i = 0; i < rawContactIds.length; i++) {
1190 for (int j = 0; j < rawContactIds.length; j++) {
1191 if (i != j) {
1192 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1193 }
1194 }
1195 }
1196
Brian Attwelld3946ca2015-03-03 11:13:49 -08001197 final ContentResolver resolver = getContentResolver();
1198
Brian Attwell548f5c62015-01-27 17:46:46 -08001199 // Use the name for contactId1 as the name for the newly aggregated contact.
1200 final Uri contactId1Uri = ContentUris.withAppendedId(
1201 Contacts.CONTENT_URI, contactId1);
1202 final Uri entityUri = Uri.withAppendedPath(
1203 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1204 Cursor c = resolver.query(entityUri,
1205 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1206 if (c == null) {
1207 Log.e(TAG, "Unable to open Contacts DB cursor");
1208 showToast(R.string.contactSavedErrorToast);
1209 return;
1210 }
1211 long dataIdToAddSuperPrimary = -1;
1212 try {
1213 if (c.moveToFirst()) {
1214 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1215 }
1216 } finally {
1217 c.close();
1218 }
1219
1220 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1221 // display name does not change as a result of the join.
1222 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001223 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001224 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1225 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1226 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001227 operations.add(builder.build());
1228 }
1229
1230 boolean success = false;
1231 // Apply all aggregation exceptions as one batch
1232 try {
1233 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001234 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001235 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001236 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001237 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001238 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001239 }
1240
1241 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1242 if (success) {
1243 Uri uri = RawContacts.getContactLookupUri(resolver,
1244 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1245 callbackIntent.setData(uri);
1246 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001247 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001248 }
1249
Brian Attwelld3946ca2015-03-03 11:13:49 -08001250 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1251 if (contactIds == null) {
1252 return null;
1253 }
1254
Brian Attwell548f5c62015-01-27 17:46:46 -08001255 final ContentResolver resolver = getContentResolver();
1256 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001257
1258 final StringBuilder queryBuilder = new StringBuilder();
1259 final String stringContactIds[] = new String[contactIds.length];
1260 for (int i = 0; i < contactIds.length; i++) {
1261 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1262 stringContactIds[i] = String.valueOf(contactIds[i]);
1263 if (contactIds[i] == -1) {
1264 return null;
1265 }
1266 if (i == contactIds.length -1) {
1267 break;
1268 }
1269 queryBuilder.append(" OR ");
1270 }
1271
Brian Attwell548f5c62015-01-27 17:46:46 -08001272 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1273 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001274 queryBuilder.toString(),
1275 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001276 if (c == null) {
1277 Log.e(TAG, "Unable to open Contacts DB cursor");
1278 showToast(R.string.contactSavedErrorToast);
1279 return null;
1280 }
1281 try {
1282 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001283 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001284 return null;
1285 }
1286 rawContactIds = new long[c.getCount()];
1287 for (int i = 0; i < rawContactIds.length; i++) {
1288 c.moveToPosition(i);
1289 long rawContactId = c.getLong(JoinContactQuery._ID);
1290 rawContactIds[i] = rawContactId;
1291 }
1292 } finally {
1293 c.close();
1294 }
1295 return rawContactIds;
1296 }
1297
Brian Attwelld3946ca2015-03-03 11:13:49 -08001298 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1299 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1300 }
1301
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001302 /**
1303 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1304 */
1305 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1306 long rawContactId1, long rawContactId2) {
1307 Builder builder =
1308 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1309 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1310 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1311 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1312 operations.add(builder.build());
1313 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001314
1315 /**
1316 * Shows a toast on the UI thread.
1317 */
1318 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001319 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001320
1321 @Override
1322 public void run() {
1323 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1324 }
1325 });
1326 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001327
1328 private void deliverCallback(final Intent callbackIntent) {
1329 mMainHandler.post(new Runnable() {
1330
1331 @Override
1332 public void run() {
1333 deliverCallbackOnUiThread(callbackIntent);
1334 }
1335 });
1336 }
1337
1338 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1339 // TODO: this assumes that if there are multiple instances of the same
1340 // activity registered, the last one registered is the one waiting for
1341 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001342 for (Listener listener : sListeners) {
1343 if (callbackIntent.getComponent().equals(
1344 ((Activity) listener).getIntent().getComponent())) {
1345 listener.onServiceCompleted(callbackIntent);
1346 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001347 }
1348 }
1349 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001350}