blob: c3a7f24c072c1a96e9a16a061241bfafbb7ceff6 [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;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070045import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070046import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070047import android.provider.ContactsContract.RawContactsEntity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070048import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080049import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070050
Wenyi Wangdd7d4562015-12-08 13:33:43 -080051import com.android.contacts.activities.ContactEditorBaseActivity;
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;
Wenyi Wangaac0e662015-12-18 17:17:33 -080061import com.android.contacts.compat.PinnedPositionsCompat;
Yorke Lee637a38e2013-09-14 08:36:33 -070062import com.android.contacts.util.ContactPhotoUtils;
63
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070064import com.google.common.collect.Lists;
65import com.google.common.collect.Sets;
66
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080067import java.util.ArrayList;
68import java.util.HashSet;
69import java.util.List;
70import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070071
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080072/**
73 * A service responsible for saving changes to the content provider.
74 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070075public class ContactSaveService extends IntentService {
76 private static final String TAG = "ContactSaveService";
77
Katherine Kuana007e442011-07-07 09:25:34 -070078 /** Set to true in order to view logs on content provider operations */
79 private static final boolean DEBUG = false;
80
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070081 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
82
83 public static final String EXTRA_ACCOUNT_NAME = "accountName";
84 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070085 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070086 public static final String EXTRA_CONTENT_VALUES = "contentValues";
87 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
88
Dmitri Plotnikova0114142011-02-15 13:53:21 -080089 public static final String ACTION_SAVE_CONTACT = "saveContact";
90 public static final String EXTRA_CONTACT_STATE = "state";
91 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070092 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070093 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080094 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070095
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080096 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080097 public static final String ACTION_RENAME_GROUP = "renameGroup";
98 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070099 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800100 public static final String EXTRA_GROUP_ID = "groupId";
101 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700102 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
103 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800104
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800105 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800106 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800107 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800108 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800109 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800110 public static final String EXTRA_STARRED_FLAG = "starred";
111
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800112 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
113 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
114 public static final String EXTRA_DATA_ID = "dataId";
115
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800116 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800117 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800118 public static final String EXTRA_CONTACT_ID1 = "contactId1";
119 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800120
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700121 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
122 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
123
124 public static final String ACTION_SET_RINGTONE = "setRingtone";
125 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
126
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700127 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
128 Data.MIMETYPE,
129 Data.IS_PRIMARY,
130 Data.DATA1,
131 Data.DATA2,
132 Data.DATA3,
133 Data.DATA4,
134 Data.DATA5,
135 Data.DATA6,
136 Data.DATA7,
137 Data.DATA8,
138 Data.DATA9,
139 Data.DATA10,
140 Data.DATA11,
141 Data.DATA12,
142 Data.DATA13,
143 Data.DATA14,
144 Data.DATA15
145 );
146
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800147 private static final int PERSIST_TRIES = 3;
148
Walter Jang0653de32015-07-24 12:12:40 -0700149 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
150
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800151 public interface Listener {
152 public void onServiceCompleted(Intent callbackIntent);
153 }
154
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100155 private static final CopyOnWriteArrayList<Listener> sListeners =
156 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800157
158 private Handler mMainHandler;
159
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700160 public ContactSaveService() {
161 super(TAG);
162 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800163 mMainHandler = new Handler(Looper.getMainLooper());
164 }
165
166 public static void registerListener(Listener listener) {
167 if (!(listener instanceof Activity)) {
168 throw new ClassCastException("Only activities can be registered to"
169 + " receive callback from " + ContactSaveService.class.getName());
170 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100171 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800172 }
173
174 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100175 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700176 }
177
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800178 /**
179 * Returns true if the ContactSaveService was started successfully and false if an exception
180 * was thrown and a Toast error message was displayed.
181 */
182 public static boolean startService(Context context, Intent intent, int saveMode) {
183 try {
184 context.startService(intent);
185 } catch (Exception exception) {
186 final int resId;
187 switch (saveMode) {
188 case ContactEditorBaseActivity.ContactEditor.SaveMode.SPLIT:
189 resId = R.string.contactUnlinkErrorToast;
190 break;
191 case ContactEditorBaseActivity.ContactEditor.SaveMode.RELOAD:
192 resId = R.string.contactJoinErrorToast;
193 break;
194 case ContactEditorBaseActivity.ContactEditor.SaveMode.CLOSE:
195 resId = R.string.contactSavedErrorToast;
196 break;
197 default:
198 resId = R.string.contactGenericErrorToast;
199 }
200 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
201 return false;
202 }
203 return true;
204 }
205
206 /**
207 * Utility method that starts service and handles exception.
208 */
209 public static void startService(Context context, Intent intent) {
210 try {
211 context.startService(intent);
212 } catch (Exception exception) {
213 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
214 }
215 }
216
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700217 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800218 public Object getSystemService(String name) {
219 Object service = super.getSystemService(name);
220 if (service != null) {
221 return service;
222 }
223
224 return getApplicationContext().getSystemService(name);
225 }
226
227 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700228 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800229 if (intent == null) {
230 Log.d(TAG, "onHandleIntent: could not handle null intent");
231 return;
232 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700233 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
234 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
235 // TODO: add more specific error string such as "Turn on Contacts
236 // permission to update your contacts"
237 showToast(R.string.contactSavedErrorToast);
238 return;
239 }
240
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700241 // Call an appropriate method. If we're sure it affects how incoming phone calls are
242 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700243 String action = intent.getAction();
244 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
245 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800246 } else if (ACTION_SAVE_CONTACT.equals(action)) {
247 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800248 } else if (ACTION_CREATE_GROUP.equals(action)) {
249 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800250 } else if (ACTION_RENAME_GROUP.equals(action)) {
251 renameGroup(intent);
252 } else if (ACTION_DELETE_GROUP.equals(action)) {
253 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700254 } else if (ACTION_UPDATE_GROUP.equals(action)) {
255 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800256 } else if (ACTION_SET_STARRED.equals(action)) {
257 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800258 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
259 setSuperPrimary(intent);
260 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
261 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800262 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
263 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800264 } else if (ACTION_DELETE_CONTACT.equals(action)) {
265 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800266 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
267 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800268 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
269 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700270 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
271 setSendToVoicemail(intent);
272 } else if (ACTION_SET_RINGTONE.equals(action)) {
273 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700274 }
275 }
276
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800277 /**
278 * Creates an intent that can be sent to this service to create a new raw contact
279 * using data presented as a set of ContentValues.
280 */
281 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700282 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700283 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800284 Intent serviceIntent = new Intent(
285 context, ContactSaveService.class);
286 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
287 if (account != null) {
288 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
289 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700290 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800291 }
292 serviceIntent.putParcelableArrayListExtra(
293 ContactSaveService.EXTRA_CONTENT_VALUES, values);
294
295 // Callback intent will be invoked by the service once the new contact is
296 // created. The service will put the URI of the new contact as "data" on
297 // the callback intent.
298 Intent callbackIntent = new Intent(context, callbackActivity);
299 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800300 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
301 return serviceIntent;
302 }
303
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700304 private void createRawContact(Intent intent) {
305 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
306 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700307 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700308 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
309 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
310
311 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
312 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
313 .withValue(RawContacts.ACCOUNT_NAME, accountName)
314 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700315 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700316 .build());
317
318 int size = valueList.size();
319 for (int i = 0; i < size; i++) {
320 ContentValues values = valueList.get(i);
321 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
322 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
323 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
324 .withValues(values)
325 .build());
326 }
327
328 ContentResolver resolver = getContentResolver();
329 ContentProviderResult[] results;
330 try {
331 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
332 } catch (Exception e) {
333 throw new RuntimeException("Failed to store new contact", e);
334 }
335
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700336 Uri rawContactUri = results[0].uri;
337 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
338
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800339 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700340 }
341
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700342 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800343 * Creates an intent that can be sent to this service to create a new raw contact
344 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800345 * This variant is more convenient to use when there is only one photo that can
346 * possibly be updated, as in the Contact Details screen.
347 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
348 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800349 */
Maurice Chu851222a2012-06-21 11:43:08 -0700350 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700351 String saveModeExtraKey, int saveMode, boolean isProfile,
352 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700353 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800354 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700355 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800356 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700357 callbackActivity, callbackAction, bundle,
358 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800359 }
360
361 /**
362 * Creates an intent that can be sent to this service to create a new raw contact
363 * using data presented as a set of ContentValues.
364 * This variant is used when multiple contacts' photos may be updated, as in the
365 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700366 *
Josh Garguse692e012012-01-18 14:53:11 -0800367 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700368 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
369 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800370 */
Maurice Chu851222a2012-06-21 11:43:08 -0700371 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700372 String saveModeExtraKey, int saveMode, boolean isProfile,
373 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700374 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800375 Intent serviceIntent = new Intent(
376 context, ContactSaveService.class);
377 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
378 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700379 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800380 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
381
Josh Garguse692e012012-01-18 14:53:11 -0800382 if (updatedPhotos != null) {
383 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
384 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800385
Josh Garguse5d3f892012-04-11 11:56:15 -0700386 if (callbackActivity != null) {
387 // Callback intent will be invoked by the service once the contact is
388 // saved. The service will put the URI of the new contact as "data" on
389 // the callback intent.
390 Intent callbackIntent = new Intent(context, callbackActivity);
391 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700392 if (joinContactIdExtraKey != null && joinContactId != null) {
393 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
394 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700395 callbackIntent.setAction(callbackAction);
396 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
397 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800398 return serviceIntent;
399 }
400
401 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700402 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700403 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800404 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800405
Jay Shrauner08099782015-03-25 14:17:11 -0700406 if (state == null) {
407 Log.e(TAG, "Invalid arguments for saveContact request");
408 return;
409 }
410
benny.lin3a4e7a22014-01-08 10:58:08 +0800411 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800412 // Trim any empty fields, and RawContacts, before persisting
413 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700414 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800415
416 Uri lookupUri = null;
417
418 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700419
Josh Garguse692e012012-01-18 14:53:11 -0800420 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800421
Josh Gargusef15c8e2012-01-30 16:42:02 -0800422 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
423 long insertedRawContactId = -1;
424
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800425 // Attempt to persist changes
426 int tries = 0;
427 while (tries++ < PERSIST_TRIES) {
428 try {
429 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800430 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
431
432 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
433
434 for (CPOWrapper cpoWrapper : diffWrapper) {
435 diff.add(cpoWrapper.getOperation());
436 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700437
Katherine Kuana007e442011-07-07 09:25:34 -0700438 if (DEBUG) {
439 Log.v(TAG, "Content Provider Operations:");
440 for (ContentProviderOperation operation : diff) {
441 Log.v(TAG, operation.toString());
442 }
443 }
444
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700445 int numberProcessed = 0;
446 boolean batchFailed = false;
447 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
448 while (numberProcessed < diff.size()) {
449 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
450 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700451 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700452 batchFailed = true;
453 break;
454 } else {
455 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700456 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800457 }
458
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700459 if (batchFailed) {
460 // Retry save
461 continue;
462 }
463
Wenyi Wang67addcc2015-11-23 10:07:48 -0800464 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800465 if (rawContactId == -1) {
466 throw new IllegalStateException("Could not determine RawContact ID after save");
467 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800468 // We don't have to check to see if the value is still -1. If we reach here,
469 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800470 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700471 if (isProfile) {
472 // Since the profile supports local raw contacts, which may have been completely
473 // removed if all information was removed, we need to do a special query to
474 // get the lookup URI for the profile contact (if it still exists).
475 Cursor c = resolver.query(Profile.CONTENT_URI,
476 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
477 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800478 if (c == null) {
479 continue;
480 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700481 try {
Erik162b7e32011-09-20 15:23:55 -0700482 if (c.moveToFirst()) {
483 final long contactId = c.getLong(0);
484 final String lookupKey = c.getString(1);
485 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
486 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700487 } finally {
488 c.close();
489 }
490 } else {
491 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
492 rawContactId);
493 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
494 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800495 if (lookupUri != null) {
496 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
497 }
Josh Garguse692e012012-01-18 14:53:11 -0800498
499 // We can change this back to false later, if we fail to save the contact photo.
500 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800501 break;
502
503 } catch (RemoteException e) {
504 // Something went wrong, bail without success
505 Log.e(TAG, "Problem persisting user edits", e);
506 break;
507
Jay Shrauner57fca182014-01-17 14:20:50 -0800508 } catch (IllegalArgumentException e) {
509 // This is thrown by applyBatch on malformed requests
510 Log.e(TAG, "Problem persisting user edits", e);
511 showToast(R.string.contactSavedErrorToast);
512 break;
513
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800514 } catch (OperationApplicationException e) {
515 // Version consistency failed, re-parent change and try again
516 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
517 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
518 boolean first = true;
519 final int count = state.size();
520 for (int i = 0; i < count; i++) {
521 Long rawContactId = state.getRawContactId(i);
522 if (rawContactId != null && rawContactId != -1) {
523 if (!first) {
524 sb.append(',');
525 }
526 sb.append(rawContactId);
527 first = false;
528 }
529 }
530 sb.append(")");
531
532 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800533 throw new IllegalStateException(
534 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800535 }
536
Maurice Chu851222a2012-06-21 11:43:08 -0700537 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700538 isProfile
539 ? RawContactsEntity.PROFILE_CONTENT_URI
540 : RawContactsEntity.CONTENT_URI,
541 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700542 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700543
544 // Update the new state to use profile URIs if appropriate.
545 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700546 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700547 delta.setProfileQueryUri();
548 }
549 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800550 }
551 }
552
Josh Garguse692e012012-01-18 14:53:11 -0800553 // Now save any updated photos. We do this at the end to ensure that
554 // the ContactProvider already knows about newly-created contacts.
555 if (updatedPhotos != null) {
556 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700557 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800558 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800559
560 // If the raw-contact ID is negative, we are saving a new raw-contact;
561 // replace the bogus ID with the new one that we actually saved the contact at.
562 if (rawContactId < 0) {
563 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800564 }
565
Jay Shrauner511561d2015-04-02 10:35:33 -0700566 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700567 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700568 succeeded = false;
569 }
Josh Garguse692e012012-01-18 14:53:11 -0800570 }
571 }
572
Josh Garguse5d3f892012-04-11 11:56:15 -0700573 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
574 if (callbackIntent != null) {
575 if (succeeded) {
576 // Mark the intent to indicate that the save was successful (even if the lookup URI
577 // is now null). For local contacts or the local profile, it's possible that the
578 // save triggered removal of the contact, so no lookup URI would exist..
579 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
580 }
581 callbackIntent.setData(lookupUri);
582 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800583 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800584 }
585
Josh Garguse692e012012-01-18 14:53:11 -0800586 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700587 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
588 * subsets, adds the returned array to "results".
589 *
590 * @return the size of the array, if not null; -1 when the array is null.
591 */
592 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
593 ContentProviderResult[] results, ContentResolver resolver)
594 throws RemoteException, OperationApplicationException {
595 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
596 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
597 subset.addAll(diff.subList(offset, offset + subsetCount));
598 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
599 .AUTHORITY, subset);
600 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
601 return -1;
602 }
603 for (ContentProviderResult c : subsetResult) {
604 results[offset++] = c;
605 }
606 return subsetResult.length;
607 }
608
609 /**
Josh Garguse692e012012-01-18 14:53:11 -0800610 * Save updated photo for the specified raw-contact.
611 * @return true for success, false for failure
612 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800613 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800614 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800615 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
616 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
617
benny.lin3a4e7a22014-01-08 10:58:08 +0800618 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800619 }
620
Josh Gargusef15c8e2012-01-30 16:42:02 -0800621 /**
622 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
623 */
Maurice Chu851222a2012-06-21 11:43:08 -0700624 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800625 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800626 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800627 long existingRawContactId = state.findRawContactId();
628 if (existingRawContactId != -1) {
629 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800630 }
631
Wenyi Wang67addcc2015-11-23 10:07:48 -0800632 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800633 }
634
635 /**
636 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
637 */
638 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800639 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800640 if (results == null) {
641 return -1;
642 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800643 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800644 final int numResults = results.length;
645 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800646 final CPOWrapper cpoWrapper = diffWrapper.get(i);
647 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
648 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
649 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800650 return ContentUris.parseId(results[i].uri);
651 }
652 }
653 return -1;
654 }
655
656 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700657 * Creates an intent that can be sent to this service to create a new group as
658 * well as add new members at the same time.
659 *
660 * @param context of the application
661 * @param account in which the group should be created
662 * @param label is the name of the group (cannot be null)
663 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
664 * should be added to the group
665 * @param callbackActivity is the activity to send the callback intent to
666 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700667 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700668 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700669 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700670 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800671 Intent serviceIntent = new Intent(context, ContactSaveService.class);
672 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
673 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
674 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700675 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800676 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700677 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700678
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800679 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700680 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800681 Intent callbackIntent = new Intent(context, callbackActivity);
682 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700683 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800684
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700685 return serviceIntent;
686 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800687
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800688 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700689 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
690 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
691 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
692 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700693 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800694
695 ContentValues values = new ContentValues();
696 values.put(Groups.ACCOUNT_TYPE, accountType);
697 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700698 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800699 values.put(Groups.TITLE, label);
700
Katherine Kuan717e3432011-07-13 17:03:24 -0700701 final ContentResolver resolver = getContentResolver();
702
703 // Create the new group
704 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
705
706 // If there's no URI, then the insertion failed. Abort early because group members can't be
707 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800708 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700709 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800710 return;
711 }
712
Katherine Kuan717e3432011-07-13 17:03:24 -0700713 // Add new group members
714 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
715
716 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
717 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800718 values.clear();
719 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
720 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
721
722 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700723 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700724 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800725 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800726 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800727 }
728
729 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800730 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800731 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700732 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700733 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800734 Intent serviceIntent = new Intent(context, ContactSaveService.class);
735 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
736 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
737 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700738
739 // Callback intent will be invoked by the service once the group is renamed.
740 Intent callbackIntent = new Intent(context, callbackActivity);
741 callbackIntent.setAction(callbackAction);
742 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
743
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800744 return serviceIntent;
745 }
746
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800747 private void renameGroup(Intent intent) {
748 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
749 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
750
751 if (groupId == -1) {
752 Log.e(TAG, "Invalid arguments for renameGroup request");
753 return;
754 }
755
756 ContentValues values = new ContentValues();
757 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700758 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
759 getContentResolver().update(groupUri, values, null, null);
760
761 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
762 callbackIntent.setData(groupUri);
763 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800764 }
765
766 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800767 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800768 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800769 public static Intent createGroupDeletionIntent(Context context, long groupId) {
770 Intent serviceIntent = new Intent(context, ContactSaveService.class);
771 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800772 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800773 return serviceIntent;
774 }
775
776 private void deleteGroup(Intent intent) {
777 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
778 if (groupId == -1) {
779 Log.e(TAG, "Invalid arguments for deleteGroup request");
780 return;
781 }
782
783 getContentResolver().delete(
784 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
785 }
786
787 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700788 * Creates an intent that can be sent to this service to rename a group as
789 * well as add and remove members from the group.
790 *
791 * @param context of the application
792 * @param groupId of the group that should be modified
793 * @param newLabel is the updated name of the group (can be null if the name
794 * should not be updated)
795 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
796 * should be added to the group
797 * @param rawContactsToRemove is an array of raw contact IDs for contacts
798 * that should be removed from the group
799 * @param callbackActivity is the activity to send the callback intent to
800 * @param callbackAction is the intent action for the callback intent
801 */
802 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
803 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700804 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700805 Intent serviceIntent = new Intent(context, ContactSaveService.class);
806 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
807 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
808 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
809 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
810 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
811 rawContactsToRemove);
812
813 // Callback intent will be invoked by the service once the group is updated
814 Intent callbackIntent = new Intent(context, callbackActivity);
815 callbackIntent.setAction(callbackAction);
816 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
817
818 return serviceIntent;
819 }
820
821 private void updateGroup(Intent intent) {
822 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
823 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
824 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
825 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
826
827 if (groupId == -1) {
828 Log.e(TAG, "Invalid arguments for updateGroup request");
829 return;
830 }
831
832 final ContentResolver resolver = getContentResolver();
833 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
834
835 // Update group name if necessary
836 if (label != null) {
837 ContentValues values = new ContentValues();
838 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700839 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700840 }
841
Katherine Kuan717e3432011-07-13 17:03:24 -0700842 // Add and remove members if necessary
843 addMembersToGroup(resolver, rawContactsToAdd, groupId);
844 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
845
846 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
847 callbackIntent.setData(groupUri);
848 deliverCallback(callbackIntent);
849 }
850
Daniel Lehmann18958a22012-02-28 17:45:25 -0800851 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700852 long groupId) {
853 if (rawContactsToAdd == null) {
854 return;
855 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700856 for (long rawContactId : rawContactsToAdd) {
857 try {
858 final ArrayList<ContentProviderOperation> rawContactOperations =
859 new ArrayList<ContentProviderOperation>();
860
861 // Build an assert operation to ensure the contact is not already in the group
862 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
863 .newAssertQuery(Data.CONTENT_URI);
864 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
865 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
866 new String[] { String.valueOf(rawContactId),
867 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
868 assertBuilder.withExpectedCount(0);
869 rawContactOperations.add(assertBuilder.build());
870
871 // Build an insert operation to add the contact to the group
872 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
873 .newInsert(Data.CONTENT_URI);
874 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
875 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
876 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
877 rawContactOperations.add(insertBuilder.build());
878
879 if (DEBUG) {
880 for (ContentProviderOperation operation : rawContactOperations) {
881 Log.v(TAG, operation.toString());
882 }
883 }
884
885 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700886 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800887 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700888 }
889 } catch (RemoteException e) {
890 // Something went wrong, bail without success
891 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
892 String.valueOf(rawContactId), e);
893 } catch (OperationApplicationException e) {
894 // The assert could have failed because the contact is already in the group,
895 // just continue to the next contact
896 Log.w(TAG, "Assert failed in adding raw contact ID " +
897 String.valueOf(rawContactId) + ". Already exists in group " +
898 String.valueOf(groupId), e);
899 }
900 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700901 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700902
Daniel Lehmann18958a22012-02-28 17:45:25 -0800903 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700904 long groupId) {
905 if (rawContactsToRemove == null) {
906 return;
907 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700908 for (long rawContactId : rawContactsToRemove) {
909 // Apply the delete operation on the data row for the given raw contact's
910 // membership in the given group. If no contact matches the provided selection, then
911 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800912 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700913 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
914 new String[] { String.valueOf(rawContactId),
915 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
916 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700917 }
918
919 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800920 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800921 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800922 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
923 Intent serviceIntent = new Intent(context, ContactSaveService.class);
924 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
925 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
926 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
927
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800928 return serviceIntent;
929 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800930
931 private void setStarred(Intent intent) {
932 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
933 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
934 if (contactUri == null) {
935 Log.e(TAG, "Invalid arguments for setStarred request");
936 return;
937 }
938
939 final ContentValues values = new ContentValues(1);
940 values.put(Contacts.STARRED, value);
941 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700942
943 // Undemote the contact if necessary
944 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
945 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800946 if (c == null) {
947 return;
948 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700949 try {
950 if (c.moveToFirst()) {
951 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700952
953 // Don't bother undemoting if this contact is the user's profile.
954 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -0800955 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700956 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700957 }
958 } finally {
959 c.close();
960 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800961 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800962
963 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700964 * Creates an intent that can be sent to this service to set the redirect to voicemail.
965 */
966 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
967 boolean value) {
968 Intent serviceIntent = new Intent(context, ContactSaveService.class);
969 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
970 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
971 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
972
973 return serviceIntent;
974 }
975
976 private void setSendToVoicemail(Intent intent) {
977 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
978 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
979 if (contactUri == null) {
980 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
981 return;
982 }
983
984 final ContentValues values = new ContentValues(1);
985 values.put(Contacts.SEND_TO_VOICEMAIL, value);
986 getContentResolver().update(contactUri, values, null, null);
987 }
988
989 /**
990 * Creates an intent that can be sent to this service to save the contact's ringtone.
991 */
992 public static Intent createSetRingtone(Context context, Uri contactUri,
993 String value) {
994 Intent serviceIntent = new Intent(context, ContactSaveService.class);
995 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
996 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
997 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
998
999 return serviceIntent;
1000 }
1001
1002 private void setRingtone(Intent intent) {
1003 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1004 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1005 if (contactUri == null) {
1006 Log.e(TAG, "Invalid arguments for setRingtone");
1007 return;
1008 }
1009 ContentValues values = new ContentValues(1);
1010 values.put(Contacts.CUSTOM_RINGTONE, value);
1011 getContentResolver().update(contactUri, values, null, null);
1012 }
1013
1014 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001015 * Creates an intent that sets the selected data item as super primary (default)
1016 */
1017 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1018 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1019 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1020 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1021 return serviceIntent;
1022 }
1023
1024 private void setSuperPrimary(Intent intent) {
1025 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1026 if (dataId == -1) {
1027 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1028 return;
1029 }
1030
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001031 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001032 }
1033
1034 /**
1035 * Creates an intent that clears the primary flag of all data items that belong to the same
1036 * raw_contact as the given data item. Will only clear, if the data item was primary before
1037 * this call
1038 */
1039 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1040 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1041 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1042 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1043 return serviceIntent;
1044 }
1045
1046 private void clearPrimary(Intent intent) {
1047 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1048 if (dataId == -1) {
1049 Log.e(TAG, "Invalid arguments for clearPrimary request");
1050 return;
1051 }
1052
1053 // Update the primary values in the data record.
1054 ContentValues values = new ContentValues(1);
1055 values.put(Data.IS_SUPER_PRIMARY, 0);
1056 values.put(Data.IS_PRIMARY, 0);
1057
1058 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1059 values, null, null);
1060 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001061
1062 /**
1063 * Creates an intent that can be sent to this service to delete a contact.
1064 */
1065 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1066 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1067 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1068 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1069 return serviceIntent;
1070 }
1071
Brian Attwelld2962a32015-03-02 14:48:50 -08001072 /**
1073 * Creates an intent that can be sent to this service to delete multiple contacts.
1074 */
1075 public static Intent createDeleteMultipleContactsIntent(Context context,
1076 long[] contactIds) {
1077 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1078 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1079 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1080 return serviceIntent;
1081 }
1082
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001083 private void deleteContact(Intent intent) {
1084 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1085 if (contactUri == null) {
1086 Log.e(TAG, "Invalid arguments for deleteContact request");
1087 return;
1088 }
1089
1090 getContentResolver().delete(contactUri, null, null);
1091 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001092
Brian Attwelld2962a32015-03-02 14:48:50 -08001093 private void deleteMultipleContacts(Intent intent) {
1094 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1095 if (contactIds == null) {
1096 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1097 return;
1098 }
1099 for (long contactId : contactIds) {
1100 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1101 getContentResolver().delete(contactUri, null, null);
1102 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001103 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1104 .contacts_deleted_toast, contactIds.length);
1105 mMainHandler.post(new Runnable() {
1106 @Override
1107 public void run() {
1108 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1109 .show();
1110 }
1111 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001112 }
1113
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001114 /**
1115 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001116 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001117 */
1118 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001119 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001120 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1121 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1122 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1123 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001124
1125 // Callback intent will be invoked by the service once the contacts are joined.
1126 Intent callbackIntent = new Intent(context, callbackActivity);
1127 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001128 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1129
1130 return serviceIntent;
1131 }
1132
Brian Attwelld3946ca2015-03-03 11:13:49 -08001133 /**
1134 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1135 * No special attention is paid to where the resulting contact's name is taken from.
1136 */
1137 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1138 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1139 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1140 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1141 return serviceIntent;
1142 }
1143
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001144
1145 private interface JoinContactQuery {
1146 String[] PROJECTION = {
1147 RawContacts._ID,
1148 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001149 RawContacts.DISPLAY_NAME_SOURCE,
1150 };
1151
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001152 int _ID = 0;
1153 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001154 int DISPLAY_NAME_SOURCE = 2;
1155 }
1156
1157 private interface ContactEntityQuery {
1158 String[] PROJECTION = {
1159 Contacts.Entity.DATA_ID,
1160 Contacts.Entity.CONTACT_ID,
1161 Contacts.Entity.IS_SUPER_PRIMARY,
1162 };
1163 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1164 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1165 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1166 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1167
1168 int DATA_ID = 0;
1169 int CONTACT_ID = 1;
1170 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001171 }
1172
Brian Attwelld3946ca2015-03-03 11:13:49 -08001173 private void joinSeveralContacts(Intent intent) {
1174 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001175
Brian Attwelld3946ca2015-03-03 11:13:49 -08001176 // Load raw contact IDs for all contacts involved.
1177 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1178 if (rawContactIds == null) {
1179 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001180 return;
1181 }
1182
Brian Attwelld3946ca2015-03-03 11:13:49 -08001183 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001184 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001185 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1186 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1187 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001188 for (int i = 0; i < rawContactIds.length; i++) {
1189 for (int j = 0; j < rawContactIds.length; j++) {
1190 if (i != j) {
1191 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1192 }
Walter Jang0653de32015-07-24 12:12:40 -07001193 // Before we get to 500 we need to flush the operations list
1194 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1195 if (!applyJoinOperations(resolver, operations)) {
1196 return;
1197 }
1198 operations.clear();
1199 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001200 }
1201 }
Walter Jang0653de32015-07-24 12:12:40 -07001202 if (operations.size() > 0 && !applyJoinOperations(resolver, operations)) {
1203 return;
1204 }
1205 showToast(R.string.contactsJoinedMessage);
1206 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001207
Walter Jang0653de32015-07-24 12:12:40 -07001208 /** Returns true if the batch was successfully applied and false otherwise. */
1209 private boolean applyJoinOperations(ContentResolver resolver,
1210 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001211 try {
1212 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001213 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001214 } catch (RemoteException | OperationApplicationException e) {
1215 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1216 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001217 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001218 }
1219 }
1220
1221
1222 private void joinContacts(Intent intent) {
1223 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1224 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001225
1226 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001227 // in the join UIs.
1228 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1229 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001230 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001231 return;
1232 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001233
Brian Attwell548f5c62015-01-27 17:46:46 -08001234 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001235
1236 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001237 for (int i = 0; i < rawContactIds.length; i++) {
1238 for (int j = 0; j < rawContactIds.length; j++) {
1239 if (i != j) {
1240 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1241 }
1242 }
1243 }
1244
Brian Attwelld3946ca2015-03-03 11:13:49 -08001245 final ContentResolver resolver = getContentResolver();
1246
Brian Attwell548f5c62015-01-27 17:46:46 -08001247 // Use the name for contactId1 as the name for the newly aggregated contact.
1248 final Uri contactId1Uri = ContentUris.withAppendedId(
1249 Contacts.CONTENT_URI, contactId1);
1250 final Uri entityUri = Uri.withAppendedPath(
1251 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1252 Cursor c = resolver.query(entityUri,
1253 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1254 if (c == null) {
1255 Log.e(TAG, "Unable to open Contacts DB cursor");
1256 showToast(R.string.contactSavedErrorToast);
1257 return;
1258 }
1259 long dataIdToAddSuperPrimary = -1;
1260 try {
1261 if (c.moveToFirst()) {
1262 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1263 }
1264 } finally {
1265 c.close();
1266 }
1267
1268 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1269 // display name does not change as a result of the join.
1270 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001271 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001272 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1273 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1274 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001275 operations.add(builder.build());
1276 }
1277
1278 boolean success = false;
1279 // Apply all aggregation exceptions as one batch
1280 try {
1281 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001282 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001283 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001284 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001285 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001286 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001287 }
1288
1289 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1290 if (success) {
1291 Uri uri = RawContacts.getContactLookupUri(resolver,
1292 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1293 callbackIntent.setData(uri);
1294 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001295 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001296 }
1297
Brian Attwelld3946ca2015-03-03 11:13:49 -08001298 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1299 if (contactIds == null) {
1300 return null;
1301 }
1302
Brian Attwell548f5c62015-01-27 17:46:46 -08001303 final ContentResolver resolver = getContentResolver();
1304 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001305
1306 final StringBuilder queryBuilder = new StringBuilder();
1307 final String stringContactIds[] = new String[contactIds.length];
1308 for (int i = 0; i < contactIds.length; i++) {
1309 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1310 stringContactIds[i] = String.valueOf(contactIds[i]);
1311 if (contactIds[i] == -1) {
1312 return null;
1313 }
1314 if (i == contactIds.length -1) {
1315 break;
1316 }
1317 queryBuilder.append(" OR ");
1318 }
1319
Brian Attwell548f5c62015-01-27 17:46:46 -08001320 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1321 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001322 queryBuilder.toString(),
1323 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001324 if (c == null) {
1325 Log.e(TAG, "Unable to open Contacts DB cursor");
1326 showToast(R.string.contactSavedErrorToast);
1327 return null;
1328 }
1329 try {
1330 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001331 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001332 return null;
1333 }
1334 rawContactIds = new long[c.getCount()];
1335 for (int i = 0; i < rawContactIds.length; i++) {
1336 c.moveToPosition(i);
1337 long rawContactId = c.getLong(JoinContactQuery._ID);
1338 rawContactIds[i] = rawContactId;
1339 }
1340 } finally {
1341 c.close();
1342 }
1343 return rawContactIds;
1344 }
1345
Brian Attwelld3946ca2015-03-03 11:13:49 -08001346 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1347 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1348 }
1349
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001350 /**
1351 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1352 */
1353 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1354 long rawContactId1, long rawContactId2) {
1355 Builder builder =
1356 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1357 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1358 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1359 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1360 operations.add(builder.build());
1361 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001362
1363 /**
1364 * Shows a toast on the UI thread.
1365 */
1366 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001367 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001368
1369 @Override
1370 public void run() {
1371 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1372 }
1373 });
1374 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001375
1376 private void deliverCallback(final Intent callbackIntent) {
1377 mMainHandler.post(new Runnable() {
1378
1379 @Override
1380 public void run() {
1381 deliverCallbackOnUiThread(callbackIntent);
1382 }
1383 });
1384 }
1385
1386 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1387 // TODO: this assumes that if there are multiple instances of the same
1388 // activity registered, the last one registered is the one waiting for
1389 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001390 for (Listener listener : sListeners) {
1391 if (callbackIntent.getComponent().equals(
1392 ((Activity) listener).getIntent().getComponent())) {
1393 listener.onServiceCompleted(callbackIntent);
1394 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001395 }
1396 }
1397 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001398}