blob: cf36edf9ae73405c6b5025632700e1693653f24f [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);
Josh Garguse692e012012-01-18 14:53:11 -0800322 if (updatedPhotos != null) {
323 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
324 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800325
Josh Garguse5d3f892012-04-11 11:56:15 -0700326 if (callbackActivity != null) {
327 // Callback intent will be invoked by the service once the contact is
328 // saved. The service will put the URI of the new contact as "data" on
329 // the callback intent.
330 Intent callbackIntent = new Intent(context, callbackActivity);
331 callbackIntent.putExtra(saveModeExtraKey, saveMode);
332 callbackIntent.setAction(callbackAction);
333 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
334 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800335 return serviceIntent;
336 }
337
338 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700339 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700340 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800341 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800342
343 // Trim any empty fields, and RawContacts, before persisting
344 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700345 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800346
347 Uri lookupUri = null;
348
349 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800350 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800351
Josh Gargusef15c8e2012-01-30 16:42:02 -0800352 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
353 long insertedRawContactId = -1;
354
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800355 // Attempt to persist changes
356 int tries = 0;
357 while (tries++ < PERSIST_TRIES) {
358 try {
359 // Build operations and try applying
360 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700361 if (DEBUG) {
362 Log.v(TAG, "Content Provider Operations:");
363 for (ContentProviderOperation operation : diff) {
364 Log.v(TAG, operation.toString());
365 }
366 }
367
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800368 ContentProviderResult[] results = null;
369 if (!diff.isEmpty()) {
370 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
371 }
372
373 final long rawContactId = getRawContactId(state, diff, results);
374 if (rawContactId == -1) {
375 throw new IllegalStateException("Could not determine RawContact ID after save");
376 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800377 // We don't have to check to see if the value is still -1. If we reach here,
378 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
379 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700380 if (isProfile) {
381 // Since the profile supports local raw contacts, which may have been completely
382 // removed if all information was removed, we need to do a special query to
383 // get the lookup URI for the profile contact (if it still exists).
384 Cursor c = resolver.query(Profile.CONTENT_URI,
385 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
386 null, null, null);
387 try {
Erik162b7e32011-09-20 15:23:55 -0700388 if (c.moveToFirst()) {
389 final long contactId = c.getLong(0);
390 final String lookupKey = c.getString(1);
391 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
392 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700393 } finally {
394 c.close();
395 }
396 } else {
397 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
398 rawContactId);
399 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
400 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800401 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
Josh Garguse692e012012-01-18 14:53:11 -0800402
403 // We can change this back to false later, if we fail to save the contact photo.
404 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800405 break;
406
407 } catch (RemoteException e) {
408 // Something went wrong, bail without success
409 Log.e(TAG, "Problem persisting user edits", e);
410 break;
411
Jay Shrauner57fca182014-01-17 14:20:50 -0800412 } catch (IllegalArgumentException e) {
413 // This is thrown by applyBatch on malformed requests
414 Log.e(TAG, "Problem persisting user edits", e);
415 showToast(R.string.contactSavedErrorToast);
416 break;
417
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800418 } catch (OperationApplicationException e) {
419 // Version consistency failed, re-parent change and try again
420 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
421 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
422 boolean first = true;
423 final int count = state.size();
424 for (int i = 0; i < count; i++) {
425 Long rawContactId = state.getRawContactId(i);
426 if (rawContactId != null && rawContactId != -1) {
427 if (!first) {
428 sb.append(',');
429 }
430 sb.append(rawContactId);
431 first = false;
432 }
433 }
434 sb.append(")");
435
436 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800437 throw new IllegalStateException(
438 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800439 }
440
Maurice Chu851222a2012-06-21 11:43:08 -0700441 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700442 isProfile
443 ? RawContactsEntity.PROFILE_CONTENT_URI
444 : RawContactsEntity.CONTENT_URI,
445 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700446 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700447
448 // Update the new state to use profile URIs if appropriate.
449 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700450 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700451 delta.setProfileQueryUri();
452 }
453 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800454 }
455 }
456
Josh Garguse692e012012-01-18 14:53:11 -0800457 // Now save any updated photos. We do this at the end to ensure that
458 // the ContactProvider already knows about newly-created contacts.
459 if (updatedPhotos != null) {
460 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700461 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800462 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800463
464 // If the raw-contact ID is negative, we are saving a new raw-contact;
465 // replace the bogus ID with the new one that we actually saved the contact at.
466 if (rawContactId < 0) {
467 rawContactId = insertedRawContactId;
468 if (rawContactId == -1) {
469 throw new IllegalStateException(
470 "Could not determine RawContact ID for image insertion");
471 }
472 }
473
Yorke Lee637a38e2013-09-14 08:36:33 -0700474 if (!saveUpdatedPhoto(rawContactId, photoUri)) succeeded = false;
Josh Garguse692e012012-01-18 14:53:11 -0800475 }
476 }
477
Josh Garguse5d3f892012-04-11 11:56:15 -0700478 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
479 if (callbackIntent != null) {
480 if (succeeded) {
481 // Mark the intent to indicate that the save was successful (even if the lookup URI
482 // is now null). For local contacts or the local profile, it's possible that the
483 // save triggered removal of the contact, so no lookup URI would exist..
484 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
485 }
486 callbackIntent.setData(lookupUri);
487 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800488 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800489 }
490
Josh Garguse692e012012-01-18 14:53:11 -0800491 /**
492 * Save updated photo for the specified raw-contact.
493 * @return true for success, false for failure
494 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700495 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800496 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800497 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
498 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
499
Yorke Lee637a38e2013-09-14 08:36:33 -0700500 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
Josh Garguse692e012012-01-18 14:53:11 -0800501 }
502
Josh Gargusef15c8e2012-01-30 16:42:02 -0800503 /**
504 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
505 */
Maurice Chu851222a2012-06-21 11:43:08 -0700506 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800507 final ArrayList<ContentProviderOperation> diff,
508 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800509 long existingRawContactId = state.findRawContactId();
510 if (existingRawContactId != -1) {
511 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800512 }
513
Josh Gargusef15c8e2012-01-30 16:42:02 -0800514 return getInsertedRawContactId(diff, results);
515 }
516
517 /**
518 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
519 */
520 private long getInsertedRawContactId(
521 final ArrayList<ContentProviderOperation> diff,
522 final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800523 if (results == null) {
524 return -1;
525 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800526 final int diffSize = diff.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800527 final int numResults = results.length;
528 for (int i = 0; i < diffSize && i < numResults; i++) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800529 ContentProviderOperation operation = diff.get(i);
Brian Attwell13f94e12015-01-22 16:27:48 -0800530 if (operation.isInsert() && operation.getUri().getEncodedPath().contains(
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800531 RawContacts.CONTENT_URI.getEncodedPath())) {
532 return ContentUris.parseId(results[i].uri);
533 }
534 }
535 return -1;
536 }
537
538 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700539 * Creates an intent that can be sent to this service to create a new group as
540 * well as add new members at the same time.
541 *
542 * @param context of the application
543 * @param account in which the group should be created
544 * @param label is the name of the group (cannot be null)
545 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
546 * should be added to the group
547 * @param callbackActivity is the activity to send the callback intent to
548 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700549 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700550 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700551 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700552 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800553 Intent serviceIntent = new Intent(context, ContactSaveService.class);
554 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
555 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
556 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700557 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800558 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700559 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700560
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800561 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700562 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800563 Intent callbackIntent = new Intent(context, callbackActivity);
564 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700565 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800566
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700567 return serviceIntent;
568 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800569
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800570 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700571 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
572 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
573 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
574 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700575 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800576
577 ContentValues values = new ContentValues();
578 values.put(Groups.ACCOUNT_TYPE, accountType);
579 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700580 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800581 values.put(Groups.TITLE, label);
582
Katherine Kuan717e3432011-07-13 17:03:24 -0700583 final ContentResolver resolver = getContentResolver();
584
585 // Create the new group
586 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
587
588 // If there's no URI, then the insertion failed. Abort early because group members can't be
589 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800590 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700591 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800592 return;
593 }
594
Katherine Kuan717e3432011-07-13 17:03:24 -0700595 // Add new group members
596 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
597
598 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
599 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800600 values.clear();
601 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
602 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
603
604 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700605 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700606 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800607 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800608 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800609 }
610
611 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800612 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800613 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700614 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700615 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800616 Intent serviceIntent = new Intent(context, ContactSaveService.class);
617 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
618 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
619 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700620
621 // Callback intent will be invoked by the service once the group is renamed.
622 Intent callbackIntent = new Intent(context, callbackActivity);
623 callbackIntent.setAction(callbackAction);
624 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
625
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800626 return serviceIntent;
627 }
628
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800629 private void renameGroup(Intent intent) {
630 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
631 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
632
633 if (groupId == -1) {
634 Log.e(TAG, "Invalid arguments for renameGroup request");
635 return;
636 }
637
638 ContentValues values = new ContentValues();
639 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700640 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
641 getContentResolver().update(groupUri, values, null, null);
642
643 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
644 callbackIntent.setData(groupUri);
645 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800646 }
647
648 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800649 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800650 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800651 public static Intent createGroupDeletionIntent(Context context, long groupId) {
652 Intent serviceIntent = new Intent(context, ContactSaveService.class);
653 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800654 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800655 return serviceIntent;
656 }
657
658 private void deleteGroup(Intent intent) {
659 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
660 if (groupId == -1) {
661 Log.e(TAG, "Invalid arguments for deleteGroup request");
662 return;
663 }
664
665 getContentResolver().delete(
666 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
667 }
668
669 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700670 * Creates an intent that can be sent to this service to rename a group as
671 * well as add and remove members from the group.
672 *
673 * @param context of the application
674 * @param groupId of the group that should be modified
675 * @param newLabel is the updated name of the group (can be null if the name
676 * should not be updated)
677 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
678 * should be added to the group
679 * @param rawContactsToRemove is an array of raw contact IDs for contacts
680 * that should be removed from the group
681 * @param callbackActivity is the activity to send the callback intent to
682 * @param callbackAction is the intent action for the callback intent
683 */
684 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
685 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700686 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700687 Intent serviceIntent = new Intent(context, ContactSaveService.class);
688 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
689 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
690 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
691 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
692 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
693 rawContactsToRemove);
694
695 // Callback intent will be invoked by the service once the group is updated
696 Intent callbackIntent = new Intent(context, callbackActivity);
697 callbackIntent.setAction(callbackAction);
698 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
699
700 return serviceIntent;
701 }
702
703 private void updateGroup(Intent intent) {
704 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
705 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
706 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
707 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
708
709 if (groupId == -1) {
710 Log.e(TAG, "Invalid arguments for updateGroup request");
711 return;
712 }
713
714 final ContentResolver resolver = getContentResolver();
715 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
716
717 // Update group name if necessary
718 if (label != null) {
719 ContentValues values = new ContentValues();
720 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700721 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700722 }
723
Katherine Kuan717e3432011-07-13 17:03:24 -0700724 // Add and remove members if necessary
725 addMembersToGroup(resolver, rawContactsToAdd, groupId);
726 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
727
728 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
729 callbackIntent.setData(groupUri);
730 deliverCallback(callbackIntent);
731 }
732
Daniel Lehmann18958a22012-02-28 17:45:25 -0800733 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700734 long groupId) {
735 if (rawContactsToAdd == null) {
736 return;
737 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700738 for (long rawContactId : rawContactsToAdd) {
739 try {
740 final ArrayList<ContentProviderOperation> rawContactOperations =
741 new ArrayList<ContentProviderOperation>();
742
743 // Build an assert operation to ensure the contact is not already in the group
744 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
745 .newAssertQuery(Data.CONTENT_URI);
746 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
747 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
748 new String[] { String.valueOf(rawContactId),
749 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
750 assertBuilder.withExpectedCount(0);
751 rawContactOperations.add(assertBuilder.build());
752
753 // Build an insert operation to add the contact to the group
754 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
755 .newInsert(Data.CONTENT_URI);
756 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
757 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
758 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
759 rawContactOperations.add(insertBuilder.build());
760
761 if (DEBUG) {
762 for (ContentProviderOperation operation : rawContactOperations) {
763 Log.v(TAG, operation.toString());
764 }
765 }
766
767 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700768 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800769 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700770 }
771 } catch (RemoteException e) {
772 // Something went wrong, bail without success
773 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
774 String.valueOf(rawContactId), e);
775 } catch (OperationApplicationException e) {
776 // The assert could have failed because the contact is already in the group,
777 // just continue to the next contact
778 Log.w(TAG, "Assert failed in adding raw contact ID " +
779 String.valueOf(rawContactId) + ". Already exists in group " +
780 String.valueOf(groupId), e);
781 }
782 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700783 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700784
Daniel Lehmann18958a22012-02-28 17:45:25 -0800785 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700786 long groupId) {
787 if (rawContactsToRemove == null) {
788 return;
789 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700790 for (long rawContactId : rawContactsToRemove) {
791 // Apply the delete operation on the data row for the given raw contact's
792 // membership in the given group. If no contact matches the provided selection, then
793 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800794 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700795 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
796 new String[] { String.valueOf(rawContactId),
797 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
798 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700799 }
800
801 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800802 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800803 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800804 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
805 Intent serviceIntent = new Intent(context, ContactSaveService.class);
806 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
807 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
808 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
809
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800810 return serviceIntent;
811 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800812
813 private void setStarred(Intent intent) {
814 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
815 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
816 if (contactUri == null) {
817 Log.e(TAG, "Invalid arguments for setStarred request");
818 return;
819 }
820
821 final ContentValues values = new ContentValues(1);
822 values.put(Contacts.STARRED, value);
823 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700824
825 // Undemote the contact if necessary
826 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
827 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800828 if (c == null) {
829 return;
830 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700831 try {
832 if (c.moveToFirst()) {
833 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700834
835 // Don't bother undemoting if this contact is the user's profile.
836 if (id < Profile.MIN_ID) {
Brian Attwell2d88efa2014-12-17 21:49:56 -0800837 PinnedPositions.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700838 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700839 }
840 } finally {
841 c.close();
842 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800843 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800844
845 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700846 * Creates an intent that can be sent to this service to set the redirect to voicemail.
847 */
848 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
849 boolean value) {
850 Intent serviceIntent = new Intent(context, ContactSaveService.class);
851 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
852 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
853 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
854
855 return serviceIntent;
856 }
857
858 private void setSendToVoicemail(Intent intent) {
859 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
860 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
861 if (contactUri == null) {
862 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
863 return;
864 }
865
866 final ContentValues values = new ContentValues(1);
867 values.put(Contacts.SEND_TO_VOICEMAIL, value);
868 getContentResolver().update(contactUri, values, null, null);
869 }
870
871 /**
872 * Creates an intent that can be sent to this service to save the contact's ringtone.
873 */
874 public static Intent createSetRingtone(Context context, Uri contactUri,
875 String value) {
876 Intent serviceIntent = new Intent(context, ContactSaveService.class);
877 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
878 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
879 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
880
881 return serviceIntent;
882 }
883
884 private void setRingtone(Intent intent) {
885 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
886 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
887 if (contactUri == null) {
888 Log.e(TAG, "Invalid arguments for setRingtone");
889 return;
890 }
891 ContentValues values = new ContentValues(1);
892 values.put(Contacts.CUSTOM_RINGTONE, value);
893 getContentResolver().update(contactUri, values, null, null);
894 }
895
896 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800897 * Creates an intent that sets the selected data item as super primary (default)
898 */
899 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
900 Intent serviceIntent = new Intent(context, ContactSaveService.class);
901 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
902 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
903 return serviceIntent;
904 }
905
906 private void setSuperPrimary(Intent intent) {
907 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
908 if (dataId == -1) {
909 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
910 return;
911 }
912
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700913 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800914 }
915
916 /**
917 * Creates an intent that clears the primary flag of all data items that belong to the same
918 * raw_contact as the given data item. Will only clear, if the data item was primary before
919 * this call
920 */
921 public static Intent createClearPrimaryIntent(Context context, long dataId) {
922 Intent serviceIntent = new Intent(context, ContactSaveService.class);
923 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
924 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
925 return serviceIntent;
926 }
927
928 private void clearPrimary(Intent intent) {
929 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
930 if (dataId == -1) {
931 Log.e(TAG, "Invalid arguments for clearPrimary request");
932 return;
933 }
934
935 // Update the primary values in the data record.
936 ContentValues values = new ContentValues(1);
937 values.put(Data.IS_SUPER_PRIMARY, 0);
938 values.put(Data.IS_PRIMARY, 0);
939
940 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
941 values, null, null);
942 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800943
944 /**
945 * Creates an intent that can be sent to this service to delete a contact.
946 */
947 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
948 Intent serviceIntent = new Intent(context, ContactSaveService.class);
949 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
950 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
951 return serviceIntent;
952 }
953
Brian Attwelld2962a32015-03-02 14:48:50 -0800954 /**
955 * Creates an intent that can be sent to this service to delete multiple contacts.
956 */
957 public static Intent createDeleteMultipleContactsIntent(Context context,
958 long[] contactIds) {
959 Intent serviceIntent = new Intent(context, ContactSaveService.class);
960 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
961 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
962 return serviceIntent;
963 }
964
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800965 private void deleteContact(Intent intent) {
966 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
967 if (contactUri == null) {
968 Log.e(TAG, "Invalid arguments for deleteContact request");
969 return;
970 }
971
972 getContentResolver().delete(contactUri, null, null);
973 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800974
Brian Attwelld2962a32015-03-02 14:48:50 -0800975 private void deleteMultipleContacts(Intent intent) {
976 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
977 if (contactIds == null) {
978 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
979 return;
980 }
981 for (long contactId : contactIds) {
982 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
983 getContentResolver().delete(contactUri, null, null);
984 }
985
986 }
987
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800988 /**
989 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -0800990 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800991 */
992 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -0800993 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800994 Intent serviceIntent = new Intent(context, ContactSaveService.class);
995 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
996 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
997 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800998
999 // Callback intent will be invoked by the service once the contacts are joined.
1000 Intent callbackIntent = new Intent(context, callbackActivity);
1001 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001002 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1003
1004 return serviceIntent;
1005 }
1006
Brian Attwelld3946ca2015-03-03 11:13:49 -08001007 /**
1008 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1009 * No special attention is paid to where the resulting contact's name is taken from.
1010 */
1011 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1012 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1013 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1014 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1015 return serviceIntent;
1016 }
1017
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001018
1019 private interface JoinContactQuery {
1020 String[] PROJECTION = {
1021 RawContacts._ID,
1022 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001023 RawContacts.DISPLAY_NAME_SOURCE,
1024 };
1025
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001026 int _ID = 0;
1027 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001028 int DISPLAY_NAME_SOURCE = 2;
1029 }
1030
1031 private interface ContactEntityQuery {
1032 String[] PROJECTION = {
1033 Contacts.Entity.DATA_ID,
1034 Contacts.Entity.CONTACT_ID,
1035 Contacts.Entity.IS_SUPER_PRIMARY,
1036 };
1037 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1038 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1039 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1040 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1041
1042 int DATA_ID = 0;
1043 int CONTACT_ID = 1;
1044 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001045 }
1046
Brian Attwelld3946ca2015-03-03 11:13:49 -08001047 private void joinSeveralContacts(Intent intent) {
1048 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001049
Brian Attwelld3946ca2015-03-03 11:13:49 -08001050 // Load raw contact IDs for all contacts involved.
1051 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1052 if (rawContactIds == null) {
1053 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001054 return;
1055 }
1056
Brian Attwelld3946ca2015-03-03 11:13:49 -08001057 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001058 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001059 final ArrayList<ContentProviderOperation> operations
1060 = new ArrayList<ContentProviderOperation>();
1061 for (int i = 0; i < rawContactIds.length; i++) {
1062 for (int j = 0; j < rawContactIds.length; j++) {
1063 if (i != j) {
1064 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1065 }
1066 }
1067 }
1068
1069 // Apply all aggregation exceptions as one batch
1070 try {
1071 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1072 showToast(R.string.contactsJoinedMessage);
1073 } catch (RemoteException | OperationApplicationException e) {
1074 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1075 showToast(R.string.contactSavedErrorToast);
1076 }
1077 }
1078
1079
1080 private void joinContacts(Intent intent) {
1081 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1082 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001083
1084 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001085 // in the join UIs.
1086 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1087 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001088 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001089 return;
1090 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001091
Brian Attwell548f5c62015-01-27 17:46:46 -08001092 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001093
1094 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001095 for (int i = 0; i < rawContactIds.length; i++) {
1096 for (int j = 0; j < rawContactIds.length; j++) {
1097 if (i != j) {
1098 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1099 }
1100 }
1101 }
1102
Brian Attwelld3946ca2015-03-03 11:13:49 -08001103 final ContentResolver resolver = getContentResolver();
1104
Brian Attwell548f5c62015-01-27 17:46:46 -08001105 // Use the name for contactId1 as the name for the newly aggregated contact.
1106 final Uri contactId1Uri = ContentUris.withAppendedId(
1107 Contacts.CONTENT_URI, contactId1);
1108 final Uri entityUri = Uri.withAppendedPath(
1109 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1110 Cursor c = resolver.query(entityUri,
1111 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1112 if (c == null) {
1113 Log.e(TAG, "Unable to open Contacts DB cursor");
1114 showToast(R.string.contactSavedErrorToast);
1115 return;
1116 }
1117 long dataIdToAddSuperPrimary = -1;
1118 try {
1119 if (c.moveToFirst()) {
1120 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1121 }
1122 } finally {
1123 c.close();
1124 }
1125
1126 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1127 // display name does not change as a result of the join.
1128 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001129 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001130 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1131 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1132 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001133 operations.add(builder.build());
1134 }
1135
1136 boolean success = false;
1137 // Apply all aggregation exceptions as one batch
1138 try {
1139 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001140 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001141 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001142 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001143 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001144 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001145 }
1146
1147 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1148 if (success) {
1149 Uri uri = RawContacts.getContactLookupUri(resolver,
1150 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1151 callbackIntent.setData(uri);
1152 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001153 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001154 }
1155
Brian Attwelld3946ca2015-03-03 11:13:49 -08001156 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1157 if (contactIds == null) {
1158 return null;
1159 }
1160
Brian Attwell548f5c62015-01-27 17:46:46 -08001161 final ContentResolver resolver = getContentResolver();
1162 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001163
1164 final StringBuilder queryBuilder = new StringBuilder();
1165 final String stringContactIds[] = new String[contactIds.length];
1166 for (int i = 0; i < contactIds.length; i++) {
1167 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1168 stringContactIds[i] = String.valueOf(contactIds[i]);
1169 if (contactIds[i] == -1) {
1170 return null;
1171 }
1172 if (i == contactIds.length -1) {
1173 break;
1174 }
1175 queryBuilder.append(" OR ");
1176 }
1177
Brian Attwell548f5c62015-01-27 17:46:46 -08001178 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1179 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001180 queryBuilder.toString(),
1181 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001182 if (c == null) {
1183 Log.e(TAG, "Unable to open Contacts DB cursor");
1184 showToast(R.string.contactSavedErrorToast);
1185 return null;
1186 }
1187 try {
1188 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001189 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001190 return null;
1191 }
1192 rawContactIds = new long[c.getCount()];
1193 for (int i = 0; i < rawContactIds.length; i++) {
1194 c.moveToPosition(i);
1195 long rawContactId = c.getLong(JoinContactQuery._ID);
1196 rawContactIds[i] = rawContactId;
1197 }
1198 } finally {
1199 c.close();
1200 }
1201 return rawContactIds;
1202 }
1203
Brian Attwelld3946ca2015-03-03 11:13:49 -08001204 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1205 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1206 }
1207
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001208 /**
1209 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1210 */
1211 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1212 long rawContactId1, long rawContactId2) {
1213 Builder builder =
1214 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1215 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1216 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1217 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1218 operations.add(builder.build());
1219 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001220
1221 /**
1222 * Shows a toast on the UI thread.
1223 */
1224 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001225 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001226
1227 @Override
1228 public void run() {
1229 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1230 }
1231 });
1232 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001233
1234 private void deliverCallback(final Intent callbackIntent) {
1235 mMainHandler.post(new Runnable() {
1236
1237 @Override
1238 public void run() {
1239 deliverCallbackOnUiThread(callbackIntent);
1240 }
1241 });
1242 }
1243
1244 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1245 // TODO: this assumes that if there are multiple instances of the same
1246 // activity registered, the last one registered is the one waiting for
1247 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001248 for (Listener listener : sListeners) {
1249 if (callbackIntent.getComponent().equals(
1250 ((Activity) listener).getIntent().getComponent())) {
1251 listener.onServiceCompleted(callbackIntent);
1252 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001253 }
1254 }
1255 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001256}