blob: 10f8f9c298ab678e230c797ea9e8fd3aeefb0b19 [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;
Wenyi Wange5bac732016-02-03 11:55:21 -080059import com.android.contacts.common.model.account.AccountType;
Chiao Cheng428f0082012-11-13 18:38:56 -080060import com.android.contacts.common.model.account.AccountWithDataSet;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070061import com.android.contacts.common.util.PermissionsUtil;
Wenyi Wangaac0e662015-12-18 17:17:33 -080062import com.android.contacts.compat.PinnedPositionsCompat;
Yorke Lee637a38e2013-09-14 08:36:33 -070063import com.android.contacts.util.ContactPhotoUtils;
64
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070065import com.google.common.collect.Lists;
66import com.google.common.collect.Sets;
67
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080068import java.util.ArrayList;
69import java.util.HashSet;
Wenyi Wange5bac732016-02-03 11:55:21 -080070import java.util.Iterator;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080071import java.util.List;
72import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070073
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080074/**
75 * A service responsible for saving changes to the content provider.
76 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070077public class ContactSaveService extends IntentService {
78 private static final String TAG = "ContactSaveService";
79
Katherine Kuana007e442011-07-07 09:25:34 -070080 /** Set to true in order to view logs on content provider operations */
81 private static final boolean DEBUG = false;
82
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070083 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
84
85 public static final String EXTRA_ACCOUNT_NAME = "accountName";
86 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070087 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070088 public static final String EXTRA_CONTENT_VALUES = "contentValues";
89 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
90
Dmitri Plotnikova0114142011-02-15 13:53:21 -080091 public static final String ACTION_SAVE_CONTACT = "saveContact";
92 public static final String EXTRA_CONTACT_STATE = "state";
93 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070094 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070095 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080096 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070097
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080098 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080099 public static final String ACTION_RENAME_GROUP = "renameGroup";
100 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700101 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800102 public static final String EXTRA_GROUP_ID = "groupId";
103 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700104 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
105 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800106
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800107 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800108 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800109 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800110 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800111 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800112 public static final String EXTRA_STARRED_FLAG = "starred";
113
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800114 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
115 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
116 public static final String EXTRA_DATA_ID = "dataId";
117
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800118 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800119 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800120 public static final String EXTRA_CONTACT_ID1 = "contactId1";
121 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800122
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700123 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
124 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
125
126 public static final String ACTION_SET_RINGTONE = "setRingtone";
127 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
128
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700129 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
130 Data.MIMETYPE,
131 Data.IS_PRIMARY,
132 Data.DATA1,
133 Data.DATA2,
134 Data.DATA3,
135 Data.DATA4,
136 Data.DATA5,
137 Data.DATA6,
138 Data.DATA7,
139 Data.DATA8,
140 Data.DATA9,
141 Data.DATA10,
142 Data.DATA11,
143 Data.DATA12,
144 Data.DATA13,
145 Data.DATA14,
146 Data.DATA15
147 );
148
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800149 private static final int PERSIST_TRIES = 3;
150
Walter Jang0653de32015-07-24 12:12:40 -0700151 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
152
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800153 public interface Listener {
154 public void onServiceCompleted(Intent callbackIntent);
155 }
156
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100157 private static final CopyOnWriteArrayList<Listener> sListeners =
158 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800159
160 private Handler mMainHandler;
161
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700162 public ContactSaveService() {
163 super(TAG);
164 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800165 mMainHandler = new Handler(Looper.getMainLooper());
166 }
167
168 public static void registerListener(Listener listener) {
169 if (!(listener instanceof Activity)) {
170 throw new ClassCastException("Only activities can be registered to"
171 + " receive callback from " + ContactSaveService.class.getName());
172 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100173 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800174 }
175
176 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100177 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700178 }
179
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800180 /**
181 * Returns true if the ContactSaveService was started successfully and false if an exception
182 * was thrown and a Toast error message was displayed.
183 */
184 public static boolean startService(Context context, Intent intent, int saveMode) {
185 try {
186 context.startService(intent);
187 } catch (Exception exception) {
188 final int resId;
189 switch (saveMode) {
190 case ContactEditorBaseActivity.ContactEditor.SaveMode.SPLIT:
191 resId = R.string.contactUnlinkErrorToast;
192 break;
193 case ContactEditorBaseActivity.ContactEditor.SaveMode.RELOAD:
194 resId = R.string.contactJoinErrorToast;
195 break;
196 case ContactEditorBaseActivity.ContactEditor.SaveMode.CLOSE:
197 resId = R.string.contactSavedErrorToast;
198 break;
199 default:
200 resId = R.string.contactGenericErrorToast;
201 }
202 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
203 return false;
204 }
205 return true;
206 }
207
208 /**
209 * Utility method that starts service and handles exception.
210 */
211 public static void startService(Context context, Intent intent) {
212 try {
213 context.startService(intent);
214 } catch (Exception exception) {
215 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
216 }
217 }
218
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700219 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800220 public Object getSystemService(String name) {
221 Object service = super.getSystemService(name);
222 if (service != null) {
223 return service;
224 }
225
226 return getApplicationContext().getSystemService(name);
227 }
228
229 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700230 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800231 if (intent == null) {
232 Log.d(TAG, "onHandleIntent: could not handle null intent");
233 return;
234 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700235 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
236 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
237 // TODO: add more specific error string such as "Turn on Contacts
238 // permission to update your contacts"
239 showToast(R.string.contactSavedErrorToast);
240 return;
241 }
242
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700243 // Call an appropriate method. If we're sure it affects how incoming phone calls are
244 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700245 String action = intent.getAction();
246 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
247 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800248 } else if (ACTION_SAVE_CONTACT.equals(action)) {
249 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800250 } else if (ACTION_CREATE_GROUP.equals(action)) {
251 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800252 } else if (ACTION_RENAME_GROUP.equals(action)) {
253 renameGroup(intent);
254 } else if (ACTION_DELETE_GROUP.equals(action)) {
255 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700256 } else if (ACTION_UPDATE_GROUP.equals(action)) {
257 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800258 } else if (ACTION_SET_STARRED.equals(action)) {
259 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800260 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
261 setSuperPrimary(intent);
262 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
263 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800264 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
265 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800266 } else if (ACTION_DELETE_CONTACT.equals(action)) {
267 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800268 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
269 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800270 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
271 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700272 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
273 setSendToVoicemail(intent);
274 } else if (ACTION_SET_RINGTONE.equals(action)) {
275 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700276 }
277 }
278
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800279 /**
280 * Creates an intent that can be sent to this service to create a new raw contact
281 * using data presented as a set of ContentValues.
282 */
283 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700284 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700285 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800286 Intent serviceIntent = new Intent(
287 context, ContactSaveService.class);
288 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
289 if (account != null) {
290 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
291 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700292 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800293 }
294 serviceIntent.putParcelableArrayListExtra(
295 ContactSaveService.EXTRA_CONTENT_VALUES, values);
296
297 // Callback intent will be invoked by the service once the new contact is
298 // created. The service will put the URI of the new contact as "data" on
299 // the callback intent.
300 Intent callbackIntent = new Intent(context, callbackActivity);
301 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800302 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
303 return serviceIntent;
304 }
305
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700306 private void createRawContact(Intent intent) {
307 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
308 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700309 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700310 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
311 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
312
313 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
314 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
315 .withValue(RawContacts.ACCOUNT_NAME, accountName)
316 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700317 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700318 .build());
319
320 int size = valueList.size();
321 for (int i = 0; i < size; i++) {
322 ContentValues values = valueList.get(i);
323 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
324 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
325 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
326 .withValues(values)
327 .build());
328 }
329
330 ContentResolver resolver = getContentResolver();
331 ContentProviderResult[] results;
332 try {
333 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
334 } catch (Exception e) {
335 throw new RuntimeException("Failed to store new contact", e);
336 }
337
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700338 Uri rawContactUri = results[0].uri;
339 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
340
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800341 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700342 }
343
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700344 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800345 * Creates an intent that can be sent to this service to create a new raw contact
346 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800347 * This variant is more convenient to use when there is only one photo that can
348 * possibly be updated, as in the Contact Details screen.
349 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
350 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800351 */
Maurice Chu851222a2012-06-21 11:43:08 -0700352 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700353 String saveModeExtraKey, int saveMode, boolean isProfile,
354 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700355 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800356 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700357 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800358 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700359 callbackActivity, callbackAction, bundle,
360 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800361 }
362
363 /**
364 * Creates an intent that can be sent to this service to create a new raw contact
365 * using data presented as a set of ContentValues.
366 * This variant is used when multiple contacts' photos may be updated, as in the
367 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700368 *
Josh Garguse692e012012-01-18 14:53:11 -0800369 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700370 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
371 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800372 */
Maurice Chu851222a2012-06-21 11:43:08 -0700373 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700374 String saveModeExtraKey, int saveMode, boolean isProfile,
375 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700376 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Wenyi Wange5bac732016-02-03 11:55:21 -0800377 // Don't pass read-only RawContactDeltas in RawContactDeltaList to contact save service,
378 // because 1. read-only RawContactDeltas are not writable anyway; 2. read-only
379 // RawContactDeltas may be problematic, see b/23896510.
380 removeReadOnlyContacts(context, state);
381
382 Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800383 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
384 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700385 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800386 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
387
Josh Garguse692e012012-01-18 14:53:11 -0800388 if (updatedPhotos != null) {
389 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
390 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800391
Josh Garguse5d3f892012-04-11 11:56:15 -0700392 if (callbackActivity != null) {
393 // Callback intent will be invoked by the service once the contact is
394 // saved. The service will put the URI of the new contact as "data" on
395 // the callback intent.
396 Intent callbackIntent = new Intent(context, callbackActivity);
397 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700398 if (joinContactIdExtraKey != null && joinContactId != null) {
399 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
400 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700401 callbackIntent.setAction(callbackAction);
402 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
403 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800404 return serviceIntent;
405 }
406
Wenyi Wange5bac732016-02-03 11:55:21 -0800407 private static void removeReadOnlyContacts(Context context, RawContactDeltaList state) {
408 if (Log.isLoggable(TAG, Log.VERBOSE)) {
409 Log.v(TAG, "Before trimming: " + state.size());
410 }
411 int countReadOnly = 0;
412 final Iterator<RawContactDelta> iterator = state.iterator();
413 while (iterator.hasNext()) {
414 final RawContactDelta rawContactDelta = iterator.next();
415 final AccountType accountType = rawContactDelta.getRawContactAccountType(context);
416 if (accountType != null && !accountType.areContactsWritable()) {
417 countReadOnly++;
418 iterator.remove();
419 }
420 }
421 if (Log.isLoggable(TAG, Log.VERBOSE)) {
422 Log.v(TAG, "# of read-only removed: " + countReadOnly);
423 Log.v(TAG, "After trimming: " + state.size());
424 }
425 }
426
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800427 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700428 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700429 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800430 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800431
Jay Shrauner08099782015-03-25 14:17:11 -0700432 if (state == null) {
433 Log.e(TAG, "Invalid arguments for saveContact request");
434 return;
435 }
436
benny.lin3a4e7a22014-01-08 10:58:08 +0800437 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800438 // Trim any empty fields, and RawContacts, before persisting
439 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700440 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800441
442 Uri lookupUri = null;
443
444 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700445
Josh Garguse692e012012-01-18 14:53:11 -0800446 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800447
Josh Gargusef15c8e2012-01-30 16:42:02 -0800448 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
449 long insertedRawContactId = -1;
450
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800451 // Attempt to persist changes
452 int tries = 0;
453 while (tries++ < PERSIST_TRIES) {
454 try {
455 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800456 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
457
458 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
459
460 for (CPOWrapper cpoWrapper : diffWrapper) {
461 diff.add(cpoWrapper.getOperation());
462 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700463
Katherine Kuana007e442011-07-07 09:25:34 -0700464 if (DEBUG) {
465 Log.v(TAG, "Content Provider Operations:");
466 for (ContentProviderOperation operation : diff) {
467 Log.v(TAG, operation.toString());
468 }
469 }
470
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700471 int numberProcessed = 0;
472 boolean batchFailed = false;
473 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
474 while (numberProcessed < diff.size()) {
475 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
476 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700477 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700478 batchFailed = true;
479 break;
480 } else {
481 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700482 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800483 }
484
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700485 if (batchFailed) {
486 // Retry save
487 continue;
488 }
489
Wenyi Wang67addcc2015-11-23 10:07:48 -0800490 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800491 if (rawContactId == -1) {
492 throw new IllegalStateException("Could not determine RawContact ID after save");
493 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800494 // We don't have to check to see if the value is still -1. If we reach here,
495 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800496 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700497 if (isProfile) {
498 // Since the profile supports local raw contacts, which may have been completely
499 // removed if all information was removed, we need to do a special query to
500 // get the lookup URI for the profile contact (if it still exists).
501 Cursor c = resolver.query(Profile.CONTENT_URI,
502 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
503 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800504 if (c == null) {
505 continue;
506 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700507 try {
Erik162b7e32011-09-20 15:23:55 -0700508 if (c.moveToFirst()) {
509 final long contactId = c.getLong(0);
510 final String lookupKey = c.getString(1);
511 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
512 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700513 } finally {
514 c.close();
515 }
516 } else {
517 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
518 rawContactId);
519 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
520 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800521 if (lookupUri != null) {
522 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
523 }
Josh Garguse692e012012-01-18 14:53:11 -0800524
525 // We can change this back to false later, if we fail to save the contact photo.
526 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800527 break;
528
529 } catch (RemoteException e) {
530 // Something went wrong, bail without success
531 Log.e(TAG, "Problem persisting user edits", e);
532 break;
533
Jay Shrauner57fca182014-01-17 14:20:50 -0800534 } catch (IllegalArgumentException e) {
535 // This is thrown by applyBatch on malformed requests
536 Log.e(TAG, "Problem persisting user edits", e);
537 showToast(R.string.contactSavedErrorToast);
538 break;
539
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800540 } catch (OperationApplicationException e) {
541 // Version consistency failed, re-parent change and try again
542 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
543 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
544 boolean first = true;
545 final int count = state.size();
546 for (int i = 0; i < count; i++) {
547 Long rawContactId = state.getRawContactId(i);
548 if (rawContactId != null && rawContactId != -1) {
549 if (!first) {
550 sb.append(',');
551 }
552 sb.append(rawContactId);
553 first = false;
554 }
555 }
556 sb.append(")");
557
558 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800559 throw new IllegalStateException(
560 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800561 }
562
Maurice Chu851222a2012-06-21 11:43:08 -0700563 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700564 isProfile
565 ? RawContactsEntity.PROFILE_CONTENT_URI
566 : RawContactsEntity.CONTENT_URI,
567 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700568 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700569
570 // Update the new state to use profile URIs if appropriate.
571 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700572 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700573 delta.setProfileQueryUri();
574 }
575 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800576 }
577 }
578
Josh Garguse692e012012-01-18 14:53:11 -0800579 // Now save any updated photos. We do this at the end to ensure that
580 // the ContactProvider already knows about newly-created contacts.
581 if (updatedPhotos != null) {
582 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700583 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800584 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800585
586 // If the raw-contact ID is negative, we are saving a new raw-contact;
587 // replace the bogus ID with the new one that we actually saved the contact at.
588 if (rawContactId < 0) {
589 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800590 }
591
Jay Shrauner511561d2015-04-02 10:35:33 -0700592 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700593 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700594 succeeded = false;
595 }
Josh Garguse692e012012-01-18 14:53:11 -0800596 }
597 }
598
Josh Garguse5d3f892012-04-11 11:56:15 -0700599 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
600 if (callbackIntent != null) {
601 if (succeeded) {
602 // Mark the intent to indicate that the save was successful (even if the lookup URI
603 // is now null). For local contacts or the local profile, it's possible that the
604 // save triggered removal of the contact, so no lookup URI would exist..
605 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
606 }
607 callbackIntent.setData(lookupUri);
608 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800609 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800610 }
611
Josh Garguse692e012012-01-18 14:53:11 -0800612 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700613 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
614 * subsets, adds the returned array to "results".
615 *
616 * @return the size of the array, if not null; -1 when the array is null.
617 */
618 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
619 ContentProviderResult[] results, ContentResolver resolver)
620 throws RemoteException, OperationApplicationException {
621 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
622 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
623 subset.addAll(diff.subList(offset, offset + subsetCount));
624 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
625 .AUTHORITY, subset);
626 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
627 return -1;
628 }
629 for (ContentProviderResult c : subsetResult) {
630 results[offset++] = c;
631 }
632 return subsetResult.length;
633 }
634
635 /**
Josh Garguse692e012012-01-18 14:53:11 -0800636 * Save updated photo for the specified raw-contact.
637 * @return true for success, false for failure
638 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800639 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800640 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800641 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
642 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
643
benny.lin3a4e7a22014-01-08 10:58:08 +0800644 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800645 }
646
Josh Gargusef15c8e2012-01-30 16:42:02 -0800647 /**
648 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
649 */
Maurice Chu851222a2012-06-21 11:43:08 -0700650 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800651 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800652 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800653 long existingRawContactId = state.findRawContactId();
654 if (existingRawContactId != -1) {
655 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800656 }
657
Wenyi Wang67addcc2015-11-23 10:07:48 -0800658 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800659 }
660
661 /**
662 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
663 */
664 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800665 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800666 if (results == null) {
667 return -1;
668 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800669 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800670 final int numResults = results.length;
671 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800672 final CPOWrapper cpoWrapper = diffWrapper.get(i);
673 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
674 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
675 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800676 return ContentUris.parseId(results[i].uri);
677 }
678 }
679 return -1;
680 }
681
682 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700683 * Creates an intent that can be sent to this service to create a new group as
684 * well as add new members at the same time.
685 *
686 * @param context of the application
687 * @param account in which the group should be created
688 * @param label is the name of the group (cannot be null)
689 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
690 * should be added to the group
691 * @param callbackActivity is the activity to send the callback intent to
692 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700693 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700694 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700695 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700696 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800697 Intent serviceIntent = new Intent(context, ContactSaveService.class);
698 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
699 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
700 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700701 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800702 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700703 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700704
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800705 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700706 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800707 Intent callbackIntent = new Intent(context, callbackActivity);
708 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700709 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800710
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700711 return serviceIntent;
712 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800713
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800714 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700715 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
716 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
717 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
718 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700719 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800720
721 ContentValues values = new ContentValues();
722 values.put(Groups.ACCOUNT_TYPE, accountType);
723 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700724 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800725 values.put(Groups.TITLE, label);
726
Katherine Kuan717e3432011-07-13 17:03:24 -0700727 final ContentResolver resolver = getContentResolver();
728
729 // Create the new group
730 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
731
732 // If there's no URI, then the insertion failed. Abort early because group members can't be
733 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800734 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700735 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800736 return;
737 }
738
Katherine Kuan717e3432011-07-13 17:03:24 -0700739 // Add new group members
740 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
741
742 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
743 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800744 values.clear();
745 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
746 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
747
748 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700749 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700750 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800751 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800752 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800753 }
754
755 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800756 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800757 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700758 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700759 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800760 Intent serviceIntent = new Intent(context, ContactSaveService.class);
761 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
762 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
763 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700764
765 // Callback intent will be invoked by the service once the group is renamed.
766 Intent callbackIntent = new Intent(context, callbackActivity);
767 callbackIntent.setAction(callbackAction);
768 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
769
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800770 return serviceIntent;
771 }
772
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800773 private void renameGroup(Intent intent) {
774 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
775 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
776
777 if (groupId == -1) {
778 Log.e(TAG, "Invalid arguments for renameGroup request");
779 return;
780 }
781
782 ContentValues values = new ContentValues();
783 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700784 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
785 getContentResolver().update(groupUri, values, null, null);
786
787 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
788 callbackIntent.setData(groupUri);
789 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800790 }
791
792 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800793 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800794 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800795 public static Intent createGroupDeletionIntent(Context context, long groupId) {
796 Intent serviceIntent = new Intent(context, ContactSaveService.class);
797 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800798 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800799 return serviceIntent;
800 }
801
802 private void deleteGroup(Intent intent) {
803 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
804 if (groupId == -1) {
805 Log.e(TAG, "Invalid arguments for deleteGroup request");
806 return;
807 }
808
809 getContentResolver().delete(
810 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
811 }
812
813 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700814 * Creates an intent that can be sent to this service to rename a group as
815 * well as add and remove members from the group.
816 *
817 * @param context of the application
818 * @param groupId of the group that should be modified
819 * @param newLabel is the updated name of the group (can be null if the name
820 * should not be updated)
821 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
822 * should be added to the group
823 * @param rawContactsToRemove is an array of raw contact IDs for contacts
824 * that should be removed from the group
825 * @param callbackActivity is the activity to send the callback intent to
826 * @param callbackAction is the intent action for the callback intent
827 */
828 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
829 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700830 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700831 Intent serviceIntent = new Intent(context, ContactSaveService.class);
832 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
833 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
834 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
835 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
836 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
837 rawContactsToRemove);
838
839 // Callback intent will be invoked by the service once the group is updated
840 Intent callbackIntent = new Intent(context, callbackActivity);
841 callbackIntent.setAction(callbackAction);
842 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
843
844 return serviceIntent;
845 }
846
847 private void updateGroup(Intent intent) {
848 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
849 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
850 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
851 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
852
853 if (groupId == -1) {
854 Log.e(TAG, "Invalid arguments for updateGroup request");
855 return;
856 }
857
858 final ContentResolver resolver = getContentResolver();
859 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
860
861 // Update group name if necessary
862 if (label != null) {
863 ContentValues values = new ContentValues();
864 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700865 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700866 }
867
Katherine Kuan717e3432011-07-13 17:03:24 -0700868 // Add and remove members if necessary
869 addMembersToGroup(resolver, rawContactsToAdd, groupId);
870 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
871
872 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
873 callbackIntent.setData(groupUri);
874 deliverCallback(callbackIntent);
875 }
876
Daniel Lehmann18958a22012-02-28 17:45:25 -0800877 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700878 long groupId) {
879 if (rawContactsToAdd == null) {
880 return;
881 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700882 for (long rawContactId : rawContactsToAdd) {
883 try {
884 final ArrayList<ContentProviderOperation> rawContactOperations =
885 new ArrayList<ContentProviderOperation>();
886
887 // Build an assert operation to ensure the contact is not already in the group
888 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
889 .newAssertQuery(Data.CONTENT_URI);
890 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
891 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
892 new String[] { String.valueOf(rawContactId),
893 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
894 assertBuilder.withExpectedCount(0);
895 rawContactOperations.add(assertBuilder.build());
896
897 // Build an insert operation to add the contact to the group
898 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
899 .newInsert(Data.CONTENT_URI);
900 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
901 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
902 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
903 rawContactOperations.add(insertBuilder.build());
904
905 if (DEBUG) {
906 for (ContentProviderOperation operation : rawContactOperations) {
907 Log.v(TAG, operation.toString());
908 }
909 }
910
911 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700912 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800913 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700914 }
915 } catch (RemoteException e) {
916 // Something went wrong, bail without success
917 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
918 String.valueOf(rawContactId), e);
919 } catch (OperationApplicationException e) {
920 // The assert could have failed because the contact is already in the group,
921 // just continue to the next contact
922 Log.w(TAG, "Assert failed in adding raw contact ID " +
923 String.valueOf(rawContactId) + ". Already exists in group " +
924 String.valueOf(groupId), e);
925 }
926 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700927 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700928
Daniel Lehmann18958a22012-02-28 17:45:25 -0800929 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700930 long groupId) {
931 if (rawContactsToRemove == null) {
932 return;
933 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700934 for (long rawContactId : rawContactsToRemove) {
935 // Apply the delete operation on the data row for the given raw contact's
936 // membership in the given group. If no contact matches the provided selection, then
937 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800938 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700939 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
940 new String[] { String.valueOf(rawContactId),
941 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
942 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700943 }
944
945 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800946 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800947 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800948 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
949 Intent serviceIntent = new Intent(context, ContactSaveService.class);
950 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
951 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
952 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
953
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800954 return serviceIntent;
955 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800956
957 private void setStarred(Intent intent) {
958 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
959 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
960 if (contactUri == null) {
961 Log.e(TAG, "Invalid arguments for setStarred request");
962 return;
963 }
964
965 final ContentValues values = new ContentValues(1);
966 values.put(Contacts.STARRED, value);
967 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700968
969 // Undemote the contact if necessary
970 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
971 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800972 if (c == null) {
973 return;
974 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700975 try {
976 if (c.moveToFirst()) {
977 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700978
979 // Don't bother undemoting if this contact is the user's profile.
980 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -0800981 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700982 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700983 }
984 } finally {
985 c.close();
986 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800987 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800988
989 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700990 * Creates an intent that can be sent to this service to set the redirect to voicemail.
991 */
992 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
993 boolean value) {
994 Intent serviceIntent = new Intent(context, ContactSaveService.class);
995 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
996 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
997 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
998
999 return serviceIntent;
1000 }
1001
1002 private void setSendToVoicemail(Intent intent) {
1003 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1004 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1005 if (contactUri == null) {
1006 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1007 return;
1008 }
1009
1010 final ContentValues values = new ContentValues(1);
1011 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1012 getContentResolver().update(contactUri, values, null, null);
1013 }
1014
1015 /**
1016 * Creates an intent that can be sent to this service to save the contact's ringtone.
1017 */
1018 public static Intent createSetRingtone(Context context, Uri contactUri,
1019 String value) {
1020 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1021 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1022 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1023 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1024
1025 return serviceIntent;
1026 }
1027
1028 private void setRingtone(Intent intent) {
1029 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1030 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1031 if (contactUri == null) {
1032 Log.e(TAG, "Invalid arguments for setRingtone");
1033 return;
1034 }
1035 ContentValues values = new ContentValues(1);
1036 values.put(Contacts.CUSTOM_RINGTONE, value);
1037 getContentResolver().update(contactUri, values, null, null);
1038 }
1039
1040 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001041 * Creates an intent that sets the selected data item as super primary (default)
1042 */
1043 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1044 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1045 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1046 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1047 return serviceIntent;
1048 }
1049
1050 private void setSuperPrimary(Intent intent) {
1051 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1052 if (dataId == -1) {
1053 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1054 return;
1055 }
1056
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001057 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001058 }
1059
1060 /**
1061 * Creates an intent that clears the primary flag of all data items that belong to the same
1062 * raw_contact as the given data item. Will only clear, if the data item was primary before
1063 * this call
1064 */
1065 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1066 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1067 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1068 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1069 return serviceIntent;
1070 }
1071
1072 private void clearPrimary(Intent intent) {
1073 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1074 if (dataId == -1) {
1075 Log.e(TAG, "Invalid arguments for clearPrimary request");
1076 return;
1077 }
1078
1079 // Update the primary values in the data record.
1080 ContentValues values = new ContentValues(1);
1081 values.put(Data.IS_SUPER_PRIMARY, 0);
1082 values.put(Data.IS_PRIMARY, 0);
1083
1084 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1085 values, null, null);
1086 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001087
1088 /**
1089 * Creates an intent that can be sent to this service to delete a contact.
1090 */
1091 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1092 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1093 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1094 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1095 return serviceIntent;
1096 }
1097
Brian Attwelld2962a32015-03-02 14:48:50 -08001098 /**
1099 * Creates an intent that can be sent to this service to delete multiple contacts.
1100 */
1101 public static Intent createDeleteMultipleContactsIntent(Context context,
1102 long[] contactIds) {
1103 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1104 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1105 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1106 return serviceIntent;
1107 }
1108
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001109 private void deleteContact(Intent intent) {
1110 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1111 if (contactUri == null) {
1112 Log.e(TAG, "Invalid arguments for deleteContact request");
1113 return;
1114 }
1115
1116 getContentResolver().delete(contactUri, null, null);
1117 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001118
Brian Attwelld2962a32015-03-02 14:48:50 -08001119 private void deleteMultipleContacts(Intent intent) {
1120 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1121 if (contactIds == null) {
1122 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1123 return;
1124 }
1125 for (long contactId : contactIds) {
1126 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1127 getContentResolver().delete(contactUri, null, null);
1128 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001129 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1130 .contacts_deleted_toast, contactIds.length);
1131 mMainHandler.post(new Runnable() {
1132 @Override
1133 public void run() {
1134 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1135 .show();
1136 }
1137 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001138 }
1139
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001140 /**
1141 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001142 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001143 */
1144 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001145 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001146 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1147 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1148 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1149 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001150
1151 // Callback intent will be invoked by the service once the contacts are joined.
1152 Intent callbackIntent = new Intent(context, callbackActivity);
1153 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001154 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1155
1156 return serviceIntent;
1157 }
1158
Brian Attwelld3946ca2015-03-03 11:13:49 -08001159 /**
1160 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1161 * No special attention is paid to where the resulting contact's name is taken from.
1162 */
1163 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1164 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1165 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1166 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1167 return serviceIntent;
1168 }
1169
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001170
1171 private interface JoinContactQuery {
1172 String[] PROJECTION = {
1173 RawContacts._ID,
1174 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001175 RawContacts.DISPLAY_NAME_SOURCE,
1176 };
1177
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001178 int _ID = 0;
1179 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001180 int DISPLAY_NAME_SOURCE = 2;
1181 }
1182
1183 private interface ContactEntityQuery {
1184 String[] PROJECTION = {
1185 Contacts.Entity.DATA_ID,
1186 Contacts.Entity.CONTACT_ID,
1187 Contacts.Entity.IS_SUPER_PRIMARY,
1188 };
1189 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1190 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1191 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1192 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1193
1194 int DATA_ID = 0;
1195 int CONTACT_ID = 1;
1196 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001197 }
1198
Brian Attwelld3946ca2015-03-03 11:13:49 -08001199 private void joinSeveralContacts(Intent intent) {
1200 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001201
Brian Attwelld3946ca2015-03-03 11:13:49 -08001202 // Load raw contact IDs for all contacts involved.
1203 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1204 if (rawContactIds == null) {
1205 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001206 return;
1207 }
1208
Brian Attwelld3946ca2015-03-03 11:13:49 -08001209 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001210 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001211 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1212 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1213 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001214 for (int i = 0; i < rawContactIds.length; i++) {
1215 for (int j = 0; j < rawContactIds.length; j++) {
1216 if (i != j) {
1217 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1218 }
Walter Jang0653de32015-07-24 12:12:40 -07001219 // Before we get to 500 we need to flush the operations list
1220 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1221 if (!applyJoinOperations(resolver, operations)) {
1222 return;
1223 }
1224 operations.clear();
1225 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001226 }
1227 }
Walter Jang0653de32015-07-24 12:12:40 -07001228 if (operations.size() > 0 && !applyJoinOperations(resolver, operations)) {
1229 return;
1230 }
1231 showToast(R.string.contactsJoinedMessage);
1232 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001233
Walter Jang0653de32015-07-24 12:12:40 -07001234 /** Returns true if the batch was successfully applied and false otherwise. */
1235 private boolean applyJoinOperations(ContentResolver resolver,
1236 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001237 try {
1238 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001239 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001240 } catch (RemoteException | OperationApplicationException e) {
1241 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1242 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001243 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001244 }
1245 }
1246
1247
1248 private void joinContacts(Intent intent) {
1249 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1250 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001251
1252 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001253 // in the join UIs.
1254 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1255 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001256 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001257 return;
1258 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001259
Brian Attwell548f5c62015-01-27 17:46:46 -08001260 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001261
1262 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001263 for (int i = 0; i < rawContactIds.length; i++) {
1264 for (int j = 0; j < rawContactIds.length; j++) {
1265 if (i != j) {
1266 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1267 }
1268 }
1269 }
1270
Brian Attwelld3946ca2015-03-03 11:13:49 -08001271 final ContentResolver resolver = getContentResolver();
1272
Brian Attwell548f5c62015-01-27 17:46:46 -08001273 // Use the name for contactId1 as the name for the newly aggregated contact.
1274 final Uri contactId1Uri = ContentUris.withAppendedId(
1275 Contacts.CONTENT_URI, contactId1);
1276 final Uri entityUri = Uri.withAppendedPath(
1277 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1278 Cursor c = resolver.query(entityUri,
1279 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1280 if (c == null) {
1281 Log.e(TAG, "Unable to open Contacts DB cursor");
1282 showToast(R.string.contactSavedErrorToast);
1283 return;
1284 }
1285 long dataIdToAddSuperPrimary = -1;
1286 try {
1287 if (c.moveToFirst()) {
1288 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1289 }
1290 } finally {
1291 c.close();
1292 }
1293
1294 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1295 // display name does not change as a result of the join.
1296 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001297 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001298 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1299 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1300 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001301 operations.add(builder.build());
1302 }
1303
1304 boolean success = false;
1305 // Apply all aggregation exceptions as one batch
1306 try {
1307 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001308 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001309 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001310 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001311 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001312 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001313 }
1314
1315 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1316 if (success) {
1317 Uri uri = RawContacts.getContactLookupUri(resolver,
1318 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1319 callbackIntent.setData(uri);
1320 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001321 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001322 }
1323
Brian Attwelld3946ca2015-03-03 11:13:49 -08001324 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1325 if (contactIds == null) {
1326 return null;
1327 }
1328
Brian Attwell548f5c62015-01-27 17:46:46 -08001329 final ContentResolver resolver = getContentResolver();
1330 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001331
1332 final StringBuilder queryBuilder = new StringBuilder();
1333 final String stringContactIds[] = new String[contactIds.length];
1334 for (int i = 0; i < contactIds.length; i++) {
1335 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1336 stringContactIds[i] = String.valueOf(contactIds[i]);
1337 if (contactIds[i] == -1) {
1338 return null;
1339 }
1340 if (i == contactIds.length -1) {
1341 break;
1342 }
1343 queryBuilder.append(" OR ");
1344 }
1345
Brian Attwell548f5c62015-01-27 17:46:46 -08001346 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1347 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001348 queryBuilder.toString(),
1349 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001350 if (c == null) {
1351 Log.e(TAG, "Unable to open Contacts DB cursor");
1352 showToast(R.string.contactSavedErrorToast);
1353 return null;
1354 }
1355 try {
1356 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001357 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001358 return null;
1359 }
1360 rawContactIds = new long[c.getCount()];
1361 for (int i = 0; i < rawContactIds.length; i++) {
1362 c.moveToPosition(i);
1363 long rawContactId = c.getLong(JoinContactQuery._ID);
1364 rawContactIds[i] = rawContactId;
1365 }
1366 } finally {
1367 c.close();
1368 }
1369 return rawContactIds;
1370 }
1371
Brian Attwelld3946ca2015-03-03 11:13:49 -08001372 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1373 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1374 }
1375
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001376 /**
1377 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1378 */
1379 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1380 long rawContactId1, long rawContactId2) {
1381 Builder builder =
1382 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1383 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1384 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1385 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1386 operations.add(builder.build());
1387 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001388
1389 /**
1390 * Shows a toast on the UI thread.
1391 */
1392 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001393 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001394
1395 @Override
1396 public void run() {
1397 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1398 }
1399 });
1400 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001401
1402 private void deliverCallback(final Intent callbackIntent) {
1403 mMainHandler.post(new Runnable() {
1404
1405 @Override
1406 public void run() {
1407 deliverCallbackOnUiThread(callbackIntent);
1408 }
1409 });
1410 }
1411
1412 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1413 // TODO: this assumes that if there are multiple instances of the same
1414 // activity registered, the last one registered is the one waiting for
1415 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001416 for (Listener listener : sListeners) {
1417 if (callbackIntent.getComponent().equals(
1418 ((Activity) listener).getIntent().getComponent())) {
1419 listener.onServiceCompleted(callbackIntent);
1420 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001421 }
1422 }
1423 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001424}