blob: 9c38712b3576318a03a8d66977a93b2dd8f8e4a8 [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
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080019import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070020import android.app.IntentService;
21import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080022import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070023import android.content.ContentProviderResult;
24import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080025import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070026import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080027import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070028import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080029import android.content.OperationApplicationException;
30import android.database.Cursor;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070031import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080032import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080033import android.os.Handler;
34import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080035import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080036import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070037import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080038import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080039import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080040import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080041import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070042import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080043import android.provider.ContactsContract.Groups;
Yorke Leee8e3fb82013-09-12 17:53:31 -070044import android.provider.ContactsContract.PinnedPositions;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070045import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070046import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070047import android.provider.ContactsContract.RawContactsEntity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070048import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080049import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070050
Chiao Chengd7ca03e2012-10-24 15:14:08 -070051import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080052import com.android.contacts.common.model.AccountTypeManager;
Yorke Leecd321f62013-10-28 15:20:15 -070053import com.android.contacts.common.model.RawContactDelta;
54import com.android.contacts.common.model.RawContactDeltaList;
55import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080056import com.android.contacts.common.model.account.AccountWithDataSet;
Yorke Lee637a38e2013-09-14 08:36:33 -070057import com.android.contacts.util.ContactPhotoUtils;
58
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070059import com.google.common.collect.Lists;
60import com.google.common.collect.Sets;
61
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080062import java.util.ArrayList;
63import java.util.HashSet;
64import java.util.List;
65import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070066
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080067/**
68 * A service responsible for saving changes to the content provider.
69 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070070public class ContactSaveService extends IntentService {
71 private static final String TAG = "ContactSaveService";
72
Katherine Kuana007e442011-07-07 09:25:34 -070073 /** Set to true in order to view logs on content provider operations */
74 private static final boolean DEBUG = false;
75
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070076 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
77
78 public static final String EXTRA_ACCOUNT_NAME = "accountName";
79 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070080 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070081 public static final String EXTRA_CONTENT_VALUES = "contentValues";
82 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
83
Dmitri Plotnikova0114142011-02-15 13:53:21 -080084 public static final String ACTION_SAVE_CONTACT = "saveContact";
85 public static final String EXTRA_CONTACT_STATE = "state";
86 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070087 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070088 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080089 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070090
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080091 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080092 public static final String ACTION_RENAME_GROUP = "renameGroup";
93 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070094 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080095 public static final String EXTRA_GROUP_ID = "groupId";
96 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070097 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
98 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080099
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800100 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800101 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800102 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800103 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800104 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800105 public static final String EXTRA_STARRED_FLAG = "starred";
106
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800107 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
108 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
109 public static final String EXTRA_DATA_ID = "dataId";
110
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800111 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800112 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800113 public static final String EXTRA_CONTACT_ID1 = "contactId1";
114 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800115
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700116 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
117 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
118
119 public static final String ACTION_SET_RINGTONE = "setRingtone";
120 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
121
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700122 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
123 Data.MIMETYPE,
124 Data.IS_PRIMARY,
125 Data.DATA1,
126 Data.DATA2,
127 Data.DATA3,
128 Data.DATA4,
129 Data.DATA5,
130 Data.DATA6,
131 Data.DATA7,
132 Data.DATA8,
133 Data.DATA9,
134 Data.DATA10,
135 Data.DATA11,
136 Data.DATA12,
137 Data.DATA13,
138 Data.DATA14,
139 Data.DATA15
140 );
141
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800142 private static final int PERSIST_TRIES = 3;
143
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800144 public interface Listener {
145 public void onServiceCompleted(Intent callbackIntent);
146 }
147
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100148 private static final CopyOnWriteArrayList<Listener> sListeners =
149 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800150
151 private Handler mMainHandler;
152
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700153 public ContactSaveService() {
154 super(TAG);
155 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800156 mMainHandler = new Handler(Looper.getMainLooper());
157 }
158
159 public static void registerListener(Listener listener) {
160 if (!(listener instanceof Activity)) {
161 throw new ClassCastException("Only activities can be registered to"
162 + " receive callback from " + ContactSaveService.class.getName());
163 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100164 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800165 }
166
167 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100168 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700169 }
170
171 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800172 public Object getSystemService(String name) {
173 Object service = super.getSystemService(name);
174 if (service != null) {
175 return service;
176 }
177
178 return getApplicationContext().getSystemService(name);
179 }
180
181 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700182 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800183 if (intent == null) {
184 Log.d(TAG, "onHandleIntent: could not handle null intent");
185 return;
186 }
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700187 // Call an appropriate method. If we're sure it affects how incoming phone calls are
188 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700189 String action = intent.getAction();
190 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
191 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800192 } else if (ACTION_SAVE_CONTACT.equals(action)) {
193 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800194 } else if (ACTION_CREATE_GROUP.equals(action)) {
195 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800196 } else if (ACTION_RENAME_GROUP.equals(action)) {
197 renameGroup(intent);
198 } else if (ACTION_DELETE_GROUP.equals(action)) {
199 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700200 } else if (ACTION_UPDATE_GROUP.equals(action)) {
201 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800202 } else if (ACTION_SET_STARRED.equals(action)) {
203 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800204 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
205 setSuperPrimary(intent);
206 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
207 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800208 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
209 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800210 } else if (ACTION_DELETE_CONTACT.equals(action)) {
211 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800212 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
213 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800214 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
215 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700216 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
217 setSendToVoicemail(intent);
218 } else if (ACTION_SET_RINGTONE.equals(action)) {
219 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700220 }
221 }
222
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800223 /**
224 * Creates an intent that can be sent to this service to create a new raw contact
225 * using data presented as a set of ContentValues.
226 */
227 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700228 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700229 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800230 Intent serviceIntent = new Intent(
231 context, ContactSaveService.class);
232 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
233 if (account != null) {
234 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
235 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700236 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800237 }
238 serviceIntent.putParcelableArrayListExtra(
239 ContactSaveService.EXTRA_CONTENT_VALUES, values);
240
241 // Callback intent will be invoked by the service once the new contact is
242 // created. The service will put the URI of the new contact as "data" on
243 // the callback intent.
244 Intent callbackIntent = new Intent(context, callbackActivity);
245 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800246 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
247 return serviceIntent;
248 }
249
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700250 private void createRawContact(Intent intent) {
251 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
252 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700253 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700254 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
255 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
256
257 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
258 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
259 .withValue(RawContacts.ACCOUNT_NAME, accountName)
260 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700261 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700262 .build());
263
264 int size = valueList.size();
265 for (int i = 0; i < size; i++) {
266 ContentValues values = valueList.get(i);
267 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
268 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
269 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
270 .withValues(values)
271 .build());
272 }
273
274 ContentResolver resolver = getContentResolver();
275 ContentProviderResult[] results;
276 try {
277 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
278 } catch (Exception e) {
279 throw new RuntimeException("Failed to store new contact", e);
280 }
281
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700282 Uri rawContactUri = results[0].uri;
283 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
284
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800285 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700286 }
287
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700288 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800289 * Creates an intent that can be sent to this service to create a new raw contact
290 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800291 * This variant is more convenient to use when there is only one photo that can
292 * possibly be updated, as in the Contact Details screen.
293 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
294 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800295 */
Maurice Chu851222a2012-06-21 11:43:08 -0700296 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700297 String saveModeExtraKey, int saveMode, boolean isProfile,
298 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700299 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800300 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700301 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800302 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
303 callbackActivity, callbackAction, bundle);
304 }
305
306 /**
307 * Creates an intent that can be sent to this service to create a new raw contact
308 * using data presented as a set of ContentValues.
309 * This variant is used when multiple contacts' photos may be updated, as in the
310 * Contact Editor.
311 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
312 */
Maurice Chu851222a2012-06-21 11:43:08 -0700313 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700314 String saveModeExtraKey, int saveMode, boolean isProfile,
315 Class<? extends Activity> callbackActivity, String callbackAction,
316 Bundle updatedPhotos) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800317 Intent serviceIntent = new Intent(
318 context, ContactSaveService.class);
319 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
320 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700321 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800322 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
323
Josh Garguse692e012012-01-18 14:53:11 -0800324 if (updatedPhotos != null) {
325 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
326 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800327
Josh Garguse5d3f892012-04-11 11:56:15 -0700328 if (callbackActivity != null) {
329 // Callback intent will be invoked by the service once the contact is
330 // saved. 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.putExtra(saveModeExtraKey, saveMode);
334 callbackIntent.setAction(callbackAction);
Walter Jang1e8801b2015-03-10 15:57:05 -0700335 if (updatedPhotos != null) {
336 callbackIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
337 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700338 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
339 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800340 return serviceIntent;
341 }
342
343 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700344 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700345 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800346 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800347
Jay Shrauner08099782015-03-25 14:17:11 -0700348 if (state == null) {
349 Log.e(TAG, "Invalid arguments for saveContact request");
350 return;
351 }
352
benny.lin3a4e7a22014-01-08 10:58:08 +0800353 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800354 // Trim any empty fields, and RawContacts, before persisting
355 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700356 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800357
358 Uri lookupUri = null;
359
360 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800361 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800362
Josh Gargusef15c8e2012-01-30 16:42:02 -0800363 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
364 long insertedRawContactId = -1;
365
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800366 // Attempt to persist changes
367 int tries = 0;
368 while (tries++ < PERSIST_TRIES) {
369 try {
370 // Build operations and try applying
371 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700372 if (DEBUG) {
373 Log.v(TAG, "Content Provider Operations:");
374 for (ContentProviderOperation operation : diff) {
375 Log.v(TAG, operation.toString());
376 }
377 }
378
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800379 ContentProviderResult[] results = null;
380 if (!diff.isEmpty()) {
381 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
Jay Shrauner511561d2015-04-02 10:35:33 -0700382 if (results == null) {
383 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
384 // Retry save
385 continue;
386 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800387 }
388
389 final long rawContactId = getRawContactId(state, diff, results);
390 if (rawContactId == -1) {
391 throw new IllegalStateException("Could not determine RawContact ID after save");
392 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800393 // We don't have to check to see if the value is still -1. If we reach here,
394 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
395 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700396 if (isProfile) {
397 // Since the profile supports local raw contacts, which may have been completely
398 // removed if all information was removed, we need to do a special query to
399 // get the lookup URI for the profile contact (if it still exists).
400 Cursor c = resolver.query(Profile.CONTENT_URI,
401 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
402 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800403 if (c == null) {
404 continue;
405 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700406 try {
Erik162b7e32011-09-20 15:23:55 -0700407 if (c.moveToFirst()) {
408 final long contactId = c.getLong(0);
409 final String lookupKey = c.getString(1);
410 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
411 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700412 } finally {
413 c.close();
414 }
415 } else {
416 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
417 rawContactId);
418 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
419 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800420 if (lookupUri != null) {
421 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
422 }
Josh Garguse692e012012-01-18 14:53:11 -0800423
424 // We can change this back to false later, if we fail to save the contact photo.
425 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800426 break;
427
428 } catch (RemoteException e) {
429 // Something went wrong, bail without success
430 Log.e(TAG, "Problem persisting user edits", e);
431 break;
432
Jay Shrauner57fca182014-01-17 14:20:50 -0800433 } catch (IllegalArgumentException e) {
434 // This is thrown by applyBatch on malformed requests
435 Log.e(TAG, "Problem persisting user edits", e);
436 showToast(R.string.contactSavedErrorToast);
437 break;
438
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800439 } catch (OperationApplicationException e) {
440 // Version consistency failed, re-parent change and try again
441 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
442 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
443 boolean first = true;
444 final int count = state.size();
445 for (int i = 0; i < count; i++) {
446 Long rawContactId = state.getRawContactId(i);
447 if (rawContactId != null && rawContactId != -1) {
448 if (!first) {
449 sb.append(',');
450 }
451 sb.append(rawContactId);
452 first = false;
453 }
454 }
455 sb.append(")");
456
457 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800458 throw new IllegalStateException(
459 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800460 }
461
Maurice Chu851222a2012-06-21 11:43:08 -0700462 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700463 isProfile
464 ? RawContactsEntity.PROFILE_CONTENT_URI
465 : RawContactsEntity.CONTENT_URI,
466 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700467 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700468
469 // Update the new state to use profile URIs if appropriate.
470 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700471 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700472 delta.setProfileQueryUri();
473 }
474 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800475 }
476 }
477
Josh Garguse692e012012-01-18 14:53:11 -0800478 // Now save any updated photos. We do this at the end to ensure that
479 // the ContactProvider already knows about newly-created contacts.
480 if (updatedPhotos != null) {
481 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700482 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800483 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800484
485 // If the raw-contact ID is negative, we are saving a new raw-contact;
486 // replace the bogus ID with the new one that we actually saved the contact at.
487 if (rawContactId < 0) {
488 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800489 }
490
Jay Shrauner511561d2015-04-02 10:35:33 -0700491 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700492 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700493 succeeded = false;
494 }
Josh Garguse692e012012-01-18 14:53:11 -0800495 }
496 }
497
Josh Garguse5d3f892012-04-11 11:56:15 -0700498 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
499 if (callbackIntent != null) {
500 if (succeeded) {
501 // Mark the intent to indicate that the save was successful (even if the lookup URI
502 // is now null). For local contacts or the local profile, it's possible that the
503 // save triggered removal of the contact, so no lookup URI would exist..
504 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
505 }
506 callbackIntent.setData(lookupUri);
507 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800508 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800509 }
510
Josh Garguse692e012012-01-18 14:53:11 -0800511 /**
512 * Save updated photo for the specified raw-contact.
513 * @return true for success, false for failure
514 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800515 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800516 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800517 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
518 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
519
benny.lin3a4e7a22014-01-08 10:58:08 +0800520 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800521 }
522
Josh Gargusef15c8e2012-01-30 16:42:02 -0800523 /**
524 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
525 */
Maurice Chu851222a2012-06-21 11:43:08 -0700526 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800527 final ArrayList<ContentProviderOperation> diff,
528 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800529 long existingRawContactId = state.findRawContactId();
530 if (existingRawContactId != -1) {
531 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800532 }
533
Josh Gargusef15c8e2012-01-30 16:42:02 -0800534 return getInsertedRawContactId(diff, results);
535 }
536
537 /**
538 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
539 */
540 private long getInsertedRawContactId(
541 final ArrayList<ContentProviderOperation> diff,
542 final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800543 if (results == null) {
544 return -1;
545 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800546 final int diffSize = diff.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800547 final int numResults = results.length;
548 for (int i = 0; i < diffSize && i < numResults; i++) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800549 ContentProviderOperation operation = diff.get(i);
Brian Attwell13f94e12015-01-22 16:27:48 -0800550 if (operation.isInsert() && operation.getUri().getEncodedPath().contains(
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800551 RawContacts.CONTENT_URI.getEncodedPath())) {
552 return ContentUris.parseId(results[i].uri);
553 }
554 }
555 return -1;
556 }
557
558 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700559 * Creates an intent that can be sent to this service to create a new group as
560 * well as add new members at the same time.
561 *
562 * @param context of the application
563 * @param account in which the group should be created
564 * @param label is the name of the group (cannot be null)
565 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
566 * should be added to the group
567 * @param callbackActivity is the activity to send the callback intent to
568 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700569 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700570 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700571 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700572 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800573 Intent serviceIntent = new Intent(context, ContactSaveService.class);
574 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
575 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
576 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700577 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800578 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700579 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700580
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800581 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700582 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800583 Intent callbackIntent = new Intent(context, callbackActivity);
584 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700585 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800586
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700587 return serviceIntent;
588 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800589
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800590 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700591 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
592 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
593 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
594 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700595 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800596
597 ContentValues values = new ContentValues();
598 values.put(Groups.ACCOUNT_TYPE, accountType);
599 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700600 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800601 values.put(Groups.TITLE, label);
602
Katherine Kuan717e3432011-07-13 17:03:24 -0700603 final ContentResolver resolver = getContentResolver();
604
605 // Create the new group
606 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
607
608 // If there's no URI, then the insertion failed. Abort early because group members can't be
609 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800610 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700611 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800612 return;
613 }
614
Katherine Kuan717e3432011-07-13 17:03:24 -0700615 // Add new group members
616 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
617
618 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
619 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800620 values.clear();
621 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
622 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
623
624 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700625 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700626 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800627 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800628 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800629 }
630
631 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800632 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800633 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700634 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700635 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800636 Intent serviceIntent = new Intent(context, ContactSaveService.class);
637 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
638 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
639 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700640
641 // Callback intent will be invoked by the service once the group is renamed.
642 Intent callbackIntent = new Intent(context, callbackActivity);
643 callbackIntent.setAction(callbackAction);
644 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
645
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800646 return serviceIntent;
647 }
648
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800649 private void renameGroup(Intent intent) {
650 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
651 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
652
653 if (groupId == -1) {
654 Log.e(TAG, "Invalid arguments for renameGroup request");
655 return;
656 }
657
658 ContentValues values = new ContentValues();
659 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700660 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
661 getContentResolver().update(groupUri, values, null, null);
662
663 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
664 callbackIntent.setData(groupUri);
665 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800666 }
667
668 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800669 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800670 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800671 public static Intent createGroupDeletionIntent(Context context, long groupId) {
672 Intent serviceIntent = new Intent(context, ContactSaveService.class);
673 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800674 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800675 return serviceIntent;
676 }
677
678 private void deleteGroup(Intent intent) {
679 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
680 if (groupId == -1) {
681 Log.e(TAG, "Invalid arguments for deleteGroup request");
682 return;
683 }
684
685 getContentResolver().delete(
686 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
687 }
688
689 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700690 * Creates an intent that can be sent to this service to rename a group as
691 * well as add and remove members from the group.
692 *
693 * @param context of the application
694 * @param groupId of the group that should be modified
695 * @param newLabel is the updated name of the group (can be null if the name
696 * should not be updated)
697 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
698 * should be added to the group
699 * @param rawContactsToRemove is an array of raw contact IDs for contacts
700 * that should be removed from the group
701 * @param callbackActivity is the activity to send the callback intent to
702 * @param callbackAction is the intent action for the callback intent
703 */
704 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
705 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700706 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700707 Intent serviceIntent = new Intent(context, ContactSaveService.class);
708 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
709 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
710 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
711 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
712 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
713 rawContactsToRemove);
714
715 // Callback intent will be invoked by the service once the group is updated
716 Intent callbackIntent = new Intent(context, callbackActivity);
717 callbackIntent.setAction(callbackAction);
718 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
719
720 return serviceIntent;
721 }
722
723 private void updateGroup(Intent intent) {
724 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
725 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
726 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
727 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
728
729 if (groupId == -1) {
730 Log.e(TAG, "Invalid arguments for updateGroup request");
731 return;
732 }
733
734 final ContentResolver resolver = getContentResolver();
735 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
736
737 // Update group name if necessary
738 if (label != null) {
739 ContentValues values = new ContentValues();
740 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700741 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700742 }
743
Katherine Kuan717e3432011-07-13 17:03:24 -0700744 // Add and remove members if necessary
745 addMembersToGroup(resolver, rawContactsToAdd, groupId);
746 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
747
748 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
749 callbackIntent.setData(groupUri);
750 deliverCallback(callbackIntent);
751 }
752
Daniel Lehmann18958a22012-02-28 17:45:25 -0800753 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700754 long groupId) {
755 if (rawContactsToAdd == null) {
756 return;
757 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700758 for (long rawContactId : rawContactsToAdd) {
759 try {
760 final ArrayList<ContentProviderOperation> rawContactOperations =
761 new ArrayList<ContentProviderOperation>();
762
763 // Build an assert operation to ensure the contact is not already in the group
764 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
765 .newAssertQuery(Data.CONTENT_URI);
766 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
767 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
768 new String[] { String.valueOf(rawContactId),
769 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
770 assertBuilder.withExpectedCount(0);
771 rawContactOperations.add(assertBuilder.build());
772
773 // Build an insert operation to add the contact to the group
774 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
775 .newInsert(Data.CONTENT_URI);
776 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
777 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
778 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
779 rawContactOperations.add(insertBuilder.build());
780
781 if (DEBUG) {
782 for (ContentProviderOperation operation : rawContactOperations) {
783 Log.v(TAG, operation.toString());
784 }
785 }
786
787 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700788 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800789 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700790 }
791 } catch (RemoteException e) {
792 // Something went wrong, bail without success
793 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
794 String.valueOf(rawContactId), e);
795 } catch (OperationApplicationException e) {
796 // The assert could have failed because the contact is already in the group,
797 // just continue to the next contact
798 Log.w(TAG, "Assert failed in adding raw contact ID " +
799 String.valueOf(rawContactId) + ". Already exists in group " +
800 String.valueOf(groupId), e);
801 }
802 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700803 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700804
Daniel Lehmann18958a22012-02-28 17:45:25 -0800805 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700806 long groupId) {
807 if (rawContactsToRemove == null) {
808 return;
809 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700810 for (long rawContactId : rawContactsToRemove) {
811 // Apply the delete operation on the data row for the given raw contact's
812 // membership in the given group. If no contact matches the provided selection, then
813 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800814 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700815 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
816 new String[] { String.valueOf(rawContactId),
817 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
818 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700819 }
820
821 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800822 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800823 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800824 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
825 Intent serviceIntent = new Intent(context, ContactSaveService.class);
826 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
827 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
828 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
829
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800830 return serviceIntent;
831 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800832
833 private void setStarred(Intent intent) {
834 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
835 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
836 if (contactUri == null) {
837 Log.e(TAG, "Invalid arguments for setStarred request");
838 return;
839 }
840
841 final ContentValues values = new ContentValues(1);
842 values.put(Contacts.STARRED, value);
843 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700844
845 // Undemote the contact if necessary
846 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
847 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800848 if (c == null) {
849 return;
850 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700851 try {
852 if (c.moveToFirst()) {
853 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700854
855 // Don't bother undemoting if this contact is the user's profile.
856 if (id < Profile.MIN_ID) {
Brian Attwell2d88efa2014-12-17 21:49:56 -0800857 PinnedPositions.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700858 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700859 }
860 } finally {
861 c.close();
862 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800863 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800864
865 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700866 * Creates an intent that can be sent to this service to set the redirect to voicemail.
867 */
868 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
869 boolean value) {
870 Intent serviceIntent = new Intent(context, ContactSaveService.class);
871 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
872 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
873 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
874
875 return serviceIntent;
876 }
877
878 private void setSendToVoicemail(Intent intent) {
879 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
880 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
881 if (contactUri == null) {
882 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
883 return;
884 }
885
886 final ContentValues values = new ContentValues(1);
887 values.put(Contacts.SEND_TO_VOICEMAIL, value);
888 getContentResolver().update(contactUri, values, null, null);
889 }
890
891 /**
892 * Creates an intent that can be sent to this service to save the contact's ringtone.
893 */
894 public static Intent createSetRingtone(Context context, Uri contactUri,
895 String value) {
896 Intent serviceIntent = new Intent(context, ContactSaveService.class);
897 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
898 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
899 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
900
901 return serviceIntent;
902 }
903
904 private void setRingtone(Intent intent) {
905 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
906 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
907 if (contactUri == null) {
908 Log.e(TAG, "Invalid arguments for setRingtone");
909 return;
910 }
911 ContentValues values = new ContentValues(1);
912 values.put(Contacts.CUSTOM_RINGTONE, value);
913 getContentResolver().update(contactUri, values, null, null);
914 }
915
916 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800917 * Creates an intent that sets the selected data item as super primary (default)
918 */
919 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
920 Intent serviceIntent = new Intent(context, ContactSaveService.class);
921 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
922 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
923 return serviceIntent;
924 }
925
926 private void setSuperPrimary(Intent intent) {
927 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
928 if (dataId == -1) {
929 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
930 return;
931 }
932
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700933 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800934 }
935
936 /**
937 * Creates an intent that clears the primary flag of all data items that belong to the same
938 * raw_contact as the given data item. Will only clear, if the data item was primary before
939 * this call
940 */
941 public static Intent createClearPrimaryIntent(Context context, long dataId) {
942 Intent serviceIntent = new Intent(context, ContactSaveService.class);
943 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
944 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
945 return serviceIntent;
946 }
947
948 private void clearPrimary(Intent intent) {
949 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
950 if (dataId == -1) {
951 Log.e(TAG, "Invalid arguments for clearPrimary request");
952 return;
953 }
954
955 // Update the primary values in the data record.
956 ContentValues values = new ContentValues(1);
957 values.put(Data.IS_SUPER_PRIMARY, 0);
958 values.put(Data.IS_PRIMARY, 0);
959
960 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
961 values, null, null);
962 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800963
964 /**
965 * Creates an intent that can be sent to this service to delete a contact.
966 */
967 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
968 Intent serviceIntent = new Intent(context, ContactSaveService.class);
969 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
970 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
971 return serviceIntent;
972 }
973
Brian Attwelld2962a32015-03-02 14:48:50 -0800974 /**
975 * Creates an intent that can be sent to this service to delete multiple contacts.
976 */
977 public static Intent createDeleteMultipleContactsIntent(Context context,
978 long[] contactIds) {
979 Intent serviceIntent = new Intent(context, ContactSaveService.class);
980 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
981 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
982 return serviceIntent;
983 }
984
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800985 private void deleteContact(Intent intent) {
986 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
987 if (contactUri == null) {
988 Log.e(TAG, "Invalid arguments for deleteContact request");
989 return;
990 }
991
992 getContentResolver().delete(contactUri, null, null);
993 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800994
Brian Attwelld2962a32015-03-02 14:48:50 -0800995 private void deleteMultipleContacts(Intent intent) {
996 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
997 if (contactIds == null) {
998 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
999 return;
1000 }
1001 for (long contactId : contactIds) {
1002 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1003 getContentResolver().delete(contactUri, null, null);
1004 }
Brian Attwelle986c6b2015-03-05 19:47:30 -08001005 showToast(R.string.contacts_deleted_toast);
Brian Attwelld2962a32015-03-02 14:48:50 -08001006 }
1007
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001008 /**
1009 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001010 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001011 */
1012 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001013 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001014 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1015 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1016 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1017 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001018
1019 // Callback intent will be invoked by the service once the contacts are joined.
1020 Intent callbackIntent = new Intent(context, callbackActivity);
1021 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001022 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1023
1024 return serviceIntent;
1025 }
1026
Brian Attwelld3946ca2015-03-03 11:13:49 -08001027 /**
1028 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1029 * No special attention is paid to where the resulting contact's name is taken from.
1030 */
1031 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1032 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1033 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1034 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1035 return serviceIntent;
1036 }
1037
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001038
1039 private interface JoinContactQuery {
1040 String[] PROJECTION = {
1041 RawContacts._ID,
1042 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001043 RawContacts.DISPLAY_NAME_SOURCE,
1044 };
1045
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001046 int _ID = 0;
1047 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001048 int DISPLAY_NAME_SOURCE = 2;
1049 }
1050
1051 private interface ContactEntityQuery {
1052 String[] PROJECTION = {
1053 Contacts.Entity.DATA_ID,
1054 Contacts.Entity.CONTACT_ID,
1055 Contacts.Entity.IS_SUPER_PRIMARY,
1056 };
1057 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1058 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1059 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1060 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1061
1062 int DATA_ID = 0;
1063 int CONTACT_ID = 1;
1064 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001065 }
1066
Brian Attwelld3946ca2015-03-03 11:13:49 -08001067 private void joinSeveralContacts(Intent intent) {
1068 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001069
Brian Attwelld3946ca2015-03-03 11:13:49 -08001070 // Load raw contact IDs for all contacts involved.
1071 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1072 if (rawContactIds == null) {
1073 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001074 return;
1075 }
1076
Brian Attwelld3946ca2015-03-03 11:13:49 -08001077 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001078 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001079 final ArrayList<ContentProviderOperation> operations
1080 = new ArrayList<ContentProviderOperation>();
1081 for (int i = 0; i < rawContactIds.length; i++) {
1082 for (int j = 0; j < rawContactIds.length; j++) {
1083 if (i != j) {
1084 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1085 }
1086 }
1087 }
1088
1089 // Apply all aggregation exceptions as one batch
1090 try {
1091 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1092 showToast(R.string.contactsJoinedMessage);
1093 } catch (RemoteException | OperationApplicationException e) {
1094 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1095 showToast(R.string.contactSavedErrorToast);
1096 }
1097 }
1098
1099
1100 private void joinContacts(Intent intent) {
1101 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1102 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001103
1104 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001105 // in the join UIs.
1106 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1107 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001108 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001109 return;
1110 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001111
Brian Attwell548f5c62015-01-27 17:46:46 -08001112 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001113
1114 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001115 for (int i = 0; i < rawContactIds.length; i++) {
1116 for (int j = 0; j < rawContactIds.length; j++) {
1117 if (i != j) {
1118 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1119 }
1120 }
1121 }
1122
Brian Attwelld3946ca2015-03-03 11:13:49 -08001123 final ContentResolver resolver = getContentResolver();
1124
Brian Attwell548f5c62015-01-27 17:46:46 -08001125 // Use the name for contactId1 as the name for the newly aggregated contact.
1126 final Uri contactId1Uri = ContentUris.withAppendedId(
1127 Contacts.CONTENT_URI, contactId1);
1128 final Uri entityUri = Uri.withAppendedPath(
1129 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1130 Cursor c = resolver.query(entityUri,
1131 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1132 if (c == null) {
1133 Log.e(TAG, "Unable to open Contacts DB cursor");
1134 showToast(R.string.contactSavedErrorToast);
1135 return;
1136 }
1137 long dataIdToAddSuperPrimary = -1;
1138 try {
1139 if (c.moveToFirst()) {
1140 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1141 }
1142 } finally {
1143 c.close();
1144 }
1145
1146 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1147 // display name does not change as a result of the join.
1148 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001149 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001150 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1151 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1152 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001153 operations.add(builder.build());
1154 }
1155
1156 boolean success = false;
1157 // Apply all aggregation exceptions as one batch
1158 try {
1159 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001160 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001161 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001162 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001163 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001164 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001165 }
1166
1167 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1168 if (success) {
1169 Uri uri = RawContacts.getContactLookupUri(resolver,
1170 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1171 callbackIntent.setData(uri);
1172 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001173 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001174 }
1175
Brian Attwelld3946ca2015-03-03 11:13:49 -08001176 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1177 if (contactIds == null) {
1178 return null;
1179 }
1180
Brian Attwell548f5c62015-01-27 17:46:46 -08001181 final ContentResolver resolver = getContentResolver();
1182 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001183
1184 final StringBuilder queryBuilder = new StringBuilder();
1185 final String stringContactIds[] = new String[contactIds.length];
1186 for (int i = 0; i < contactIds.length; i++) {
1187 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1188 stringContactIds[i] = String.valueOf(contactIds[i]);
1189 if (contactIds[i] == -1) {
1190 return null;
1191 }
1192 if (i == contactIds.length -1) {
1193 break;
1194 }
1195 queryBuilder.append(" OR ");
1196 }
1197
Brian Attwell548f5c62015-01-27 17:46:46 -08001198 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1199 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001200 queryBuilder.toString(),
1201 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001202 if (c == null) {
1203 Log.e(TAG, "Unable to open Contacts DB cursor");
1204 showToast(R.string.contactSavedErrorToast);
1205 return null;
1206 }
1207 try {
1208 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001209 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001210 return null;
1211 }
1212 rawContactIds = new long[c.getCount()];
1213 for (int i = 0; i < rawContactIds.length; i++) {
1214 c.moveToPosition(i);
1215 long rawContactId = c.getLong(JoinContactQuery._ID);
1216 rawContactIds[i] = rawContactId;
1217 }
1218 } finally {
1219 c.close();
1220 }
1221 return rawContactIds;
1222 }
1223
Brian Attwelld3946ca2015-03-03 11:13:49 -08001224 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1225 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1226 }
1227
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001228 /**
1229 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1230 */
1231 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1232 long rawContactId1, long rawContactId2) {
1233 Builder builder =
1234 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1235 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1236 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1237 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1238 operations.add(builder.build());
1239 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001240
1241 /**
1242 * Shows a toast on the UI thread.
1243 */
1244 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001245 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001246
1247 @Override
1248 public void run() {
1249 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1250 }
1251 });
1252 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001253
1254 private void deliverCallback(final Intent callbackIntent) {
1255 mMainHandler.post(new Runnable() {
1256
1257 @Override
1258 public void run() {
1259 deliverCallbackOnUiThread(callbackIntent);
1260 }
1261 });
1262 }
1263
1264 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1265 // TODO: this assumes that if there are multiple instances of the same
1266 // activity registered, the last one registered is the one waiting for
1267 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001268 for (Listener listener : sListeners) {
1269 if (callbackIntent.getComponent().equals(
1270 ((Activity) listener).getIntent().getComponent())) {
1271 listener.onServiceCompleted(callbackIntent);
1272 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001273 }
1274 }
1275 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001276}