blob: b34f38483af8b2764a882b8fed9cb55ec132e13b [file] [log] [blame]
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080017package com.android.contacts;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070018
Jay Shrauner615ed9c2015-07-29 11:27:56 -070019import static android.Manifest.permission.WRITE_CONTACTS;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080020import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070021import android.app.IntentService;
22import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080023import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070024import android.content.ContentProviderResult;
25import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080026import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070027import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080028import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070029import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080030import android.content.OperationApplicationException;
31import android.database.Cursor;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070032import android.database.DatabaseUtils;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070033import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080034import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080035import android.os.Handler;
36import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080037import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080038import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070039import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080040import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080041import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080042import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080043import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070044import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080045import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070046import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070047import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070048import android.provider.ContactsContract.RawContactsEntity;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070049import android.support.v4.content.LocalBroadcastManager;
Gary Mai7efa9942016-05-12 11:26:49 -070050import android.support.v4.os.ResultReceiver;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070051import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080052import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070053
Wenyi Wangdd7d4562015-12-08 13:33:43 -080054import com.android.contacts.activities.ContactEditorBaseActivity;
Wenyi Wang67addcc2015-11-23 10:07:48 -080055import com.android.contacts.common.compat.CompatUtils;
Chiao Chengd7ca03e2012-10-24 15:14:08 -070056import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080057import com.android.contacts.common.model.AccountTypeManager;
Wenyi Wang67addcc2015-11-23 10:07:48 -080058import com.android.contacts.common.model.CPOWrapper;
Yorke Leecd321f62013-10-28 15:20:15 -070059import com.android.contacts.common.model.RawContactDelta;
60import com.android.contacts.common.model.RawContactDeltaList;
61import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080062import com.android.contacts.common.model.account.AccountWithDataSet;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070063import com.android.contacts.common.util.PermissionsUtil;
Wenyi Wangaac0e662015-12-18 17:17:33 -080064import com.android.contacts.compat.PinnedPositionsCompat;
Yorke Lee637a38e2013-09-14 08:36:33 -070065import com.android.contacts.util.ContactPhotoUtils;
66
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070067import com.google.common.collect.Lists;
68import com.google.common.collect.Sets;
69
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080070import java.util.ArrayList;
71import java.util.HashSet;
72import java.util.List;
73import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070074
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080075/**
76 * A service responsible for saving changes to the content provider.
77 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070078public class ContactSaveService extends IntentService {
79 private static final String TAG = "ContactSaveService";
80
Katherine Kuana007e442011-07-07 09:25:34 -070081 /** Set to true in order to view logs on content provider operations */
82 private static final boolean DEBUG = false;
83
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070084 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
85
86 public static final String EXTRA_ACCOUNT_NAME = "accountName";
87 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070088 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070089 public static final String EXTRA_CONTENT_VALUES = "contentValues";
90 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
Gary Mai7efa9942016-05-12 11:26:49 -070091 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
92 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070093
Dmitri Plotnikova0114142011-02-15 13:53:21 -080094 public static final String ACTION_SAVE_CONTACT = "saveContact";
95 public static final String EXTRA_CONTACT_STATE = "state";
96 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070097 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070098 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080099 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700100
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800101 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800102 public static final String ACTION_RENAME_GROUP = "renameGroup";
103 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700104 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800105 public static final String EXTRA_GROUP_ID = "groupId";
106 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700107 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
108 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800109
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800110 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800111 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800112 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800113 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800114 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800115 public static final String EXTRA_STARRED_FLAG = "starred";
Marcus Hagerott3bb85142016-07-29 10:46:36 -0700116 public static final String EXTRA_DISPLAY_NAME = "extraDisplayName";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800117
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800118 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
119 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
120 public static final String EXTRA_DATA_ID = "dataId";
121
Gary Mai7efa9942016-05-12 11:26:49 -0700122 public static final String ACTION_SPLIT_CONTACT = "splitContact";
123
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800124 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800125 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800126 public static final String EXTRA_CONTACT_ID1 = "contactId1";
127 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800128
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700129 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
130 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
131
132 public static final String ACTION_SET_RINGTONE = "setRingtone";
133 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
134
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700135 public static final String ACTION_UNDO = "undo";
136 public static final String EXTRA_UNDO_ACTION = "undoAction";
137 public static final String EXTRA_UNDO_DATA = "undoData";
138
139 public static final String BROADCAST_ACTION_GROUP_DELETED = "groupDeleted";
140
Gary Mai7efa9942016-05-12 11:26:49 -0700141 public static final int CP2_ERROR = 0;
142 public static final int CONTACTS_LINKED = 1;
143 public static final int CONTACTS_SPLIT = 2;
Gary Mai31d572e2016-06-03 14:04:32 -0700144 public static final int BAD_ARGUMENTS = 3;
Gary Mai7efa9942016-05-12 11:26:49 -0700145
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700146 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
147 Data.MIMETYPE,
148 Data.IS_PRIMARY,
149 Data.DATA1,
150 Data.DATA2,
151 Data.DATA3,
152 Data.DATA4,
153 Data.DATA5,
154 Data.DATA6,
155 Data.DATA7,
156 Data.DATA8,
157 Data.DATA9,
158 Data.DATA10,
159 Data.DATA11,
160 Data.DATA12,
161 Data.DATA13,
162 Data.DATA14,
163 Data.DATA15
164 );
165
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800166 private static final int PERSIST_TRIES = 3;
167
Walter Jang0653de32015-07-24 12:12:40 -0700168 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
169
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800170 public interface Listener {
171 public void onServiceCompleted(Intent callbackIntent);
172 }
173
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100174 private static final CopyOnWriteArrayList<Listener> sListeners =
175 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800176
177 private Handler mMainHandler;
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700178 private GroupsDao mGroupsDao;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800179
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700180 public ContactSaveService() {
181 super(TAG);
182 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800183 mMainHandler = new Handler(Looper.getMainLooper());
184 }
185
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700186 @Override
187 public void onCreate() {
188 super.onCreate();
189 mGroupsDao = new GroupsDaoImpl(this);
190 }
191
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800192 public static void registerListener(Listener listener) {
193 if (!(listener instanceof Activity)) {
194 throw new ClassCastException("Only activities can be registered to"
195 + " receive callback from " + ContactSaveService.class.getName());
196 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100197 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800198 }
199
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700200 public static boolean canUndo(Intent resultIntent) {
201 return resultIntent.hasExtra(EXTRA_UNDO_DATA);
202 }
203
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800204 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100205 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700206 }
207
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800208 /**
209 * Returns true if the ContactSaveService was started successfully and false if an exception
210 * was thrown and a Toast error message was displayed.
211 */
212 public static boolean startService(Context context, Intent intent, int saveMode) {
213 try {
214 context.startService(intent);
215 } catch (Exception exception) {
216 final int resId;
217 switch (saveMode) {
218 case ContactEditorBaseActivity.ContactEditor.SaveMode.SPLIT:
219 resId = R.string.contactUnlinkErrorToast;
220 break;
221 case ContactEditorBaseActivity.ContactEditor.SaveMode.RELOAD:
222 resId = R.string.contactJoinErrorToast;
223 break;
224 case ContactEditorBaseActivity.ContactEditor.SaveMode.CLOSE:
225 resId = R.string.contactSavedErrorToast;
226 break;
227 default:
228 resId = R.string.contactGenericErrorToast;
229 }
230 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
231 return false;
232 }
233 return true;
234 }
235
236 /**
237 * Utility method that starts service and handles exception.
238 */
239 public static void startService(Context context, Intent intent) {
240 try {
241 context.startService(intent);
242 } catch (Exception exception) {
243 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
244 }
245 }
246
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700247 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800248 public Object getSystemService(String name) {
249 Object service = super.getSystemService(name);
250 if (service != null) {
251 return service;
252 }
253
254 return getApplicationContext().getSystemService(name);
255 }
256
257 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700258 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800259 if (intent == null) {
260 Log.d(TAG, "onHandleIntent: could not handle null intent");
261 return;
262 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700263 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
264 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
265 // TODO: add more specific error string such as "Turn on Contacts
266 // permission to update your contacts"
267 showToast(R.string.contactSavedErrorToast);
268 return;
269 }
270
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700271 // Call an appropriate method. If we're sure it affects how incoming phone calls are
272 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700273 String action = intent.getAction();
274 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
275 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800276 } else if (ACTION_SAVE_CONTACT.equals(action)) {
277 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800278 } else if (ACTION_CREATE_GROUP.equals(action)) {
279 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800280 } else if (ACTION_RENAME_GROUP.equals(action)) {
281 renameGroup(intent);
282 } else if (ACTION_DELETE_GROUP.equals(action)) {
283 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700284 } else if (ACTION_UPDATE_GROUP.equals(action)) {
285 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800286 } else if (ACTION_SET_STARRED.equals(action)) {
287 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800288 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
289 setSuperPrimary(intent);
290 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
291 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800292 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
293 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800294 } else if (ACTION_DELETE_CONTACT.equals(action)) {
295 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700296 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
297 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800298 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
299 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800300 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
301 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700302 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
303 setSendToVoicemail(intent);
304 } else if (ACTION_SET_RINGTONE.equals(action)) {
305 setRingtone(intent);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700306 } else if (ACTION_UNDO.equals(action)) {
307 undo(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700308 }
309 }
310
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800311 /**
312 * Creates an intent that can be sent to this service to create a new raw contact
313 * using data presented as a set of ContentValues.
314 */
315 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700316 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700317 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800318 Intent serviceIntent = new Intent(
319 context, ContactSaveService.class);
320 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
321 if (account != null) {
322 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
323 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700324 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800325 }
326 serviceIntent.putParcelableArrayListExtra(
327 ContactSaveService.EXTRA_CONTENT_VALUES, values);
328
329 // Callback intent will be invoked by the service once the new contact is
330 // created. The service will put the URI of the new contact as "data" on
331 // the callback intent.
332 Intent callbackIntent = new Intent(context, callbackActivity);
333 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800334 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
335 return serviceIntent;
336 }
337
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700338 private void createRawContact(Intent intent) {
339 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
340 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700341 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700342 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
343 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
344
345 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
346 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
347 .withValue(RawContacts.ACCOUNT_NAME, accountName)
348 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700349 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700350 .build());
351
352 int size = valueList.size();
353 for (int i = 0; i < size; i++) {
354 ContentValues values = valueList.get(i);
355 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
356 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
357 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
358 .withValues(values)
359 .build());
360 }
361
362 ContentResolver resolver = getContentResolver();
363 ContentProviderResult[] results;
364 try {
365 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
366 } catch (Exception e) {
367 throw new RuntimeException("Failed to store new contact", e);
368 }
369
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700370 Uri rawContactUri = results[0].uri;
371 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
372
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800373 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700374 }
375
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700376 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800377 * Creates an intent that can be sent to this service to create a new raw contact
378 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800379 * This variant is more convenient to use when there is only one photo that can
380 * possibly be updated, as in the Contact Details screen.
381 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
382 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800383 */
Maurice Chu851222a2012-06-21 11:43:08 -0700384 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700385 String saveModeExtraKey, int saveMode, boolean isProfile,
386 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700387 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800388 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700389 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800390 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700391 callbackActivity, callbackAction, bundle,
392 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800393 }
394
395 /**
396 * Creates an intent that can be sent to this service to create a new raw contact
397 * using data presented as a set of ContentValues.
398 * This variant is used when multiple contacts' photos may be updated, as in the
399 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700400 *
Josh Garguse692e012012-01-18 14:53:11 -0800401 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700402 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
403 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800404 */
Maurice Chu851222a2012-06-21 11:43:08 -0700405 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700406 String saveModeExtraKey, int saveMode, boolean isProfile,
407 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700408 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000409 Intent serviceIntent = new Intent(
410 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800411 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
412 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700413 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800414 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
415
Josh Garguse692e012012-01-18 14:53:11 -0800416 if (updatedPhotos != null) {
417 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
418 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800419
Josh Garguse5d3f892012-04-11 11:56:15 -0700420 if (callbackActivity != null) {
421 // Callback intent will be invoked by the service once the contact is
422 // saved. The service will put the URI of the new contact as "data" on
423 // the callback intent.
424 Intent callbackIntent = new Intent(context, callbackActivity);
425 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700426 if (joinContactIdExtraKey != null && joinContactId != null) {
427 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
428 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700429 callbackIntent.setAction(callbackAction);
430 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
431 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800432 return serviceIntent;
433 }
434
435 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700436 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700437 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800438 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800439
Jay Shrauner08099782015-03-25 14:17:11 -0700440 if (state == null) {
441 Log.e(TAG, "Invalid arguments for saveContact request");
442 return;
443 }
444
benny.lin3a4e7a22014-01-08 10:58:08 +0800445 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800446 // Trim any empty fields, and RawContacts, before persisting
447 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700448 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800449
450 Uri lookupUri = null;
451
452 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700453
Josh Garguse692e012012-01-18 14:53:11 -0800454 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800455
Josh Gargusef15c8e2012-01-30 16:42:02 -0800456 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
457 long insertedRawContactId = -1;
458
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800459 // Attempt to persist changes
460 int tries = 0;
461 while (tries++ < PERSIST_TRIES) {
462 try {
463 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800464 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
465
466 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
467
468 for (CPOWrapper cpoWrapper : diffWrapper) {
469 diff.add(cpoWrapper.getOperation());
470 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700471
Katherine Kuana007e442011-07-07 09:25:34 -0700472 if (DEBUG) {
473 Log.v(TAG, "Content Provider Operations:");
474 for (ContentProviderOperation operation : diff) {
475 Log.v(TAG, operation.toString());
476 }
477 }
478
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700479 int numberProcessed = 0;
480 boolean batchFailed = false;
481 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
482 while (numberProcessed < diff.size()) {
483 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
484 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700485 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700486 batchFailed = true;
487 break;
488 } else {
489 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700490 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800491 }
492
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700493 if (batchFailed) {
494 // Retry save
495 continue;
496 }
497
Wenyi Wang67addcc2015-11-23 10:07:48 -0800498 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800499 if (rawContactId == -1) {
500 throw new IllegalStateException("Could not determine RawContact ID after save");
501 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800502 // We don't have to check to see if the value is still -1. If we reach here,
503 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800504 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700505 if (isProfile) {
506 // Since the profile supports local raw contacts, which may have been completely
507 // removed if all information was removed, we need to do a special query to
508 // get the lookup URI for the profile contact (if it still exists).
509 Cursor c = resolver.query(Profile.CONTENT_URI,
510 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
511 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800512 if (c == null) {
513 continue;
514 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700515 try {
Erik162b7e32011-09-20 15:23:55 -0700516 if (c.moveToFirst()) {
517 final long contactId = c.getLong(0);
518 final String lookupKey = c.getString(1);
519 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
520 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700521 } finally {
522 c.close();
523 }
524 } else {
525 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
526 rawContactId);
527 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
528 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800529 if (lookupUri != null) {
530 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
531 }
Josh Garguse692e012012-01-18 14:53:11 -0800532
533 // We can change this back to false later, if we fail to save the contact photo.
534 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800535 break;
536
537 } catch (RemoteException e) {
538 // Something went wrong, bail without success
539 Log.e(TAG, "Problem persisting user edits", e);
540 break;
541
Jay Shrauner57fca182014-01-17 14:20:50 -0800542 } catch (IllegalArgumentException e) {
543 // This is thrown by applyBatch on malformed requests
544 Log.e(TAG, "Problem persisting user edits", e);
545 showToast(R.string.contactSavedErrorToast);
546 break;
547
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800548 } catch (OperationApplicationException e) {
549 // Version consistency failed, re-parent change and try again
550 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
551 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
552 boolean first = true;
553 final int count = state.size();
554 for (int i = 0; i < count; i++) {
555 Long rawContactId = state.getRawContactId(i);
556 if (rawContactId != null && rawContactId != -1) {
557 if (!first) {
558 sb.append(',');
559 }
560 sb.append(rawContactId);
561 first = false;
562 }
563 }
564 sb.append(")");
565
566 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800567 throw new IllegalStateException(
568 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800569 }
570
Maurice Chu851222a2012-06-21 11:43:08 -0700571 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700572 isProfile
573 ? RawContactsEntity.PROFILE_CONTENT_URI
574 : RawContactsEntity.CONTENT_URI,
575 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700576 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700577
578 // Update the new state to use profile URIs if appropriate.
579 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700580 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700581 delta.setProfileQueryUri();
582 }
583 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800584 }
585 }
586
Josh Garguse692e012012-01-18 14:53:11 -0800587 // Now save any updated photos. We do this at the end to ensure that
588 // the ContactProvider already knows about newly-created contacts.
589 if (updatedPhotos != null) {
590 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700591 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800592 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800593
594 // If the raw-contact ID is negative, we are saving a new raw-contact;
595 // replace the bogus ID with the new one that we actually saved the contact at.
596 if (rawContactId < 0) {
597 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800598 }
599
Jay Shrauner511561d2015-04-02 10:35:33 -0700600 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700601 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700602 succeeded = false;
603 }
Josh Garguse692e012012-01-18 14:53:11 -0800604 }
605 }
606
Josh Garguse5d3f892012-04-11 11:56:15 -0700607 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
608 if (callbackIntent != null) {
609 if (succeeded) {
610 // Mark the intent to indicate that the save was successful (even if the lookup URI
611 // is now null). For local contacts or the local profile, it's possible that the
612 // save triggered removal of the contact, so no lookup URI would exist..
613 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
614 }
615 callbackIntent.setData(lookupUri);
616 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800617 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800618 }
619
Josh Garguse692e012012-01-18 14:53:11 -0800620 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700621 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
622 * subsets, adds the returned array to "results".
623 *
624 * @return the size of the array, if not null; -1 when the array is null.
625 */
626 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
627 ContentProviderResult[] results, ContentResolver resolver)
628 throws RemoteException, OperationApplicationException {
629 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
630 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
631 subset.addAll(diff.subList(offset, offset + subsetCount));
632 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
633 .AUTHORITY, subset);
634 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
635 return -1;
636 }
637 for (ContentProviderResult c : subsetResult) {
638 results[offset++] = c;
639 }
640 return subsetResult.length;
641 }
642
643 /**
Josh Garguse692e012012-01-18 14:53:11 -0800644 * Save updated photo for the specified raw-contact.
645 * @return true for success, false for failure
646 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800647 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800648 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800649 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
650 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
651
benny.lin3a4e7a22014-01-08 10:58:08 +0800652 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800653 }
654
Josh Gargusef15c8e2012-01-30 16:42:02 -0800655 /**
656 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
657 */
Maurice Chu851222a2012-06-21 11:43:08 -0700658 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800659 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800660 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800661 long existingRawContactId = state.findRawContactId();
662 if (existingRawContactId != -1) {
663 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800664 }
665
Wenyi Wang67addcc2015-11-23 10:07:48 -0800666 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800667 }
668
669 /**
670 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
671 */
672 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800673 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800674 if (results == null) {
675 return -1;
676 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800677 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800678 final int numResults = results.length;
679 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800680 final CPOWrapper cpoWrapper = diffWrapper.get(i);
681 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
682 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
683 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800684 return ContentUris.parseId(results[i].uri);
685 }
686 }
687 return -1;
688 }
689
690 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700691 * Creates an intent that can be sent to this service to create a new group as
692 * well as add new members at the same time.
693 *
694 * @param context of the application
695 * @param account in which the group should be created
696 * @param label is the name of the group (cannot be null)
697 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
698 * should be added to the group
699 * @param callbackActivity is the activity to send the callback intent to
700 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700701 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700702 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700703 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700704 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800705 Intent serviceIntent = new Intent(context, ContactSaveService.class);
706 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
707 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
708 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700709 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800710 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700711 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700712
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800713 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700714 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800715 Intent callbackIntent = new Intent(context, callbackActivity);
716 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700717 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800718
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700719 return serviceIntent;
720 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800721
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800722 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700723 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
724 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
725 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
726 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700727 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800728
Katherine Kuan717e3432011-07-13 17:03:24 -0700729 // Create the new group
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700730 final Uri groupUri = mGroupsDao.create(label,
731 new AccountWithDataSet(accountName, accountType, dataSet));
732 final ContentResolver resolver = getContentResolver();
Katherine Kuan717e3432011-07-13 17:03:24 -0700733
734 // If there's no URI, then the insertion failed. Abort early because group members can't be
735 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800736 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700737 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800738 return;
739 }
740
Katherine Kuan717e3432011-07-13 17:03:24 -0700741 // Add new group members
742 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
743
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700744 ContentValues values = new ContentValues();
Katherine Kuan717e3432011-07-13 17:03:24 -0700745 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
746 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800747 values.clear();
748 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
749 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
750
751 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700752 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700753 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800754 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800755 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800756 }
757
758 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800759 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800760 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700761 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700762 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800763 Intent serviceIntent = new Intent(context, ContactSaveService.class);
764 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
765 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
766 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700767
768 // Callback intent will be invoked by the service once the group is renamed.
769 Intent callbackIntent = new Intent(context, callbackActivity);
770 callbackIntent.setAction(callbackAction);
771 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
772
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800773 return serviceIntent;
774 }
775
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800776 private void renameGroup(Intent intent) {
777 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
778 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
779
780 if (groupId == -1) {
781 Log.e(TAG, "Invalid arguments for renameGroup request");
782 return;
783 }
784
785 ContentValues values = new ContentValues();
786 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700787 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
788 getContentResolver().update(groupUri, values, null, null);
789
790 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
791 callbackIntent.setData(groupUri);
792 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800793 }
794
795 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800796 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800797 */
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700798 public static Intent createGroupDeletionIntent(Context context, long groupId) {
799 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800800 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800801 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700802
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800803 return serviceIntent;
804 }
805
806 private void deleteGroup(Intent intent) {
807 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
808 if (groupId == -1) {
809 Log.e(TAG, "Invalid arguments for deleteGroup request");
810 return;
811 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700812 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800813
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700814 final Intent callbackIntent = new Intent(BROADCAST_ACTION_GROUP_DELETED);
815 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
816 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
817 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
Walter Jang72f99882016-05-26 09:01:31 -0700818
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700819 mGroupsDao.delete(groupUri);
820
821 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
822 }
823
824 public static Intent createUndoIntent(Context context, Intent resultIntent) {
825 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
826 serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
827 serviceIntent.putExtras(resultIntent);
828 return serviceIntent;
829 }
830
831 private void undo(Intent intent) {
832 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
833 if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
834 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
Walter Jang72f99882016-05-26 09:01:31 -0700835 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800836 }
837
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700838
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800839 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700840 * Creates an intent that can be sent to this service to rename a group as
841 * well as add and remove members from the group.
842 *
843 * @param context of the application
844 * @param groupId of the group that should be modified
845 * @param newLabel is the updated name of the group (can be null if the name
846 * should not be updated)
847 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
848 * should be added to the group
849 * @param rawContactsToRemove is an array of raw contact IDs for contacts
850 * that should be removed from the group
851 * @param callbackActivity is the activity to send the callback intent to
852 * @param callbackAction is the intent action for the callback intent
853 */
854 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
855 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700856 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700857 Intent serviceIntent = new Intent(context, ContactSaveService.class);
858 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
859 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
860 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
861 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
862 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
863 rawContactsToRemove);
864
865 // Callback intent will be invoked by the service once the group is updated
866 Intent callbackIntent = new Intent(context, callbackActivity);
867 callbackIntent.setAction(callbackAction);
868 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
869
870 return serviceIntent;
871 }
872
873 private void updateGroup(Intent intent) {
874 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
875 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
876 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
877 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
878
879 if (groupId == -1) {
880 Log.e(TAG, "Invalid arguments for updateGroup request");
881 return;
882 }
883
884 final ContentResolver resolver = getContentResolver();
885 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
886
887 // Update group name if necessary
888 if (label != null) {
889 ContentValues values = new ContentValues();
890 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700891 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700892 }
893
Katherine Kuan717e3432011-07-13 17:03:24 -0700894 // Add and remove members if necessary
895 addMembersToGroup(resolver, rawContactsToAdd, groupId);
896 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
897
898 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
899 callbackIntent.setData(groupUri);
900 deliverCallback(callbackIntent);
901 }
902
Daniel Lehmann18958a22012-02-28 17:45:25 -0800903 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700904 long groupId) {
905 if (rawContactsToAdd == null) {
906 return;
907 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700908 for (long rawContactId : rawContactsToAdd) {
909 try {
910 final ArrayList<ContentProviderOperation> rawContactOperations =
911 new ArrayList<ContentProviderOperation>();
912
913 // Build an assert operation to ensure the contact is not already in the group
914 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
915 .newAssertQuery(Data.CONTENT_URI);
916 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
917 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
918 new String[] { String.valueOf(rawContactId),
919 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
920 assertBuilder.withExpectedCount(0);
921 rawContactOperations.add(assertBuilder.build());
922
923 // Build an insert operation to add the contact to the group
924 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
925 .newInsert(Data.CONTENT_URI);
926 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
927 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
928 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
929 rawContactOperations.add(insertBuilder.build());
930
931 if (DEBUG) {
932 for (ContentProviderOperation operation : rawContactOperations) {
933 Log.v(TAG, operation.toString());
934 }
935 }
936
937 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700938 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800939 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700940 }
941 } catch (RemoteException e) {
942 // Something went wrong, bail without success
943 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
944 String.valueOf(rawContactId), e);
945 } catch (OperationApplicationException e) {
946 // The assert could have failed because the contact is already in the group,
947 // just continue to the next contact
948 Log.w(TAG, "Assert failed in adding raw contact ID " +
949 String.valueOf(rawContactId) + ". Already exists in group " +
950 String.valueOf(groupId), e);
951 }
952 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700953 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700954
Daniel Lehmann18958a22012-02-28 17:45:25 -0800955 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700956 long groupId) {
957 if (rawContactsToRemove == null) {
958 return;
959 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700960 for (long rawContactId : rawContactsToRemove) {
961 // Apply the delete operation on the data row for the given raw contact's
962 // membership in the given group. If no contact matches the provided selection, then
963 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800964 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700965 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
966 new String[] { String.valueOf(rawContactId),
967 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
968 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700969 }
970
971 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800972 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800973 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800974 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
975 Intent serviceIntent = new Intent(context, ContactSaveService.class);
976 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
977 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
978 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
979
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800980 return serviceIntent;
981 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800982
983 private void setStarred(Intent intent) {
984 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
985 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
986 if (contactUri == null) {
987 Log.e(TAG, "Invalid arguments for setStarred request");
988 return;
989 }
990
991 final ContentValues values = new ContentValues(1);
992 values.put(Contacts.STARRED, value);
993 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700994
995 // Undemote the contact if necessary
996 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
997 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800998 if (c == null) {
999 return;
1000 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001001 try {
1002 if (c.moveToFirst()) {
1003 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -07001004
1005 // Don't bother undemoting if this contact is the user's profile.
1006 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -08001007 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -07001008 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001009 }
1010 } finally {
1011 c.close();
1012 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001013 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001014
1015 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -07001016 * Creates an intent that can be sent to this service to set the redirect to voicemail.
1017 */
1018 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
1019 boolean value) {
1020 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1021 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1022 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1023 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1024
1025 return serviceIntent;
1026 }
1027
1028 private void setSendToVoicemail(Intent intent) {
1029 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1030 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1031 if (contactUri == null) {
1032 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1033 return;
1034 }
1035
1036 final ContentValues values = new ContentValues(1);
1037 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1038 getContentResolver().update(contactUri, values, null, null);
1039 }
1040
1041 /**
1042 * Creates an intent that can be sent to this service to save the contact's ringtone.
1043 */
1044 public static Intent createSetRingtone(Context context, Uri contactUri,
1045 String value) {
1046 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1047 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1048 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1049 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1050
1051 return serviceIntent;
1052 }
1053
1054 private void setRingtone(Intent intent) {
1055 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1056 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1057 if (contactUri == null) {
1058 Log.e(TAG, "Invalid arguments for setRingtone");
1059 return;
1060 }
1061 ContentValues values = new ContentValues(1);
1062 values.put(Contacts.CUSTOM_RINGTONE, value);
1063 getContentResolver().update(contactUri, values, null, null);
1064 }
1065
1066 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001067 * Creates an intent that sets the selected data item as super primary (default)
1068 */
1069 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1070 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1071 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1072 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1073 return serviceIntent;
1074 }
1075
1076 private void setSuperPrimary(Intent intent) {
1077 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1078 if (dataId == -1) {
1079 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1080 return;
1081 }
1082
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001083 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001084 }
1085
1086 /**
1087 * Creates an intent that clears the primary flag of all data items that belong to the same
1088 * raw_contact as the given data item. Will only clear, if the data item was primary before
1089 * this call
1090 */
1091 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1092 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1093 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1094 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1095 return serviceIntent;
1096 }
1097
1098 private void clearPrimary(Intent intent) {
1099 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1100 if (dataId == -1) {
1101 Log.e(TAG, "Invalid arguments for clearPrimary request");
1102 return;
1103 }
1104
1105 // Update the primary values in the data record.
1106 ContentValues values = new ContentValues(1);
1107 values.put(Data.IS_SUPER_PRIMARY, 0);
1108 values.put(Data.IS_PRIMARY, 0);
1109
1110 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1111 values, null, null);
1112 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001113
1114 /**
1115 * Creates an intent that can be sent to this service to delete a contact.
1116 */
1117 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1118 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1119 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1120 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1121 return serviceIntent;
1122 }
1123
Brian Attwelld2962a32015-03-02 14:48:50 -08001124 /**
1125 * Creates an intent that can be sent to this service to delete multiple contacts.
1126 */
1127 public static Intent createDeleteMultipleContactsIntent(Context context,
1128 long[] contactIds) {
1129 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1130 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1131 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1132 return serviceIntent;
1133 }
1134
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001135 private void deleteContact(Intent intent) {
1136 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1137 if (contactUri == null) {
1138 Log.e(TAG, "Invalid arguments for deleteContact request");
1139 return;
1140 }
1141
1142 getContentResolver().delete(contactUri, null, null);
1143 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001144
Brian Attwelld2962a32015-03-02 14:48:50 -08001145 private void deleteMultipleContacts(Intent intent) {
1146 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1147 if (contactIds == null) {
1148 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1149 return;
1150 }
1151 for (long contactId : contactIds) {
1152 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1153 getContentResolver().delete(contactUri, null, null);
1154 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001155 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1156 .contacts_deleted_toast, contactIds.length);
1157 mMainHandler.post(new Runnable() {
1158 @Override
1159 public void run() {
1160 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1161 .show();
1162 }
1163 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001164 }
1165
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001166 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001167 * Creates an intent that can be sent to this service to split a contact into it's constituent
Gary Mai53fe0d22016-07-26 17:23:53 -07001168 * pieces. This will set the raw contact ids to TYPE_AUTOMATIC for AggregationExceptions so
1169 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001170 */
1171 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1172 ResultReceiver receiver) {
1173 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1174 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1175 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1176 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1177 return serviceIntent;
1178 }
1179
1180 private void splitContact(Intent intent) {
1181 final long rawContactIds[][] = (long[][]) intent
1182 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001183 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Mai7efa9942016-05-12 11:26:49 -07001184 if (rawContactIds == null) {
1185 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001186 if (receiver != null) {
1187 receiver.send(BAD_ARGUMENTS, new Bundle());
1188 }
Gary Mai7efa9942016-05-12 11:26:49 -07001189 return;
1190 }
1191 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1192 final ContentResolver resolver = getContentResolver();
1193 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001194 for (int i = 0; i < rawContactIds.length; i++) {
1195 for (int j = 0; j < rawContactIds.length; j++) {
1196 if (i != j) {
1197 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
1198 if (receiver != null) {
1199 receiver.send(CP2_ERROR, new Bundle());
1200 return;
1201 }
1202 }
1203 }
1204 }
1205 }
1206 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1207 if (receiver != null) {
1208 receiver.send(CP2_ERROR, new Bundle());
1209 }
1210 return;
1211 }
1212 if (receiver != null) {
1213 receiver.send(CONTACTS_SPLIT, new Bundle());
1214 } else {
1215 showToast(R.string.contactUnlinkedToast);
1216 }
1217 }
1218
1219 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001220 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001221 * and {@param rawContactIds2} to {@param operations}.
1222 * @return false if an error occurred, true otherwise.
1223 */
1224 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
1225 long[] rawContactIds1, long[] rawContactIds2) {
1226 if (rawContactIds1 == null || rawContactIds2 == null) {
1227 Log.e(TAG, "Invalid arguments for splitContact request");
1228 return false;
1229 }
1230 // For each pair of raw contacts, insert an aggregation exception
1231 final ContentResolver resolver = getContentResolver();
1232 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1233 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1234 for (int i = 0; i < rawContactIds1.length; i++) {
1235 for (int j = 0; j < rawContactIds2.length; j++) {
1236 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
1237 // Before we get to 500 we need to flush the operations list
1238 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1239 if (!applyOperations(resolver, operations)) {
1240 return false;
1241 }
1242 operations.clear();
1243 }
1244 }
1245 }
1246 return true;
1247 }
1248
1249 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001250 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001251 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001252 */
1253 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001254 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001255 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1256 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1257 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1258 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001259
1260 // Callback intent will be invoked by the service once the contacts are joined.
1261 Intent callbackIntent = new Intent(context, callbackActivity);
1262 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001263 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1264
1265 return serviceIntent;
1266 }
1267
Brian Attwelld3946ca2015-03-03 11:13:49 -08001268 /**
1269 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1270 * No special attention is paid to where the resulting contact's name is taken from.
1271 */
Gary Mai7efa9942016-05-12 11:26:49 -07001272 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1273 ResultReceiver receiver) {
1274 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001275 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1276 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001277 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001278 return serviceIntent;
1279 }
1280
Gary Mai7efa9942016-05-12 11:26:49 -07001281 /**
1282 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1283 * No special attention is paid to where the resulting contact's name is taken from.
1284 */
1285 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1286 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1287 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001288
1289 private interface JoinContactQuery {
1290 String[] PROJECTION = {
1291 RawContacts._ID,
1292 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001293 RawContacts.DISPLAY_NAME_SOURCE,
1294 };
1295
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001296 int _ID = 0;
1297 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001298 int DISPLAY_NAME_SOURCE = 2;
1299 }
1300
1301 private interface ContactEntityQuery {
1302 String[] PROJECTION = {
1303 Contacts.Entity.DATA_ID,
1304 Contacts.Entity.CONTACT_ID,
1305 Contacts.Entity.IS_SUPER_PRIMARY,
1306 };
1307 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1308 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1309 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1310 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1311
1312 int DATA_ID = 0;
1313 int CONTACT_ID = 1;
1314 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001315 }
1316
Brian Attwelld3946ca2015-03-03 11:13:49 -08001317 private void joinSeveralContacts(Intent intent) {
1318 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001319
Gary Mai7efa9942016-05-12 11:26:49 -07001320 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001321
Brian Attwelld3946ca2015-03-03 11:13:49 -08001322 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001323 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1324 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001325 if (rawContactIds == null) {
1326 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001327 if (receiver != null) {
1328 receiver.send(BAD_ARGUMENTS, new Bundle());
1329 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001330 return;
1331 }
1332
Brian Attwelld3946ca2015-03-03 11:13:49 -08001333 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001334 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001335 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1336 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1337 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001338 for (int i = 0; i < rawContactIds.length; i++) {
1339 for (int j = 0; j < rawContactIds.length; j++) {
1340 if (i != j) {
1341 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1342 }
Walter Jang0653de32015-07-24 12:12:40 -07001343 // Before we get to 500 we need to flush the operations list
1344 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001345 if (!applyOperations(resolver, operations)) {
1346 if (receiver != null) {
1347 receiver.send(CP2_ERROR, new Bundle());
1348 }
Walter Jang0653de32015-07-24 12:12:40 -07001349 return;
1350 }
1351 operations.clear();
1352 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001353 }
1354 }
Gary Mai7efa9942016-05-12 11:26:49 -07001355 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1356 if (receiver != null) {
1357 receiver.send(CP2_ERROR, new Bundle());
1358 }
Walter Jang0653de32015-07-24 12:12:40 -07001359 return;
1360 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001361
Gary Mai7efa9942016-05-12 11:26:49 -07001362 if (receiver != null) {
1363 final Bundle result = new Bundle();
1364 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001365 result.putString(EXTRA_DISPLAY_NAME, queryNameOfLinkedContacts(contactIds));
Gary Mai7efa9942016-05-12 11:26:49 -07001366 receiver.send(CONTACTS_LINKED, result);
1367 } else {
1368 showToast(R.string.contactsJoinedMessage);
1369 }
Walter Jang0653de32015-07-24 12:12:40 -07001370 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001371
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001372 // Get the display name of the top-level contact after the contacts have been linked.
1373 private String queryNameOfLinkedContacts(long[] contactIds) {
1374 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1375 final String[] whereArgs = new String[contactIds.length];
1376 for (int i = 0; i < contactIds.length; i++) {
1377 whereArgs[i] = String.valueOf(contactIds[i]);
1378 whereBuilder.append("?,");
1379 }
1380 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1381 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
1382 new String[]{Contacts.DISPLAY_NAME}, whereBuilder.toString(), whereArgs, null);
1383 try {
1384 if (cursor.moveToFirst()) {
1385 return cursor.getString(0);
1386 }
1387 return null;
1388 } finally {
1389 cursor.close();
1390 }
1391 }
1392
1393
Walter Jang0653de32015-07-24 12:12:40 -07001394 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001395 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001396 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001397 try {
1398 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001399 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001400 } catch (RemoteException | OperationApplicationException e) {
1401 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1402 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001403 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001404 }
1405 }
1406
Brian Attwelld3946ca2015-03-03 11:13:49 -08001407 private void joinContacts(Intent intent) {
1408 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1409 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001410
1411 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001412 // in the join UIs.
1413 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1414 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001415 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001416 return;
1417 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001418
Brian Attwell548f5c62015-01-27 17:46:46 -08001419 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001420
1421 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001422 for (int i = 0; i < rawContactIds.length; i++) {
1423 for (int j = 0; j < rawContactIds.length; j++) {
1424 if (i != j) {
1425 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1426 }
1427 }
1428 }
1429
Brian Attwelld3946ca2015-03-03 11:13:49 -08001430 final ContentResolver resolver = getContentResolver();
1431
Brian Attwell548f5c62015-01-27 17:46:46 -08001432 // Use the name for contactId1 as the name for the newly aggregated contact.
1433 final Uri contactId1Uri = ContentUris.withAppendedId(
1434 Contacts.CONTENT_URI, contactId1);
1435 final Uri entityUri = Uri.withAppendedPath(
1436 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1437 Cursor c = resolver.query(entityUri,
1438 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1439 if (c == null) {
1440 Log.e(TAG, "Unable to open Contacts DB cursor");
1441 showToast(R.string.contactSavedErrorToast);
1442 return;
1443 }
1444 long dataIdToAddSuperPrimary = -1;
1445 try {
1446 if (c.moveToFirst()) {
1447 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1448 }
1449 } finally {
1450 c.close();
1451 }
1452
1453 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1454 // display name does not change as a result of the join.
1455 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001456 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001457 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1458 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1459 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001460 operations.add(builder.build());
1461 }
1462
1463 boolean success = false;
1464 // Apply all aggregation exceptions as one batch
1465 try {
1466 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001467 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001468 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001469 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001470 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001471 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001472 }
1473
1474 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1475 if (success) {
1476 Uri uri = RawContacts.getContactLookupUri(resolver,
1477 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1478 callbackIntent.setData(uri);
1479 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001480 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001481 }
1482
Gary Mai7efa9942016-05-12 11:26:49 -07001483 /**
1484 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1485 * array of the return value holds an array of raw contact ids for one contactId.
1486 * @param contactIds
1487 * @return
1488 */
1489 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1490 final long[][] rawContactIds = new long[contactIds.length][];
1491 for (int i = 0; i < contactIds.length; i++) {
1492 rawContactIds[i] = getRawContactIds(contactIds[i]);
1493 }
1494 return rawContactIds;
1495 }
1496
1497 /**
1498 * Gets the raw contact ids associated with {@param contactId}.
1499 * @param contactId
1500 * @return Array of raw contact ids.
1501 */
1502 private long[] getRawContactIds(long contactId) {
1503 final ContentResolver resolver = getContentResolver();
1504 long rawContactIds[];
1505
1506 final StringBuilder queryBuilder = new StringBuilder();
1507 queryBuilder.append(RawContacts.CONTACT_ID)
1508 .append("=")
1509 .append(String.valueOf(contactId));
1510
1511 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1512 JoinContactQuery.PROJECTION,
1513 queryBuilder.toString(),
1514 null, null);
1515 if (c == null) {
1516 Log.e(TAG, "Unable to open Contacts DB cursor");
1517 return null;
1518 }
1519 try {
1520 rawContactIds = new long[c.getCount()];
1521 for (int i = 0; i < rawContactIds.length; i++) {
1522 c.moveToPosition(i);
1523 final long rawContactId = c.getLong(JoinContactQuery._ID);
1524 rawContactIds[i] = rawContactId;
1525 }
1526 } finally {
1527 c.close();
1528 }
1529 return rawContactIds;
1530 }
1531
Brian Attwelld3946ca2015-03-03 11:13:49 -08001532 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1533 if (contactIds == null) {
1534 return null;
1535 }
1536
Brian Attwell548f5c62015-01-27 17:46:46 -08001537 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001538
1539 final StringBuilder queryBuilder = new StringBuilder();
1540 final String stringContactIds[] = new String[contactIds.length];
1541 for (int i = 0; i < contactIds.length; i++) {
1542 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1543 stringContactIds[i] = String.valueOf(contactIds[i]);
1544 if (contactIds[i] == -1) {
1545 return null;
1546 }
1547 if (i == contactIds.length -1) {
1548 break;
1549 }
1550 queryBuilder.append(" OR ");
1551 }
1552
Brian Attwell548f5c62015-01-27 17:46:46 -08001553 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1554 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001555 queryBuilder.toString(),
1556 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001557 if (c == null) {
1558 Log.e(TAG, "Unable to open Contacts DB cursor");
1559 showToast(R.string.contactSavedErrorToast);
1560 return null;
1561 }
Gary Mai7efa9942016-05-12 11:26:49 -07001562 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001563 try {
1564 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001565 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001566 return null;
1567 }
1568 rawContactIds = new long[c.getCount()];
1569 for (int i = 0; i < rawContactIds.length; i++) {
1570 c.moveToPosition(i);
1571 long rawContactId = c.getLong(JoinContactQuery._ID);
1572 rawContactIds[i] = rawContactId;
1573 }
1574 } finally {
1575 c.close();
1576 }
1577 return rawContactIds;
1578 }
1579
Brian Attwelld3946ca2015-03-03 11:13:49 -08001580 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1581 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1582 }
1583
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001584 /**
1585 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1586 */
1587 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1588 long rawContactId1, long rawContactId2) {
1589 Builder builder =
1590 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1591 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1592 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1593 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1594 operations.add(builder.build());
1595 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001596
1597 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001598 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} ContentProviderOperation.
Gary Mai7efa9942016-05-12 11:26:49 -07001599 */
1600 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
1601 long rawContactId1, long rawContactId2) {
1602 final Builder builder =
1603 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Mai53fe0d22016-07-26 17:23:53 -07001604 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001605 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1606 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1607 operations.add(builder.build());
1608 }
1609
1610 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001611 * Shows a toast on the UI thread.
1612 */
1613 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001614 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001615
1616 @Override
1617 public void run() {
1618 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1619 }
1620 });
1621 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001622
1623 private void deliverCallback(final Intent callbackIntent) {
1624 mMainHandler.post(new Runnable() {
1625
1626 @Override
1627 public void run() {
1628 deliverCallbackOnUiThread(callbackIntent);
1629 }
1630 });
1631 }
1632
1633 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1634 // TODO: this assumes that if there are multiple instances of the same
1635 // activity registered, the last one registered is the one waiting for
1636 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001637 for (Listener listener : sListeners) {
1638 if (callbackIntent.getComponent().equals(
1639 ((Activity) listener).getIntent().getComponent())) {
1640 listener.onServiceCompleted(callbackIntent);
1641 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001642 }
1643 }
1644 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001645
1646 public interface GroupsDao {
1647 Uri create(String title, AccountWithDataSet account);
1648 int delete(Uri groupUri);
1649 Bundle captureDeletionUndoData(Uri groupUri);
1650 Uri undoDeletion(Bundle undoData);
1651 }
1652
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001653 public static class GroupsDaoImpl implements GroupsDao {
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001654 public static final String KEY_GROUP_DATA = "groupData";
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001655 public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
1656
1657 private static final String TAG = "GroupsDao";
1658 private final Context context;
1659 private final ContentResolver contentResolver;
1660
1661 public GroupsDaoImpl(Context context) {
1662 this(context, context.getContentResolver());
1663 }
1664
1665 public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
1666 this.context = context;
1667 this.contentResolver = contentResolver;
1668 }
1669
1670 public Bundle captureDeletionUndoData(Uri groupUri) {
1671 final long groupId = ContentUris.parseId(groupUri);
1672 final Bundle result = new Bundle();
1673
1674 final Cursor cursor = contentResolver.query(groupUri,
1675 new String[]{
1676 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
1677 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
1678 Groups.SHOULD_SYNC
1679 },
1680 Groups.DELETED + "=?", new String[] { "0" }, null);
1681 try {
1682 if (cursor.moveToFirst()) {
1683 final ContentValues groupValues = new ContentValues();
1684 DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
1685 result.putParcelable(KEY_GROUP_DATA, groupValues);
1686 } else {
1687 // Group doesn't exist.
1688 return result;
1689 }
1690 } finally {
1691 cursor.close();
1692 }
1693
1694 final Cursor membersCursor = contentResolver.query(
1695 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
1696 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1697 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
1698 final long[] memberIds = new long[membersCursor.getCount()];
1699 int i = 0;
1700 while (membersCursor.moveToNext()) {
1701 memberIds[i++] = membersCursor.getLong(0);
1702 }
1703 result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
1704 return result;
1705 }
1706
1707 public Uri undoDeletion(Bundle deletedGroupData) {
1708 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
1709 if (groupData == null) {
1710 return null;
1711 }
1712 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
1713 final long groupId = ContentUris.parseId(groupUri);
1714
1715 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
1716 if (memberIds == null) {
1717 return groupUri;
1718 }
1719 final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
1720 for (int i = 0; i < memberIds.length; i++) {
1721 memberInsertions[i] = new ContentValues();
1722 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
1723 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1724 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
1725 }
1726 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
1727 if (inserted != memberIds.length) {
1728 Log.e(TAG, "Could not recover some members for group deletion undo");
1729 }
1730
1731 return groupUri;
1732 }
1733
1734 public Uri create(String title, AccountWithDataSet account) {
1735 final ContentValues values = new ContentValues();
1736 values.put(Groups.TITLE, title);
1737 values.put(Groups.ACCOUNT_NAME, account.name);
1738 values.put(Groups.ACCOUNT_TYPE, account.type);
1739 values.put(Groups.DATA_SET, account.dataSet);
1740 return contentResolver.insert(Groups.CONTENT_URI, values);
1741 }
1742
1743 public int delete(Uri groupUri) {
1744 return contentResolver.delete(groupUri, null, null);
1745 }
1746 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001747}