blob: 29a4a7b398d7120c0c5cfdad17a29af9db998026 [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);
Walter Jang1e8801b2015-03-10 15:57:05 -0700333 if (updatedPhotos != null) {
334 callbackIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
335 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700336 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
337 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800338 return serviceIntent;
339 }
340
341 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700342 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700343 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800344 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800345
Jay Shrauner08099782015-03-25 14:17:11 -0700346 if (state == null) {
347 Log.e(TAG, "Invalid arguments for saveContact request");
348 return;
349 }
350
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800351 // Trim any empty fields, and RawContacts, before persisting
352 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700353 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800354
355 Uri lookupUri = null;
356
357 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800358 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800359
Josh Gargusef15c8e2012-01-30 16:42:02 -0800360 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
361 long insertedRawContactId = -1;
362
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800363 // Attempt to persist changes
364 int tries = 0;
365 while (tries++ < PERSIST_TRIES) {
366 try {
367 // Build operations and try applying
368 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700369 if (DEBUG) {
370 Log.v(TAG, "Content Provider Operations:");
371 for (ContentProviderOperation operation : diff) {
372 Log.v(TAG, operation.toString());
373 }
374 }
375
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800376 ContentProviderResult[] results = null;
377 if (!diff.isEmpty()) {
378 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
Jay Shrauner511561d2015-04-02 10:35:33 -0700379 if (results == null) {
380 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
381 // Retry save
382 continue;
383 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800384 }
385
386 final long rawContactId = getRawContactId(state, diff, results);
387 if (rawContactId == -1) {
388 throw new IllegalStateException("Could not determine RawContact ID after save");
389 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800390 // We don't have to check to see if the value is still -1. If we reach here,
391 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
392 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700393 if (isProfile) {
394 // Since the profile supports local raw contacts, which may have been completely
395 // removed if all information was removed, we need to do a special query to
396 // get the lookup URI for the profile contact (if it still exists).
397 Cursor c = resolver.query(Profile.CONTENT_URI,
398 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
399 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800400 if (c == null) {
401 continue;
402 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700403 try {
Erik162b7e32011-09-20 15:23:55 -0700404 if (c.moveToFirst()) {
405 final long contactId = c.getLong(0);
406 final String lookupKey = c.getString(1);
407 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
408 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700409 } finally {
410 c.close();
411 }
412 } else {
413 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
414 rawContactId);
415 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
416 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800417 if (lookupUri != null) {
418 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
419 }
Josh Garguse692e012012-01-18 14:53:11 -0800420
421 // We can change this back to false later, if we fail to save the contact photo.
422 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800423 break;
424
425 } catch (RemoteException e) {
426 // Something went wrong, bail without success
427 Log.e(TAG, "Problem persisting user edits", e);
428 break;
429
Jay Shrauner57fca182014-01-17 14:20:50 -0800430 } catch (IllegalArgumentException e) {
431 // This is thrown by applyBatch on malformed requests
432 Log.e(TAG, "Problem persisting user edits", e);
433 showToast(R.string.contactSavedErrorToast);
434 break;
435
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800436 } catch (OperationApplicationException e) {
437 // Version consistency failed, re-parent change and try again
438 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
439 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
440 boolean first = true;
441 final int count = state.size();
442 for (int i = 0; i < count; i++) {
443 Long rawContactId = state.getRawContactId(i);
444 if (rawContactId != null && rawContactId != -1) {
445 if (!first) {
446 sb.append(',');
447 }
448 sb.append(rawContactId);
449 first = false;
450 }
451 }
452 sb.append(")");
453
454 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800455 throw new IllegalStateException(
456 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800457 }
458
Maurice Chu851222a2012-06-21 11:43:08 -0700459 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700460 isProfile
461 ? RawContactsEntity.PROFILE_CONTENT_URI
462 : RawContactsEntity.CONTENT_URI,
463 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700464 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700465
466 // Update the new state to use profile URIs if appropriate.
467 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700468 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700469 delta.setProfileQueryUri();
470 }
471 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800472 }
473 }
474
Josh Garguse692e012012-01-18 14:53:11 -0800475 // Now save any updated photos. We do this at the end to ensure that
476 // the ContactProvider already knows about newly-created contacts.
477 if (updatedPhotos != null) {
478 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700479 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800480 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800481
482 // If the raw-contact ID is negative, we are saving a new raw-contact;
483 // replace the bogus ID with the new one that we actually saved the contact at.
484 if (rawContactId < 0) {
485 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800486 }
487
Jay Shrauner511561d2015-04-02 10:35:33 -0700488 // If the save failed, insertedRawContactId will be -1
489 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri)) {
490 succeeded = false;
491 }
Josh Garguse692e012012-01-18 14:53:11 -0800492 }
493 }
494
Josh Garguse5d3f892012-04-11 11:56:15 -0700495 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
496 if (callbackIntent != null) {
497 if (succeeded) {
498 // Mark the intent to indicate that the save was successful (even if the lookup URI
499 // is now null). For local contacts or the local profile, it's possible that the
500 // save triggered removal of the contact, so no lookup URI would exist..
501 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
502 }
503 callbackIntent.setData(lookupUri);
504 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800505 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800506 }
507
Josh Garguse692e012012-01-18 14:53:11 -0800508 /**
509 * Save updated photo for the specified raw-contact.
510 * @return true for success, false for failure
511 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700512 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800513 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800514 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
515 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
516
Yorke Lee637a38e2013-09-14 08:36:33 -0700517 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
Josh Garguse692e012012-01-18 14:53:11 -0800518 }
519
Josh Gargusef15c8e2012-01-30 16:42:02 -0800520 /**
521 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
522 */
Maurice Chu851222a2012-06-21 11:43:08 -0700523 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800524 final ArrayList<ContentProviderOperation> diff,
525 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800526 long existingRawContactId = state.findRawContactId();
527 if (existingRawContactId != -1) {
528 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800529 }
530
Josh Gargusef15c8e2012-01-30 16:42:02 -0800531 return getInsertedRawContactId(diff, results);
532 }
533
534 /**
535 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
536 */
537 private long getInsertedRawContactId(
538 final ArrayList<ContentProviderOperation> diff,
539 final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800540 if (results == null) {
541 return -1;
542 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800543 final int diffSize = diff.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800544 final int numResults = results.length;
545 for (int i = 0; i < diffSize && i < numResults; i++) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800546 ContentProviderOperation operation = diff.get(i);
Brian Attwell13f94e12015-01-22 16:27:48 -0800547 if (operation.isInsert() && operation.getUri().getEncodedPath().contains(
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800548 RawContacts.CONTENT_URI.getEncodedPath())) {
549 return ContentUris.parseId(results[i].uri);
550 }
551 }
552 return -1;
553 }
554
555 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700556 * Creates an intent that can be sent to this service to create a new group as
557 * well as add new members at the same time.
558 *
559 * @param context of the application
560 * @param account in which the group should be created
561 * @param label is the name of the group (cannot be null)
562 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
563 * should be added to the group
564 * @param callbackActivity is the activity to send the callback intent to
565 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700566 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700567 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700568 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700569 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800570 Intent serviceIntent = new Intent(context, ContactSaveService.class);
571 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
572 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
573 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700574 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800575 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700576 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700577
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800578 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700579 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800580 Intent callbackIntent = new Intent(context, callbackActivity);
581 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700582 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800583
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700584 return serviceIntent;
585 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800586
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800587 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700588 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
589 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
590 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
591 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700592 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800593
594 ContentValues values = new ContentValues();
595 values.put(Groups.ACCOUNT_TYPE, accountType);
596 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700597 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800598 values.put(Groups.TITLE, label);
599
Katherine Kuan717e3432011-07-13 17:03:24 -0700600 final ContentResolver resolver = getContentResolver();
601
602 // Create the new group
603 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
604
605 // If there's no URI, then the insertion failed. Abort early because group members can't be
606 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800607 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700608 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800609 return;
610 }
611
Katherine Kuan717e3432011-07-13 17:03:24 -0700612 // Add new group members
613 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
614
615 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
616 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800617 values.clear();
618 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
619 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
620
621 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700622 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700623 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800624 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800625 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800626 }
627
628 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800629 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800630 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700631 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700632 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800633 Intent serviceIntent = new Intent(context, ContactSaveService.class);
634 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
635 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
636 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700637
638 // Callback intent will be invoked by the service once the group is renamed.
639 Intent callbackIntent = new Intent(context, callbackActivity);
640 callbackIntent.setAction(callbackAction);
641 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
642
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800643 return serviceIntent;
644 }
645
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800646 private void renameGroup(Intent intent) {
647 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
648 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
649
650 if (groupId == -1) {
651 Log.e(TAG, "Invalid arguments for renameGroup request");
652 return;
653 }
654
655 ContentValues values = new ContentValues();
656 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700657 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
658 getContentResolver().update(groupUri, values, null, null);
659
660 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
661 callbackIntent.setData(groupUri);
662 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800663 }
664
665 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800666 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800667 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800668 public static Intent createGroupDeletionIntent(Context context, long groupId) {
669 Intent serviceIntent = new Intent(context, ContactSaveService.class);
670 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800671 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800672 return serviceIntent;
673 }
674
675 private void deleteGroup(Intent intent) {
676 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
677 if (groupId == -1) {
678 Log.e(TAG, "Invalid arguments for deleteGroup request");
679 return;
680 }
681
682 getContentResolver().delete(
683 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
684 }
685
686 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700687 * Creates an intent that can be sent to this service to rename a group as
688 * well as add and remove members from the group.
689 *
690 * @param context of the application
691 * @param groupId of the group that should be modified
692 * @param newLabel is the updated name of the group (can be null if the name
693 * should not be updated)
694 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
695 * should be added to the group
696 * @param rawContactsToRemove is an array of raw contact IDs for contacts
697 * that should be removed from the group
698 * @param callbackActivity is the activity to send the callback intent to
699 * @param callbackAction is the intent action for the callback intent
700 */
701 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
702 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700703 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700704 Intent serviceIntent = new Intent(context, ContactSaveService.class);
705 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
706 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
707 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
708 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
709 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
710 rawContactsToRemove);
711
712 // Callback intent will be invoked by the service once the group is updated
713 Intent callbackIntent = new Intent(context, callbackActivity);
714 callbackIntent.setAction(callbackAction);
715 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
716
717 return serviceIntent;
718 }
719
720 private void updateGroup(Intent intent) {
721 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
722 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
723 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
724 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
725
726 if (groupId == -1) {
727 Log.e(TAG, "Invalid arguments for updateGroup request");
728 return;
729 }
730
731 final ContentResolver resolver = getContentResolver();
732 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
733
734 // Update group name if necessary
735 if (label != null) {
736 ContentValues values = new ContentValues();
737 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700738 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700739 }
740
Katherine Kuan717e3432011-07-13 17:03:24 -0700741 // Add and remove members if necessary
742 addMembersToGroup(resolver, rawContactsToAdd, groupId);
743 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
744
745 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
746 callbackIntent.setData(groupUri);
747 deliverCallback(callbackIntent);
748 }
749
Daniel Lehmann18958a22012-02-28 17:45:25 -0800750 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700751 long groupId) {
752 if (rawContactsToAdd == null) {
753 return;
754 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700755 for (long rawContactId : rawContactsToAdd) {
756 try {
757 final ArrayList<ContentProviderOperation> rawContactOperations =
758 new ArrayList<ContentProviderOperation>();
759
760 // Build an assert operation to ensure the contact is not already in the group
761 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
762 .newAssertQuery(Data.CONTENT_URI);
763 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
764 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
765 new String[] { String.valueOf(rawContactId),
766 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
767 assertBuilder.withExpectedCount(0);
768 rawContactOperations.add(assertBuilder.build());
769
770 // Build an insert operation to add the contact to the group
771 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
772 .newInsert(Data.CONTENT_URI);
773 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
774 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
775 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
776 rawContactOperations.add(insertBuilder.build());
777
778 if (DEBUG) {
779 for (ContentProviderOperation operation : rawContactOperations) {
780 Log.v(TAG, operation.toString());
781 }
782 }
783
784 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700785 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800786 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700787 }
788 } catch (RemoteException e) {
789 // Something went wrong, bail without success
790 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
791 String.valueOf(rawContactId), e);
792 } catch (OperationApplicationException e) {
793 // The assert could have failed because the contact is already in the group,
794 // just continue to the next contact
795 Log.w(TAG, "Assert failed in adding raw contact ID " +
796 String.valueOf(rawContactId) + ". Already exists in group " +
797 String.valueOf(groupId), e);
798 }
799 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700800 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700801
Daniel Lehmann18958a22012-02-28 17:45:25 -0800802 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700803 long groupId) {
804 if (rawContactsToRemove == null) {
805 return;
806 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700807 for (long rawContactId : rawContactsToRemove) {
808 // Apply the delete operation on the data row for the given raw contact's
809 // membership in the given group. If no contact matches the provided selection, then
810 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800811 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700812 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
813 new String[] { String.valueOf(rawContactId),
814 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
815 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700816 }
817
818 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800819 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800820 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800821 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
822 Intent serviceIntent = new Intent(context, ContactSaveService.class);
823 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
824 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
825 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
826
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800827 return serviceIntent;
828 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800829
830 private void setStarred(Intent intent) {
831 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
832 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
833 if (contactUri == null) {
834 Log.e(TAG, "Invalid arguments for setStarred request");
835 return;
836 }
837
838 final ContentValues values = new ContentValues(1);
839 values.put(Contacts.STARRED, value);
840 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700841
842 // Undemote the contact if necessary
843 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
844 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800845 if (c == null) {
846 return;
847 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700848 try {
849 if (c.moveToFirst()) {
850 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700851
852 // Don't bother undemoting if this contact is the user's profile.
853 if (id < Profile.MIN_ID) {
Brian Attwell2d88efa2014-12-17 21:49:56 -0800854 PinnedPositions.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700855 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700856 }
857 } finally {
858 c.close();
859 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800860 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800861
862 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700863 * Creates an intent that can be sent to this service to set the redirect to voicemail.
864 */
865 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
866 boolean value) {
867 Intent serviceIntent = new Intent(context, ContactSaveService.class);
868 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
869 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
870 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
871
872 return serviceIntent;
873 }
874
875 private void setSendToVoicemail(Intent intent) {
876 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
877 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
878 if (contactUri == null) {
879 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
880 return;
881 }
882
883 final ContentValues values = new ContentValues(1);
884 values.put(Contacts.SEND_TO_VOICEMAIL, value);
885 getContentResolver().update(contactUri, values, null, null);
886 }
887
888 /**
889 * Creates an intent that can be sent to this service to save the contact's ringtone.
890 */
891 public static Intent createSetRingtone(Context context, Uri contactUri,
892 String value) {
893 Intent serviceIntent = new Intent(context, ContactSaveService.class);
894 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
895 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
896 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
897
898 return serviceIntent;
899 }
900
901 private void setRingtone(Intent intent) {
902 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
903 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
904 if (contactUri == null) {
905 Log.e(TAG, "Invalid arguments for setRingtone");
906 return;
907 }
908 ContentValues values = new ContentValues(1);
909 values.put(Contacts.CUSTOM_RINGTONE, value);
910 getContentResolver().update(contactUri, values, null, null);
911 }
912
913 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800914 * Creates an intent that sets the selected data item as super primary (default)
915 */
916 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
917 Intent serviceIntent = new Intent(context, ContactSaveService.class);
918 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
919 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
920 return serviceIntent;
921 }
922
923 private void setSuperPrimary(Intent intent) {
924 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
925 if (dataId == -1) {
926 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
927 return;
928 }
929
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700930 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800931 }
932
933 /**
934 * Creates an intent that clears the primary flag of all data items that belong to the same
935 * raw_contact as the given data item. Will only clear, if the data item was primary before
936 * this call
937 */
938 public static Intent createClearPrimaryIntent(Context context, long dataId) {
939 Intent serviceIntent = new Intent(context, ContactSaveService.class);
940 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
941 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
942 return serviceIntent;
943 }
944
945 private void clearPrimary(Intent intent) {
946 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
947 if (dataId == -1) {
948 Log.e(TAG, "Invalid arguments for clearPrimary request");
949 return;
950 }
951
952 // Update the primary values in the data record.
953 ContentValues values = new ContentValues(1);
954 values.put(Data.IS_SUPER_PRIMARY, 0);
955 values.put(Data.IS_PRIMARY, 0);
956
957 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
958 values, null, null);
959 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800960
961 /**
962 * Creates an intent that can be sent to this service to delete a contact.
963 */
964 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
965 Intent serviceIntent = new Intent(context, ContactSaveService.class);
966 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
967 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
968 return serviceIntent;
969 }
970
Brian Attwelld2962a32015-03-02 14:48:50 -0800971 /**
972 * Creates an intent that can be sent to this service to delete multiple contacts.
973 */
974 public static Intent createDeleteMultipleContactsIntent(Context context,
975 long[] contactIds) {
976 Intent serviceIntent = new Intent(context, ContactSaveService.class);
977 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
978 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
979 return serviceIntent;
980 }
981
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800982 private void deleteContact(Intent intent) {
983 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
984 if (contactUri == null) {
985 Log.e(TAG, "Invalid arguments for deleteContact request");
986 return;
987 }
988
989 getContentResolver().delete(contactUri, null, null);
990 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800991
Brian Attwelld2962a32015-03-02 14:48:50 -0800992 private void deleteMultipleContacts(Intent intent) {
993 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
994 if (contactIds == null) {
995 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
996 return;
997 }
998 for (long contactId : contactIds) {
999 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1000 getContentResolver().delete(contactUri, null, null);
1001 }
Brian Attwelle986c6b2015-03-05 19:47:30 -08001002 showToast(R.string.contacts_deleted_toast);
Brian Attwelld2962a32015-03-02 14:48:50 -08001003 }
1004
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001005 /**
1006 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001007 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001008 */
1009 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001010 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001011 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1012 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1013 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1014 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001015
1016 // Callback intent will be invoked by the service once the contacts are joined.
1017 Intent callbackIntent = new Intent(context, callbackActivity);
1018 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001019 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1020
1021 return serviceIntent;
1022 }
1023
Brian Attwelld3946ca2015-03-03 11:13:49 -08001024 /**
1025 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1026 * No special attention is paid to where the resulting contact's name is taken from.
1027 */
1028 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1029 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1030 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1031 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1032 return serviceIntent;
1033 }
1034
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001035
1036 private interface JoinContactQuery {
1037 String[] PROJECTION = {
1038 RawContacts._ID,
1039 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001040 RawContacts.DISPLAY_NAME_SOURCE,
1041 };
1042
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001043 int _ID = 0;
1044 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001045 int DISPLAY_NAME_SOURCE = 2;
1046 }
1047
1048 private interface ContactEntityQuery {
1049 String[] PROJECTION = {
1050 Contacts.Entity.DATA_ID,
1051 Contacts.Entity.CONTACT_ID,
1052 Contacts.Entity.IS_SUPER_PRIMARY,
1053 };
1054 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1055 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1056 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1057 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1058
1059 int DATA_ID = 0;
1060 int CONTACT_ID = 1;
1061 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001062 }
1063
Brian Attwelld3946ca2015-03-03 11:13:49 -08001064 private void joinSeveralContacts(Intent intent) {
1065 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001066
Brian Attwelld3946ca2015-03-03 11:13:49 -08001067 // Load raw contact IDs for all contacts involved.
1068 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1069 if (rawContactIds == null) {
1070 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001071 return;
1072 }
1073
Brian Attwelld3946ca2015-03-03 11:13:49 -08001074 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001075 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001076 final ArrayList<ContentProviderOperation> operations
1077 = new ArrayList<ContentProviderOperation>();
1078 for (int i = 0; i < rawContactIds.length; i++) {
1079 for (int j = 0; j < rawContactIds.length; j++) {
1080 if (i != j) {
1081 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1082 }
1083 }
1084 }
1085
1086 // Apply all aggregation exceptions as one batch
1087 try {
1088 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1089 showToast(R.string.contactsJoinedMessage);
1090 } catch (RemoteException | OperationApplicationException e) {
1091 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1092 showToast(R.string.contactSavedErrorToast);
1093 }
1094 }
1095
1096
1097 private void joinContacts(Intent intent) {
1098 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1099 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001100
1101 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001102 // in the join UIs.
1103 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1104 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001105 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001106 return;
1107 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001108
Brian Attwell548f5c62015-01-27 17:46:46 -08001109 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001110
1111 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001112 for (int i = 0; i < rawContactIds.length; i++) {
1113 for (int j = 0; j < rawContactIds.length; j++) {
1114 if (i != j) {
1115 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1116 }
1117 }
1118 }
1119
Brian Attwelld3946ca2015-03-03 11:13:49 -08001120 final ContentResolver resolver = getContentResolver();
1121
Brian Attwell548f5c62015-01-27 17:46:46 -08001122 // Use the name for contactId1 as the name for the newly aggregated contact.
1123 final Uri contactId1Uri = ContentUris.withAppendedId(
1124 Contacts.CONTENT_URI, contactId1);
1125 final Uri entityUri = Uri.withAppendedPath(
1126 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1127 Cursor c = resolver.query(entityUri,
1128 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1129 if (c == null) {
1130 Log.e(TAG, "Unable to open Contacts DB cursor");
1131 showToast(R.string.contactSavedErrorToast);
1132 return;
1133 }
1134 long dataIdToAddSuperPrimary = -1;
1135 try {
1136 if (c.moveToFirst()) {
1137 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1138 }
1139 } finally {
1140 c.close();
1141 }
1142
1143 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1144 // display name does not change as a result of the join.
1145 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001146 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001147 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1148 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1149 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001150 operations.add(builder.build());
1151 }
1152
1153 boolean success = false;
1154 // Apply all aggregation exceptions as one batch
1155 try {
1156 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001157 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001158 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001159 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001160 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001161 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001162 }
1163
1164 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1165 if (success) {
1166 Uri uri = RawContacts.getContactLookupUri(resolver,
1167 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1168 callbackIntent.setData(uri);
1169 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001170 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001171 }
1172
Brian Attwelld3946ca2015-03-03 11:13:49 -08001173 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1174 if (contactIds == null) {
1175 return null;
1176 }
1177
Brian Attwell548f5c62015-01-27 17:46:46 -08001178 final ContentResolver resolver = getContentResolver();
1179 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001180
1181 final StringBuilder queryBuilder = new StringBuilder();
1182 final String stringContactIds[] = new String[contactIds.length];
1183 for (int i = 0; i < contactIds.length; i++) {
1184 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1185 stringContactIds[i] = String.valueOf(contactIds[i]);
1186 if (contactIds[i] == -1) {
1187 return null;
1188 }
1189 if (i == contactIds.length -1) {
1190 break;
1191 }
1192 queryBuilder.append(" OR ");
1193 }
1194
Brian Attwell548f5c62015-01-27 17:46:46 -08001195 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1196 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001197 queryBuilder.toString(),
1198 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001199 if (c == null) {
1200 Log.e(TAG, "Unable to open Contacts DB cursor");
1201 showToast(R.string.contactSavedErrorToast);
1202 return null;
1203 }
1204 try {
1205 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001206 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001207 return null;
1208 }
1209 rawContactIds = new long[c.getCount()];
1210 for (int i = 0; i < rawContactIds.length; i++) {
1211 c.moveToPosition(i);
1212 long rawContactId = c.getLong(JoinContactQuery._ID);
1213 rawContactIds[i] = rawContactId;
1214 }
1215 } finally {
1216 c.close();
1217 }
1218 return rawContactIds;
1219 }
1220
Brian Attwelld3946ca2015-03-03 11:13:49 -08001221 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1222 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1223 }
1224
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001225 /**
1226 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1227 */
1228 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1229 long rawContactId1, long rawContactId2) {
1230 Builder builder =
1231 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1232 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1233 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1234 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1235 operations.add(builder.build());
1236 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001237
1238 /**
1239 * Shows a toast on the UI thread.
1240 */
1241 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001242 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001243
1244 @Override
1245 public void run() {
1246 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1247 }
1248 });
1249 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001250
1251 private void deliverCallback(final Intent callbackIntent) {
1252 mMainHandler.post(new Runnable() {
1253
1254 @Override
1255 public void run() {
1256 deliverCallbackOnUiThread(callbackIntent);
1257 }
1258 });
1259 }
1260
1261 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1262 // TODO: this assumes that if there are multiple instances of the same
1263 // activity registered, the last one registered is the one waiting for
1264 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001265 for (Listener listener : sListeners) {
1266 if (callbackIntent.getComponent().equals(
1267 ((Activity) listener).getIntent().getComponent())) {
1268 listener.onServiceCompleted(callbackIntent);
1269 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001270 }
1271 }
1272 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001273}