blob: d4da5882137de558fb14b69c708c2e39c250218d [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
Gary Mai363af602016-09-28 10:01:23 -070054import com.android.contacts.activities.ContactEditorActivity;
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) {
Gary Mai363af602016-09-28 10:01:23 -0700218 case ContactEditorActivity.ContactEditor.SaveMode.SPLIT:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800219 resId = R.string.contactUnlinkErrorToast;
220 break;
Gary Mai363af602016-09-28 10:01:23 -0700221 case ContactEditorActivity.ContactEditor.SaveMode.RELOAD:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800222 resId = R.string.contactJoinErrorToast;
223 break;
Gary Mai363af602016-09-28 10:01:23 -0700224 case ContactEditorActivity.ContactEditor.SaveMode.CLOSE:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800225 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
Walter Jang8bac28b2016-08-30 10:34:55 -0700746 // with the way other intent extras that are passed to the
Gary Mai363af602016-09-28 10:01:23 -0700747 // {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800748 values.clear();
749 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
750 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
751
752 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700753 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700754 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800755 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800756 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800757 }
758
759 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800760 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800761 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700762 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700763 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800764 Intent serviceIntent = new Intent(context, ContactSaveService.class);
765 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
766 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
767 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700768
769 // Callback intent will be invoked by the service once the group is renamed.
770 Intent callbackIntent = new Intent(context, callbackActivity);
771 callbackIntent.setAction(callbackAction);
772 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
773
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800774 return serviceIntent;
775 }
776
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800777 private void renameGroup(Intent intent) {
778 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
779 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
780
781 if (groupId == -1) {
782 Log.e(TAG, "Invalid arguments for renameGroup request");
783 return;
784 }
785
786 ContentValues values = new ContentValues();
787 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700788 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
789 getContentResolver().update(groupUri, values, null, null);
790
791 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
792 callbackIntent.setData(groupUri);
793 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800794 }
795
796 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800797 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800798 */
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700799 public static Intent createGroupDeletionIntent(Context context, long groupId) {
800 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800801 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800802 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700803
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800804 return serviceIntent;
805 }
806
807 private void deleteGroup(Intent intent) {
808 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
809 if (groupId == -1) {
810 Log.e(TAG, "Invalid arguments for deleteGroup request");
811 return;
812 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700813 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800814
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700815 final Intent callbackIntent = new Intent(BROADCAST_ACTION_GROUP_DELETED);
816 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
817 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
818 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
Walter Jang72f99882016-05-26 09:01:31 -0700819
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700820 mGroupsDao.delete(groupUri);
821
822 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
823 }
824
825 public static Intent createUndoIntent(Context context, Intent resultIntent) {
826 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
827 serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
828 serviceIntent.putExtras(resultIntent);
829 return serviceIntent;
830 }
831
832 private void undo(Intent intent) {
833 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
834 if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
835 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
Walter Jang72f99882016-05-26 09:01:31 -0700836 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800837 }
838
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700839
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800840 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700841 * Creates an intent that can be sent to this service to rename a group as
842 * well as add and remove members from the group.
843 *
844 * @param context of the application
845 * @param groupId of the group that should be modified
846 * @param newLabel is the updated name of the group (can be null if the name
847 * should not be updated)
848 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
849 * should be added to the group
850 * @param rawContactsToRemove is an array of raw contact IDs for contacts
851 * that should be removed from the group
852 * @param callbackActivity is the activity to send the callback intent to
853 * @param callbackAction is the intent action for the callback intent
854 */
855 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
856 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700857 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700858 Intent serviceIntent = new Intent(context, ContactSaveService.class);
859 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
860 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
861 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
862 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
863 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
864 rawContactsToRemove);
865
866 // Callback intent will be invoked by the service once the group is updated
867 Intent callbackIntent = new Intent(context, callbackActivity);
868 callbackIntent.setAction(callbackAction);
869 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
870
871 return serviceIntent;
872 }
873
874 private void updateGroup(Intent intent) {
875 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
876 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
877 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
878 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
879
880 if (groupId == -1) {
881 Log.e(TAG, "Invalid arguments for updateGroup request");
882 return;
883 }
884
885 final ContentResolver resolver = getContentResolver();
886 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
887
888 // Update group name if necessary
889 if (label != null) {
890 ContentValues values = new ContentValues();
891 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700892 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700893 }
894
Katherine Kuan717e3432011-07-13 17:03:24 -0700895 // Add and remove members if necessary
896 addMembersToGroup(resolver, rawContactsToAdd, groupId);
897 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
898
899 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
900 callbackIntent.setData(groupUri);
901 deliverCallback(callbackIntent);
902 }
903
Daniel Lehmann18958a22012-02-28 17:45:25 -0800904 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700905 long groupId) {
906 if (rawContactsToAdd == null) {
907 return;
908 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700909 for (long rawContactId : rawContactsToAdd) {
910 try {
911 final ArrayList<ContentProviderOperation> rawContactOperations =
912 new ArrayList<ContentProviderOperation>();
913
914 // Build an assert operation to ensure the contact is not already in the group
915 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
916 .newAssertQuery(Data.CONTENT_URI);
917 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
918 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
919 new String[] { String.valueOf(rawContactId),
920 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
921 assertBuilder.withExpectedCount(0);
922 rawContactOperations.add(assertBuilder.build());
923
924 // Build an insert operation to add the contact to the group
925 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
926 .newInsert(Data.CONTENT_URI);
927 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
928 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
929 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
930 rawContactOperations.add(insertBuilder.build());
931
932 if (DEBUG) {
933 for (ContentProviderOperation operation : rawContactOperations) {
934 Log.v(TAG, operation.toString());
935 }
936 }
937
938 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700939 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800940 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700941 }
942 } catch (RemoteException e) {
943 // Something went wrong, bail without success
944 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
945 String.valueOf(rawContactId), e);
946 } catch (OperationApplicationException e) {
947 // The assert could have failed because the contact is already in the group,
948 // just continue to the next contact
949 Log.w(TAG, "Assert failed in adding raw contact ID " +
950 String.valueOf(rawContactId) + ". Already exists in group " +
951 String.valueOf(groupId), e);
952 }
953 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700954 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700955
Daniel Lehmann18958a22012-02-28 17:45:25 -0800956 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700957 long groupId) {
958 if (rawContactsToRemove == null) {
959 return;
960 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700961 for (long rawContactId : rawContactsToRemove) {
962 // Apply the delete operation on the data row for the given raw contact's
963 // membership in the given group. If no contact matches the provided selection, then
964 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800965 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700966 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
967 new String[] { String.valueOf(rawContactId),
968 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
969 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700970 }
971
972 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800973 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800974 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800975 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
976 Intent serviceIntent = new Intent(context, ContactSaveService.class);
977 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
978 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
979 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
980
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800981 return serviceIntent;
982 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800983
984 private void setStarred(Intent intent) {
985 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
986 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
987 if (contactUri == null) {
988 Log.e(TAG, "Invalid arguments for setStarred request");
989 return;
990 }
991
992 final ContentValues values = new ContentValues(1);
993 values.put(Contacts.STARRED, value);
994 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700995
996 // Undemote the contact if necessary
997 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
998 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800999 if (c == null) {
1000 return;
1001 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001002 try {
1003 if (c.moveToFirst()) {
1004 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -07001005
1006 // Don't bother undemoting if this contact is the user's profile.
1007 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -08001008 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -07001009 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001010 }
1011 } finally {
1012 c.close();
1013 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001014 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001015
1016 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -07001017 * Creates an intent that can be sent to this service to set the redirect to voicemail.
1018 */
1019 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
1020 boolean value) {
1021 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1022 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1023 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1024 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1025
1026 return serviceIntent;
1027 }
1028
1029 private void setSendToVoicemail(Intent intent) {
1030 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1031 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1032 if (contactUri == null) {
1033 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1034 return;
1035 }
1036
1037 final ContentValues values = new ContentValues(1);
1038 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1039 getContentResolver().update(contactUri, values, null, null);
1040 }
1041
1042 /**
1043 * Creates an intent that can be sent to this service to save the contact's ringtone.
1044 */
1045 public static Intent createSetRingtone(Context context, Uri contactUri,
1046 String value) {
1047 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1048 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1049 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1050 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1051
1052 return serviceIntent;
1053 }
1054
1055 private void setRingtone(Intent intent) {
1056 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1057 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1058 if (contactUri == null) {
1059 Log.e(TAG, "Invalid arguments for setRingtone");
1060 return;
1061 }
1062 ContentValues values = new ContentValues(1);
1063 values.put(Contacts.CUSTOM_RINGTONE, value);
1064 getContentResolver().update(contactUri, values, null, null);
1065 }
1066
1067 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001068 * Creates an intent that sets the selected data item as super primary (default)
1069 */
1070 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1071 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1072 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1073 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1074 return serviceIntent;
1075 }
1076
1077 private void setSuperPrimary(Intent intent) {
1078 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1079 if (dataId == -1) {
1080 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1081 return;
1082 }
1083
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001084 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001085 }
1086
1087 /**
1088 * Creates an intent that clears the primary flag of all data items that belong to the same
1089 * raw_contact as the given data item. Will only clear, if the data item was primary before
1090 * this call
1091 */
1092 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1093 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1094 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1095 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1096 return serviceIntent;
1097 }
1098
1099 private void clearPrimary(Intent intent) {
1100 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1101 if (dataId == -1) {
1102 Log.e(TAG, "Invalid arguments for clearPrimary request");
1103 return;
1104 }
1105
1106 // Update the primary values in the data record.
1107 ContentValues values = new ContentValues(1);
1108 values.put(Data.IS_SUPER_PRIMARY, 0);
1109 values.put(Data.IS_PRIMARY, 0);
1110
1111 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1112 values, null, null);
1113 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001114
1115 /**
1116 * Creates an intent that can be sent to this service to delete a contact.
1117 */
1118 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1119 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1120 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1121 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1122 return serviceIntent;
1123 }
1124
Brian Attwelld2962a32015-03-02 14:48:50 -08001125 /**
1126 * Creates an intent that can be sent to this service to delete multiple contacts.
1127 */
1128 public static Intent createDeleteMultipleContactsIntent(Context context,
1129 long[] contactIds) {
1130 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1131 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1132 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1133 return serviceIntent;
1134 }
1135
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001136 private void deleteContact(Intent intent) {
1137 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1138 if (contactUri == null) {
1139 Log.e(TAG, "Invalid arguments for deleteContact request");
1140 return;
1141 }
1142
1143 getContentResolver().delete(contactUri, null, null);
1144 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001145
Brian Attwelld2962a32015-03-02 14:48:50 -08001146 private void deleteMultipleContacts(Intent intent) {
1147 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1148 if (contactIds == null) {
1149 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1150 return;
1151 }
1152 for (long contactId : contactIds) {
1153 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1154 getContentResolver().delete(contactUri, null, null);
1155 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001156 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1157 .contacts_deleted_toast, contactIds.length);
1158 mMainHandler.post(new Runnable() {
1159 @Override
1160 public void run() {
1161 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1162 .show();
1163 }
1164 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001165 }
1166
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001167 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001168 * 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 -07001169 * pieces. This will set the raw contact ids to TYPE_AUTOMATIC for AggregationExceptions so
1170 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001171 */
1172 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1173 ResultReceiver receiver) {
1174 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1175 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1176 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1177 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1178 return serviceIntent;
1179 }
1180
1181 private void splitContact(Intent intent) {
1182 final long rawContactIds[][] = (long[][]) intent
1183 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001184 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Mai7efa9942016-05-12 11:26:49 -07001185 if (rawContactIds == null) {
1186 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001187 if (receiver != null) {
1188 receiver.send(BAD_ARGUMENTS, new Bundle());
1189 }
Gary Mai7efa9942016-05-12 11:26:49 -07001190 return;
1191 }
1192 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1193 final ContentResolver resolver = getContentResolver();
1194 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001195 for (int i = 0; i < rawContactIds.length; i++) {
1196 for (int j = 0; j < rawContactIds.length; j++) {
1197 if (i != j) {
1198 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
1199 if (receiver != null) {
1200 receiver.send(CP2_ERROR, new Bundle());
1201 return;
1202 }
1203 }
1204 }
1205 }
1206 }
1207 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1208 if (receiver != null) {
1209 receiver.send(CP2_ERROR, new Bundle());
1210 }
1211 return;
1212 }
1213 if (receiver != null) {
1214 receiver.send(CONTACTS_SPLIT, new Bundle());
1215 } else {
1216 showToast(R.string.contactUnlinkedToast);
1217 }
1218 }
1219
1220 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001221 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001222 * and {@param rawContactIds2} to {@param operations}.
1223 * @return false if an error occurred, true otherwise.
1224 */
1225 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
1226 long[] rawContactIds1, long[] rawContactIds2) {
1227 if (rawContactIds1 == null || rawContactIds2 == null) {
1228 Log.e(TAG, "Invalid arguments for splitContact request");
1229 return false;
1230 }
1231 // For each pair of raw contacts, insert an aggregation exception
1232 final ContentResolver resolver = getContentResolver();
1233 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1234 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1235 for (int i = 0; i < rawContactIds1.length; i++) {
1236 for (int j = 0; j < rawContactIds2.length; j++) {
1237 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
1238 // Before we get to 500 we need to flush the operations list
1239 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1240 if (!applyOperations(resolver, operations)) {
1241 return false;
1242 }
1243 operations.clear();
1244 }
1245 }
1246 }
1247 return true;
1248 }
1249
1250 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001251 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001252 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001253 */
1254 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001255 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001256 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1257 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1258 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1259 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001260
1261 // Callback intent will be invoked by the service once the contacts are joined.
1262 Intent callbackIntent = new Intent(context, callbackActivity);
1263 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001264 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1265
1266 return serviceIntent;
1267 }
1268
Brian Attwelld3946ca2015-03-03 11:13:49 -08001269 /**
1270 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1271 * No special attention is paid to where the resulting contact's name is taken from.
1272 */
Gary Mai7efa9942016-05-12 11:26:49 -07001273 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1274 ResultReceiver receiver) {
1275 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001276 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1277 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001278 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001279 return serviceIntent;
1280 }
1281
Gary Mai7efa9942016-05-12 11:26:49 -07001282 /**
1283 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1284 * No special attention is paid to where the resulting contact's name is taken from.
1285 */
1286 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1287 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1288 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001289
1290 private interface JoinContactQuery {
1291 String[] PROJECTION = {
1292 RawContacts._ID,
1293 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001294 RawContacts.DISPLAY_NAME_SOURCE,
1295 };
1296
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001297 int _ID = 0;
1298 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001299 int DISPLAY_NAME_SOURCE = 2;
1300 }
1301
1302 private interface ContactEntityQuery {
1303 String[] PROJECTION = {
1304 Contacts.Entity.DATA_ID,
1305 Contacts.Entity.CONTACT_ID,
1306 Contacts.Entity.IS_SUPER_PRIMARY,
1307 };
1308 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1309 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1310 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1311 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1312
1313 int DATA_ID = 0;
1314 int CONTACT_ID = 1;
1315 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001316 }
1317
Brian Attwelld3946ca2015-03-03 11:13:49 -08001318 private void joinSeveralContacts(Intent intent) {
1319 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001320
Gary Mai7efa9942016-05-12 11:26:49 -07001321 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001322
Brian Attwelld3946ca2015-03-03 11:13:49 -08001323 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001324 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1325 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001326 if (rawContactIds == null) {
1327 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001328 if (receiver != null) {
1329 receiver.send(BAD_ARGUMENTS, new Bundle());
1330 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001331 return;
1332 }
1333
Brian Attwelld3946ca2015-03-03 11:13:49 -08001334 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001335 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001336 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1337 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1338 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001339 for (int i = 0; i < rawContactIds.length; i++) {
1340 for (int j = 0; j < rawContactIds.length; j++) {
1341 if (i != j) {
1342 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1343 }
Walter Jang0653de32015-07-24 12:12:40 -07001344 // Before we get to 500 we need to flush the operations list
1345 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001346 if (!applyOperations(resolver, operations)) {
1347 if (receiver != null) {
1348 receiver.send(CP2_ERROR, new Bundle());
1349 }
Walter Jang0653de32015-07-24 12:12:40 -07001350 return;
1351 }
1352 operations.clear();
1353 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001354 }
1355 }
Gary Mai7efa9942016-05-12 11:26:49 -07001356 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1357 if (receiver != null) {
1358 receiver.send(CP2_ERROR, new Bundle());
1359 }
Walter Jang0653de32015-07-24 12:12:40 -07001360 return;
1361 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001362
John Shaoa3c507a2016-09-13 14:26:17 -07001363
1364 final String name = queryNameOfLinkedContacts(contactIds);
1365 if (name != null) {
1366 if (receiver != null) {
1367 final Bundle result = new Bundle();
1368 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1369 result.putString(EXTRA_DISPLAY_NAME, name);
1370 receiver.send(CONTACTS_LINKED, result);
1371 } else {
1372 showToast(R.string.contactsJoinedMessage);
1373 }
Gary Mai7efa9942016-05-12 11:26:49 -07001374 } else {
John Shaoa3c507a2016-09-13 14:26:17 -07001375 if (receiver != null) {
1376 receiver.send(CP2_ERROR, new Bundle());
1377 }
1378 showToast(R.string.contactJoinErrorToast);
Gary Mai7efa9942016-05-12 11:26:49 -07001379 }
Walter Jang0653de32015-07-24 12:12:40 -07001380 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001381
John Shaoa3c507a2016-09-13 14:26:17 -07001382 /** Get the display name of the top-level contact after the contacts have been linked. */
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001383 private String queryNameOfLinkedContacts(long[] contactIds) {
1384 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1385 final String[] whereArgs = new String[contactIds.length];
1386 for (int i = 0; i < contactIds.length; i++) {
1387 whereArgs[i] = String.valueOf(contactIds[i]);
1388 whereBuilder.append("?,");
1389 }
1390 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1391 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
John Shaoa3c507a2016-09-13 14:26:17 -07001392 new String[]{Contacts._ID, Contacts.DISPLAY_NAME},
1393 whereBuilder.toString(), whereArgs, null);
1394
1395 String name = null;
1396 long contactId = 0;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001397 try {
1398 if (cursor.moveToFirst()) {
John Shaoa3c507a2016-09-13 14:26:17 -07001399 contactId = cursor.getLong(0);
1400 name = cursor.getString(1);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001401 }
John Shaoa3c507a2016-09-13 14:26:17 -07001402 while(cursor.moveToNext()) {
1403 if (cursor.getLong(0) != contactId) {
1404 return null;
1405 }
1406 }
1407 return name == null ? "" : name;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001408 } finally {
John Shaoa3c507a2016-09-13 14:26:17 -07001409 if (cursor != null) {
1410 cursor.close();
1411 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001412 }
1413 }
1414
Walter Jang0653de32015-07-24 12:12:40 -07001415 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001416 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001417 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001418 try {
John Shaoa3c507a2016-09-13 14:26:17 -07001419 final ContentProviderResult[] result =
1420 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1421 for (int i = 0; i < result.length; ++i) {
1422 // if no rows were modified in the operation then we count it as fail.
1423 if (result[i].count < 0) {
1424 throw new OperationApplicationException();
1425 }
1426 }
Walter Jang0653de32015-07-24 12:12:40 -07001427 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001428 } catch (RemoteException | OperationApplicationException e) {
1429 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1430 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001431 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001432 }
1433 }
1434
Brian Attwelld3946ca2015-03-03 11:13:49 -08001435 private void joinContacts(Intent intent) {
1436 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1437 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001438
1439 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001440 // in the join UIs.
1441 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1442 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001443 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001444 return;
1445 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001446
Brian Attwell548f5c62015-01-27 17:46:46 -08001447 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001448
1449 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001450 for (int i = 0; i < rawContactIds.length; i++) {
1451 for (int j = 0; j < rawContactIds.length; j++) {
1452 if (i != j) {
1453 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1454 }
1455 }
1456 }
1457
Brian Attwelld3946ca2015-03-03 11:13:49 -08001458 final ContentResolver resolver = getContentResolver();
1459
Brian Attwell548f5c62015-01-27 17:46:46 -08001460 // Use the name for contactId1 as the name for the newly aggregated contact.
1461 final Uri contactId1Uri = ContentUris.withAppendedId(
1462 Contacts.CONTENT_URI, contactId1);
1463 final Uri entityUri = Uri.withAppendedPath(
1464 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1465 Cursor c = resolver.query(entityUri,
1466 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1467 if (c == null) {
1468 Log.e(TAG, "Unable to open Contacts DB cursor");
1469 showToast(R.string.contactSavedErrorToast);
1470 return;
1471 }
1472 long dataIdToAddSuperPrimary = -1;
1473 try {
1474 if (c.moveToFirst()) {
1475 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1476 }
1477 } finally {
1478 c.close();
1479 }
1480
1481 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1482 // display name does not change as a result of the join.
1483 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001484 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001485 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1486 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1487 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001488 operations.add(builder.build());
1489 }
1490
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001491 // Apply all aggregation exceptions as one batch
John Shaoa3c507a2016-09-13 14:26:17 -07001492 final boolean success = applyOperations(resolver, operations);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001493
John Shaoa3c507a2016-09-13 14:26:17 -07001494 final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2});
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001495 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
John Shaoa3c507a2016-09-13 14:26:17 -07001496 if (success && name != null) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001497 Uri uri = RawContacts.getContactLookupUri(resolver,
1498 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1499 callbackIntent.setData(uri);
1500 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001501 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001502 }
1503
Gary Mai7efa9942016-05-12 11:26:49 -07001504 /**
1505 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1506 * array of the return value holds an array of raw contact ids for one contactId.
1507 * @param contactIds
1508 * @return
1509 */
1510 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1511 final long[][] rawContactIds = new long[contactIds.length][];
1512 for (int i = 0; i < contactIds.length; i++) {
1513 rawContactIds[i] = getRawContactIds(contactIds[i]);
1514 }
1515 return rawContactIds;
1516 }
1517
1518 /**
1519 * Gets the raw contact ids associated with {@param contactId}.
1520 * @param contactId
1521 * @return Array of raw contact ids.
1522 */
1523 private long[] getRawContactIds(long contactId) {
1524 final ContentResolver resolver = getContentResolver();
1525 long rawContactIds[];
1526
1527 final StringBuilder queryBuilder = new StringBuilder();
1528 queryBuilder.append(RawContacts.CONTACT_ID)
1529 .append("=")
1530 .append(String.valueOf(contactId));
1531
1532 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1533 JoinContactQuery.PROJECTION,
1534 queryBuilder.toString(),
1535 null, null);
1536 if (c == null) {
1537 Log.e(TAG, "Unable to open Contacts DB cursor");
1538 return null;
1539 }
1540 try {
1541 rawContactIds = new long[c.getCount()];
1542 for (int i = 0; i < rawContactIds.length; i++) {
1543 c.moveToPosition(i);
1544 final long rawContactId = c.getLong(JoinContactQuery._ID);
1545 rawContactIds[i] = rawContactId;
1546 }
1547 } finally {
1548 c.close();
1549 }
1550 return rawContactIds;
1551 }
1552
Brian Attwelld3946ca2015-03-03 11:13:49 -08001553 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1554 if (contactIds == null) {
1555 return null;
1556 }
1557
Brian Attwell548f5c62015-01-27 17:46:46 -08001558 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001559
1560 final StringBuilder queryBuilder = new StringBuilder();
1561 final String stringContactIds[] = new String[contactIds.length];
1562 for (int i = 0; i < contactIds.length; i++) {
1563 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1564 stringContactIds[i] = String.valueOf(contactIds[i]);
1565 if (contactIds[i] == -1) {
1566 return null;
1567 }
1568 if (i == contactIds.length -1) {
1569 break;
1570 }
1571 queryBuilder.append(" OR ");
1572 }
1573
Brian Attwell548f5c62015-01-27 17:46:46 -08001574 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1575 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001576 queryBuilder.toString(),
1577 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001578 if (c == null) {
1579 Log.e(TAG, "Unable to open Contacts DB cursor");
1580 showToast(R.string.contactSavedErrorToast);
1581 return null;
1582 }
Gary Mai7efa9942016-05-12 11:26:49 -07001583 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001584 try {
1585 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001586 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001587 return null;
1588 }
1589 rawContactIds = new long[c.getCount()];
1590 for (int i = 0; i < rawContactIds.length; i++) {
1591 c.moveToPosition(i);
1592 long rawContactId = c.getLong(JoinContactQuery._ID);
1593 rawContactIds[i] = rawContactId;
1594 }
1595 } finally {
1596 c.close();
1597 }
1598 return rawContactIds;
1599 }
1600
Brian Attwelld3946ca2015-03-03 11:13:49 -08001601 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1602 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1603 }
1604
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001605 /**
1606 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1607 */
1608 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1609 long rawContactId1, long rawContactId2) {
1610 Builder builder =
1611 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1612 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1613 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1614 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1615 operations.add(builder.build());
1616 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001617
1618 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001619 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} ContentProviderOperation.
Gary Mai7efa9942016-05-12 11:26:49 -07001620 */
1621 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
1622 long rawContactId1, long rawContactId2) {
1623 final Builder builder =
1624 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Mai53fe0d22016-07-26 17:23:53 -07001625 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001626 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1627 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1628 operations.add(builder.build());
1629 }
1630
1631 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001632 * Shows a toast on the UI thread.
1633 */
1634 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001635 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001636
1637 @Override
1638 public void run() {
1639 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1640 }
1641 });
1642 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001643
1644 private void deliverCallback(final Intent callbackIntent) {
1645 mMainHandler.post(new Runnable() {
1646
1647 @Override
1648 public void run() {
1649 deliverCallbackOnUiThread(callbackIntent);
1650 }
1651 });
1652 }
1653
1654 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1655 // TODO: this assumes that if there are multiple instances of the same
1656 // activity registered, the last one registered is the one waiting for
1657 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001658 for (Listener listener : sListeners) {
1659 if (callbackIntent.getComponent().equals(
1660 ((Activity) listener).getIntent().getComponent())) {
1661 listener.onServiceCompleted(callbackIntent);
1662 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001663 }
1664 }
1665 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001666
1667 public interface GroupsDao {
1668 Uri create(String title, AccountWithDataSet account);
1669 int delete(Uri groupUri);
1670 Bundle captureDeletionUndoData(Uri groupUri);
1671 Uri undoDeletion(Bundle undoData);
1672 }
1673
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001674 public static class GroupsDaoImpl implements GroupsDao {
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001675 public static final String KEY_GROUP_DATA = "groupData";
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001676 public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
1677
1678 private static final String TAG = "GroupsDao";
1679 private final Context context;
1680 private final ContentResolver contentResolver;
1681
1682 public GroupsDaoImpl(Context context) {
1683 this(context, context.getContentResolver());
1684 }
1685
1686 public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
1687 this.context = context;
1688 this.contentResolver = contentResolver;
1689 }
1690
1691 public Bundle captureDeletionUndoData(Uri groupUri) {
1692 final long groupId = ContentUris.parseId(groupUri);
1693 final Bundle result = new Bundle();
1694
1695 final Cursor cursor = contentResolver.query(groupUri,
1696 new String[]{
1697 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
1698 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
1699 Groups.SHOULD_SYNC
1700 },
1701 Groups.DELETED + "=?", new String[] { "0" }, null);
1702 try {
1703 if (cursor.moveToFirst()) {
1704 final ContentValues groupValues = new ContentValues();
1705 DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
1706 result.putParcelable(KEY_GROUP_DATA, groupValues);
1707 } else {
1708 // Group doesn't exist.
1709 return result;
1710 }
1711 } finally {
1712 cursor.close();
1713 }
1714
1715 final Cursor membersCursor = contentResolver.query(
1716 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
1717 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1718 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
1719 final long[] memberIds = new long[membersCursor.getCount()];
1720 int i = 0;
1721 while (membersCursor.moveToNext()) {
1722 memberIds[i++] = membersCursor.getLong(0);
1723 }
1724 result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
1725 return result;
1726 }
1727
1728 public Uri undoDeletion(Bundle deletedGroupData) {
1729 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
1730 if (groupData == null) {
1731 return null;
1732 }
1733 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
1734 final long groupId = ContentUris.parseId(groupUri);
1735
1736 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
1737 if (memberIds == null) {
1738 return groupUri;
1739 }
1740 final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
1741 for (int i = 0; i < memberIds.length; i++) {
1742 memberInsertions[i] = new ContentValues();
1743 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
1744 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1745 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
1746 }
1747 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
1748 if (inserted != memberIds.length) {
1749 Log.e(TAG, "Could not recover some members for group deletion undo");
1750 }
1751
1752 return groupUri;
1753 }
1754
1755 public Uri create(String title, AccountWithDataSet account) {
1756 final ContentValues values = new ContentValues();
1757 values.put(Groups.TITLE, title);
1758 values.put(Groups.ACCOUNT_NAME, account.name);
1759 values.put(Groups.ACCOUNT_TYPE, account.type);
1760 values.put(Groups.DATA_SET, account.dataSet);
1761 return contentResolver.insert(Groups.CONTENT_URI, values);
1762 }
1763
1764 public int delete(Uri groupUri) {
1765 return contentResolver.delete(groupUri, null, null);
1766 }
1767 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001768}