blob: 4c3671f7745ef409dcae3fca8cfc7c033d17ec2b [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
346 // Trim any empty fields, and RawContacts, before persisting
347 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700348 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800349
350 Uri lookupUri = null;
351
352 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800353 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800354
Josh Gargusef15c8e2012-01-30 16:42:02 -0800355 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
356 long insertedRawContactId = -1;
357
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800358 // Attempt to persist changes
359 int tries = 0;
360 while (tries++ < PERSIST_TRIES) {
361 try {
362 // Build operations and try applying
363 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700364 if (DEBUG) {
365 Log.v(TAG, "Content Provider Operations:");
366 for (ContentProviderOperation operation : diff) {
367 Log.v(TAG, operation.toString());
368 }
369 }
370
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800371 ContentProviderResult[] results = null;
372 if (!diff.isEmpty()) {
373 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
374 }
375
376 final long rawContactId = getRawContactId(state, diff, results);
377 if (rawContactId == -1) {
378 throw new IllegalStateException("Could not determine RawContact ID after save");
379 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800380 // We don't have to check to see if the value is still -1. If we reach here,
381 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
382 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700383 if (isProfile) {
384 // Since the profile supports local raw contacts, which may have been completely
385 // removed if all information was removed, we need to do a special query to
386 // get the lookup URI for the profile contact (if it still exists).
387 Cursor c = resolver.query(Profile.CONTENT_URI,
388 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
389 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800390 if (c == null) {
391 continue;
392 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700393 try {
Erik162b7e32011-09-20 15:23:55 -0700394 if (c.moveToFirst()) {
395 final long contactId = c.getLong(0);
396 final String lookupKey = c.getString(1);
397 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
398 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700399 } finally {
400 c.close();
401 }
402 } else {
403 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
404 rawContactId);
405 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
406 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800407 if (lookupUri != null) {
408 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
409 }
Josh Garguse692e012012-01-18 14:53:11 -0800410
411 // We can change this back to false later, if we fail to save the contact photo.
412 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800413 break;
414
415 } catch (RemoteException e) {
416 // Something went wrong, bail without success
417 Log.e(TAG, "Problem persisting user edits", e);
418 break;
419
Jay Shrauner57fca182014-01-17 14:20:50 -0800420 } catch (IllegalArgumentException e) {
421 // This is thrown by applyBatch on malformed requests
422 Log.e(TAG, "Problem persisting user edits", e);
423 showToast(R.string.contactSavedErrorToast);
424 break;
425
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800426 } catch (OperationApplicationException e) {
427 // Version consistency failed, re-parent change and try again
428 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
429 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
430 boolean first = true;
431 final int count = state.size();
432 for (int i = 0; i < count; i++) {
433 Long rawContactId = state.getRawContactId(i);
434 if (rawContactId != null && rawContactId != -1) {
435 if (!first) {
436 sb.append(',');
437 }
438 sb.append(rawContactId);
439 first = false;
440 }
441 }
442 sb.append(")");
443
444 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800445 throw new IllegalStateException(
446 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800447 }
448
Maurice Chu851222a2012-06-21 11:43:08 -0700449 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700450 isProfile
451 ? RawContactsEntity.PROFILE_CONTENT_URI
452 : RawContactsEntity.CONTENT_URI,
453 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700454 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700455
456 // Update the new state to use profile URIs if appropriate.
457 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700458 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700459 delta.setProfileQueryUri();
460 }
461 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800462 }
463 }
464
Josh Garguse692e012012-01-18 14:53:11 -0800465 // Now save any updated photos. We do this at the end to ensure that
466 // the ContactProvider already knows about newly-created contacts.
467 if (updatedPhotos != null) {
468 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700469 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800470 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800471
472 // If the raw-contact ID is negative, we are saving a new raw-contact;
473 // replace the bogus ID with the new one that we actually saved the contact at.
474 if (rawContactId < 0) {
475 rawContactId = insertedRawContactId;
476 if (rawContactId == -1) {
477 throw new IllegalStateException(
478 "Could not determine RawContact ID for image insertion");
479 }
480 }
481
Yorke Lee637a38e2013-09-14 08:36:33 -0700482 if (!saveUpdatedPhoto(rawContactId, photoUri)) succeeded = false;
Josh Garguse692e012012-01-18 14:53:11 -0800483 }
484 }
485
Josh Garguse5d3f892012-04-11 11:56:15 -0700486 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
487 if (callbackIntent != null) {
488 if (succeeded) {
489 // Mark the intent to indicate that the save was successful (even if the lookup URI
490 // is now null). For local contacts or the local profile, it's possible that the
491 // save triggered removal of the contact, so no lookup URI would exist..
492 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
493 }
494 callbackIntent.setData(lookupUri);
495 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800496 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800497 }
498
Josh Garguse692e012012-01-18 14:53:11 -0800499 /**
500 * Save updated photo for the specified raw-contact.
501 * @return true for success, false for failure
502 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700503 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800504 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800505 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
506 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
507
Yorke Lee637a38e2013-09-14 08:36:33 -0700508 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
Josh Garguse692e012012-01-18 14:53:11 -0800509 }
510
Josh Gargusef15c8e2012-01-30 16:42:02 -0800511 /**
512 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
513 */
Maurice Chu851222a2012-06-21 11:43:08 -0700514 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800515 final ArrayList<ContentProviderOperation> diff,
516 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800517 long existingRawContactId = state.findRawContactId();
518 if (existingRawContactId != -1) {
519 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800520 }
521
Josh Gargusef15c8e2012-01-30 16:42:02 -0800522 return getInsertedRawContactId(diff, results);
523 }
524
525 /**
526 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
527 */
528 private long getInsertedRawContactId(
529 final ArrayList<ContentProviderOperation> diff,
530 final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800531 if (results == null) {
532 return -1;
533 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800534 final int diffSize = diff.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800535 final int numResults = results.length;
536 for (int i = 0; i < diffSize && i < numResults; i++) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800537 ContentProviderOperation operation = diff.get(i);
Brian Attwell13f94e12015-01-22 16:27:48 -0800538 if (operation.isInsert() && operation.getUri().getEncodedPath().contains(
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800539 RawContacts.CONTENT_URI.getEncodedPath())) {
540 return ContentUris.parseId(results[i].uri);
541 }
542 }
543 return -1;
544 }
545
546 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700547 * Creates an intent that can be sent to this service to create a new group as
548 * well as add new members at the same time.
549 *
550 * @param context of the application
551 * @param account in which the group should be created
552 * @param label is the name of the group (cannot be null)
553 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
554 * should be added to the group
555 * @param callbackActivity is the activity to send the callback intent to
556 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700557 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700558 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700559 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700560 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800561 Intent serviceIntent = new Intent(context, ContactSaveService.class);
562 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
563 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
564 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700565 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800566 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700567 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700568
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800569 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700570 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800571 Intent callbackIntent = new Intent(context, callbackActivity);
572 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700573 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800574
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700575 return serviceIntent;
576 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800577
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800578 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700579 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
580 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
581 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
582 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700583 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800584
585 ContentValues values = new ContentValues();
586 values.put(Groups.ACCOUNT_TYPE, accountType);
587 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700588 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800589 values.put(Groups.TITLE, label);
590
Katherine Kuan717e3432011-07-13 17:03:24 -0700591 final ContentResolver resolver = getContentResolver();
592
593 // Create the new group
594 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
595
596 // If there's no URI, then the insertion failed. Abort early because group members can't be
597 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800598 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700599 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800600 return;
601 }
602
Katherine Kuan717e3432011-07-13 17:03:24 -0700603 // Add new group members
604 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
605
606 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
607 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800608 values.clear();
609 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
610 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
611
612 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700613 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700614 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800615 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800616 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800617 }
618
619 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800620 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800621 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700622 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700623 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800624 Intent serviceIntent = new Intent(context, ContactSaveService.class);
625 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
626 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
627 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700628
629 // Callback intent will be invoked by the service once the group is renamed.
630 Intent callbackIntent = new Intent(context, callbackActivity);
631 callbackIntent.setAction(callbackAction);
632 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
633
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800634 return serviceIntent;
635 }
636
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800637 private void renameGroup(Intent intent) {
638 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
639 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
640
641 if (groupId == -1) {
642 Log.e(TAG, "Invalid arguments for renameGroup request");
643 return;
644 }
645
646 ContentValues values = new ContentValues();
647 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700648 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
649 getContentResolver().update(groupUri, values, null, null);
650
651 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
652 callbackIntent.setData(groupUri);
653 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800654 }
655
656 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800657 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800658 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800659 public static Intent createGroupDeletionIntent(Context context, long groupId) {
660 Intent serviceIntent = new Intent(context, ContactSaveService.class);
661 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800662 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800663 return serviceIntent;
664 }
665
666 private void deleteGroup(Intent intent) {
667 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
668 if (groupId == -1) {
669 Log.e(TAG, "Invalid arguments for deleteGroup request");
670 return;
671 }
672
673 getContentResolver().delete(
674 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
675 }
676
677 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700678 * Creates an intent that can be sent to this service to rename a group as
679 * well as add and remove members from the group.
680 *
681 * @param context of the application
682 * @param groupId of the group that should be modified
683 * @param newLabel is the updated name of the group (can be null if the name
684 * should not be updated)
685 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
686 * should be added to the group
687 * @param rawContactsToRemove is an array of raw contact IDs for contacts
688 * that should be removed from the group
689 * @param callbackActivity is the activity to send the callback intent to
690 * @param callbackAction is the intent action for the callback intent
691 */
692 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
693 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700694 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700695 Intent serviceIntent = new Intent(context, ContactSaveService.class);
696 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
697 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
698 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
699 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
700 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
701 rawContactsToRemove);
702
703 // Callback intent will be invoked by the service once the group is updated
704 Intent callbackIntent = new Intent(context, callbackActivity);
705 callbackIntent.setAction(callbackAction);
706 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
707
708 return serviceIntent;
709 }
710
711 private void updateGroup(Intent intent) {
712 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
713 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
714 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
715 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
716
717 if (groupId == -1) {
718 Log.e(TAG, "Invalid arguments for updateGroup request");
719 return;
720 }
721
722 final ContentResolver resolver = getContentResolver();
723 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
724
725 // Update group name if necessary
726 if (label != null) {
727 ContentValues values = new ContentValues();
728 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700729 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700730 }
731
Katherine Kuan717e3432011-07-13 17:03:24 -0700732 // Add and remove members if necessary
733 addMembersToGroup(resolver, rawContactsToAdd, groupId);
734 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
735
736 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
737 callbackIntent.setData(groupUri);
738 deliverCallback(callbackIntent);
739 }
740
Daniel Lehmann18958a22012-02-28 17:45:25 -0800741 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700742 long groupId) {
743 if (rawContactsToAdd == null) {
744 return;
745 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700746 for (long rawContactId : rawContactsToAdd) {
747 try {
748 final ArrayList<ContentProviderOperation> rawContactOperations =
749 new ArrayList<ContentProviderOperation>();
750
751 // Build an assert operation to ensure the contact is not already in the group
752 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
753 .newAssertQuery(Data.CONTENT_URI);
754 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
755 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
756 new String[] { String.valueOf(rawContactId),
757 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
758 assertBuilder.withExpectedCount(0);
759 rawContactOperations.add(assertBuilder.build());
760
761 // Build an insert operation to add the contact to the group
762 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
763 .newInsert(Data.CONTENT_URI);
764 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
765 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
766 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
767 rawContactOperations.add(insertBuilder.build());
768
769 if (DEBUG) {
770 for (ContentProviderOperation operation : rawContactOperations) {
771 Log.v(TAG, operation.toString());
772 }
773 }
774
775 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700776 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800777 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700778 }
779 } catch (RemoteException e) {
780 // Something went wrong, bail without success
781 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
782 String.valueOf(rawContactId), e);
783 } catch (OperationApplicationException e) {
784 // The assert could have failed because the contact is already in the group,
785 // just continue to the next contact
786 Log.w(TAG, "Assert failed in adding raw contact ID " +
787 String.valueOf(rawContactId) + ". Already exists in group " +
788 String.valueOf(groupId), e);
789 }
790 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700791 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700792
Daniel Lehmann18958a22012-02-28 17:45:25 -0800793 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700794 long groupId) {
795 if (rawContactsToRemove == null) {
796 return;
797 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700798 for (long rawContactId : rawContactsToRemove) {
799 // Apply the delete operation on the data row for the given raw contact's
800 // membership in the given group. If no contact matches the provided selection, then
801 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800802 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700803 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
804 new String[] { String.valueOf(rawContactId),
805 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
806 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700807 }
808
809 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800810 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800811 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800812 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
813 Intent serviceIntent = new Intent(context, ContactSaveService.class);
814 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
815 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
816 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
817
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800818 return serviceIntent;
819 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800820
821 private void setStarred(Intent intent) {
822 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
823 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
824 if (contactUri == null) {
825 Log.e(TAG, "Invalid arguments for setStarred request");
826 return;
827 }
828
829 final ContentValues values = new ContentValues(1);
830 values.put(Contacts.STARRED, value);
831 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700832
833 // Undemote the contact if necessary
834 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
835 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800836 if (c == null) {
837 return;
838 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700839 try {
840 if (c.moveToFirst()) {
841 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700842
843 // Don't bother undemoting if this contact is the user's profile.
844 if (id < Profile.MIN_ID) {
Brian Attwell2d88efa2014-12-17 21:49:56 -0800845 PinnedPositions.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700846 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700847 }
848 } finally {
849 c.close();
850 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800851 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800852
853 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700854 * Creates an intent that can be sent to this service to set the redirect to voicemail.
855 */
856 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
857 boolean value) {
858 Intent serviceIntent = new Intent(context, ContactSaveService.class);
859 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
860 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
861 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
862
863 return serviceIntent;
864 }
865
866 private void setSendToVoicemail(Intent intent) {
867 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
868 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
869 if (contactUri == null) {
870 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
871 return;
872 }
873
874 final ContentValues values = new ContentValues(1);
875 values.put(Contacts.SEND_TO_VOICEMAIL, value);
876 getContentResolver().update(contactUri, values, null, null);
877 }
878
879 /**
880 * Creates an intent that can be sent to this service to save the contact's ringtone.
881 */
882 public static Intent createSetRingtone(Context context, Uri contactUri,
883 String value) {
884 Intent serviceIntent = new Intent(context, ContactSaveService.class);
885 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
886 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
887 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
888
889 return serviceIntent;
890 }
891
892 private void setRingtone(Intent intent) {
893 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
894 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
895 if (contactUri == null) {
896 Log.e(TAG, "Invalid arguments for setRingtone");
897 return;
898 }
899 ContentValues values = new ContentValues(1);
900 values.put(Contacts.CUSTOM_RINGTONE, value);
901 getContentResolver().update(contactUri, values, null, null);
902 }
903
904 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800905 * Creates an intent that sets the selected data item as super primary (default)
906 */
907 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
908 Intent serviceIntent = new Intent(context, ContactSaveService.class);
909 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
910 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
911 return serviceIntent;
912 }
913
914 private void setSuperPrimary(Intent intent) {
915 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
916 if (dataId == -1) {
917 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
918 return;
919 }
920
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700921 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800922 }
923
924 /**
925 * Creates an intent that clears the primary flag of all data items that belong to the same
926 * raw_contact as the given data item. Will only clear, if the data item was primary before
927 * this call
928 */
929 public static Intent createClearPrimaryIntent(Context context, long dataId) {
930 Intent serviceIntent = new Intent(context, ContactSaveService.class);
931 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
932 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
933 return serviceIntent;
934 }
935
936 private void clearPrimary(Intent intent) {
937 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
938 if (dataId == -1) {
939 Log.e(TAG, "Invalid arguments for clearPrimary request");
940 return;
941 }
942
943 // Update the primary values in the data record.
944 ContentValues values = new ContentValues(1);
945 values.put(Data.IS_SUPER_PRIMARY, 0);
946 values.put(Data.IS_PRIMARY, 0);
947
948 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
949 values, null, null);
950 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800951
952 /**
953 * Creates an intent that can be sent to this service to delete a contact.
954 */
955 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
956 Intent serviceIntent = new Intent(context, ContactSaveService.class);
957 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
958 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
959 return serviceIntent;
960 }
961
Brian Attwelld2962a32015-03-02 14:48:50 -0800962 /**
963 * Creates an intent that can be sent to this service to delete multiple contacts.
964 */
965 public static Intent createDeleteMultipleContactsIntent(Context context,
966 long[] contactIds) {
967 Intent serviceIntent = new Intent(context, ContactSaveService.class);
968 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
969 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
970 return serviceIntent;
971 }
972
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800973 private void deleteContact(Intent intent) {
974 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
975 if (contactUri == null) {
976 Log.e(TAG, "Invalid arguments for deleteContact request");
977 return;
978 }
979
980 getContentResolver().delete(contactUri, null, null);
981 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800982
Brian Attwelld2962a32015-03-02 14:48:50 -0800983 private void deleteMultipleContacts(Intent intent) {
984 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
985 if (contactIds == null) {
986 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
987 return;
988 }
989 for (long contactId : contactIds) {
990 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
991 getContentResolver().delete(contactUri, null, null);
992 }
Brian Attwelle986c6b2015-03-05 19:47:30 -0800993 showToast(R.string.contacts_deleted_toast);
Brian Attwelld2962a32015-03-02 14:48:50 -0800994 }
995
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800996 /**
997 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -0800998 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800999 */
1000 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001001 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001002 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1003 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1004 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1005 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001006
1007 // Callback intent will be invoked by the service once the contacts are joined.
1008 Intent callbackIntent = new Intent(context, callbackActivity);
1009 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001010 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1011
1012 return serviceIntent;
1013 }
1014
Brian Attwelld3946ca2015-03-03 11:13:49 -08001015 /**
1016 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1017 * No special attention is paid to where the resulting contact's name is taken from.
1018 */
1019 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1020 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1021 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1022 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1023 return serviceIntent;
1024 }
1025
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001026
1027 private interface JoinContactQuery {
1028 String[] PROJECTION = {
1029 RawContacts._ID,
1030 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001031 RawContacts.DISPLAY_NAME_SOURCE,
1032 };
1033
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001034 int _ID = 0;
1035 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001036 int DISPLAY_NAME_SOURCE = 2;
1037 }
1038
1039 private interface ContactEntityQuery {
1040 String[] PROJECTION = {
1041 Contacts.Entity.DATA_ID,
1042 Contacts.Entity.CONTACT_ID,
1043 Contacts.Entity.IS_SUPER_PRIMARY,
1044 };
1045 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1046 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1047 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1048 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1049
1050 int DATA_ID = 0;
1051 int CONTACT_ID = 1;
1052 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001053 }
1054
Brian Attwelld3946ca2015-03-03 11:13:49 -08001055 private void joinSeveralContacts(Intent intent) {
1056 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001057
Brian Attwelld3946ca2015-03-03 11:13:49 -08001058 // Load raw contact IDs for all contacts involved.
1059 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1060 if (rawContactIds == null) {
1061 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001062 return;
1063 }
1064
Brian Attwelld3946ca2015-03-03 11:13:49 -08001065 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001066 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001067 final ArrayList<ContentProviderOperation> operations
1068 = new ArrayList<ContentProviderOperation>();
1069 for (int i = 0; i < rawContactIds.length; i++) {
1070 for (int j = 0; j < rawContactIds.length; j++) {
1071 if (i != j) {
1072 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1073 }
1074 }
1075 }
1076
1077 // Apply all aggregation exceptions as one batch
1078 try {
1079 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1080 showToast(R.string.contactsJoinedMessage);
1081 } catch (RemoteException | OperationApplicationException e) {
1082 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1083 showToast(R.string.contactSavedErrorToast);
1084 }
1085 }
1086
1087
1088 private void joinContacts(Intent intent) {
1089 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1090 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001091
1092 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001093 // in the join UIs.
1094 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1095 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001096 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001097 return;
1098 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001099
Brian Attwell548f5c62015-01-27 17:46:46 -08001100 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001101
1102 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001103 for (int i = 0; i < rawContactIds.length; i++) {
1104 for (int j = 0; j < rawContactIds.length; j++) {
1105 if (i != j) {
1106 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1107 }
1108 }
1109 }
1110
Brian Attwelld3946ca2015-03-03 11:13:49 -08001111 final ContentResolver resolver = getContentResolver();
1112
Brian Attwell548f5c62015-01-27 17:46:46 -08001113 // Use the name for contactId1 as the name for the newly aggregated contact.
1114 final Uri contactId1Uri = ContentUris.withAppendedId(
1115 Contacts.CONTENT_URI, contactId1);
1116 final Uri entityUri = Uri.withAppendedPath(
1117 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1118 Cursor c = resolver.query(entityUri,
1119 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1120 if (c == null) {
1121 Log.e(TAG, "Unable to open Contacts DB cursor");
1122 showToast(R.string.contactSavedErrorToast);
1123 return;
1124 }
1125 long dataIdToAddSuperPrimary = -1;
1126 try {
1127 if (c.moveToFirst()) {
1128 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1129 }
1130 } finally {
1131 c.close();
1132 }
1133
1134 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1135 // display name does not change as a result of the join.
1136 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001137 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001138 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1139 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1140 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001141 operations.add(builder.build());
1142 }
1143
1144 boolean success = false;
1145 // Apply all aggregation exceptions as one batch
1146 try {
1147 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001148 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001149 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001150 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001151 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001152 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001153 }
1154
1155 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1156 if (success) {
1157 Uri uri = RawContacts.getContactLookupUri(resolver,
1158 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1159 callbackIntent.setData(uri);
1160 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001161 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001162 }
1163
Brian Attwelld3946ca2015-03-03 11:13:49 -08001164 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1165 if (contactIds == null) {
1166 return null;
1167 }
1168
Brian Attwell548f5c62015-01-27 17:46:46 -08001169 final ContentResolver resolver = getContentResolver();
1170 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001171
1172 final StringBuilder queryBuilder = new StringBuilder();
1173 final String stringContactIds[] = new String[contactIds.length];
1174 for (int i = 0; i < contactIds.length; i++) {
1175 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1176 stringContactIds[i] = String.valueOf(contactIds[i]);
1177 if (contactIds[i] == -1) {
1178 return null;
1179 }
1180 if (i == contactIds.length -1) {
1181 break;
1182 }
1183 queryBuilder.append(" OR ");
1184 }
1185
Brian Attwell548f5c62015-01-27 17:46:46 -08001186 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1187 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001188 queryBuilder.toString(),
1189 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001190 if (c == null) {
1191 Log.e(TAG, "Unable to open Contacts DB cursor");
1192 showToast(R.string.contactSavedErrorToast);
1193 return null;
1194 }
1195 try {
1196 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001197 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001198 return null;
1199 }
1200 rawContactIds = new long[c.getCount()];
1201 for (int i = 0; i < rawContactIds.length; i++) {
1202 c.moveToPosition(i);
1203 long rawContactId = c.getLong(JoinContactQuery._ID);
1204 rawContactIds[i] = rawContactId;
1205 }
1206 } finally {
1207 c.close();
1208 }
1209 return rawContactIds;
1210 }
1211
Brian Attwelld3946ca2015-03-03 11:13:49 -08001212 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1213 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1214 }
1215
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001216 /**
1217 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1218 */
1219 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1220 long rawContactId1, long rawContactId2) {
1221 Builder builder =
1222 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1223 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1224 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1225 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1226 operations.add(builder.build());
1227 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001228
1229 /**
1230 * Shows a toast on the UI thread.
1231 */
1232 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001233 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001234
1235 @Override
1236 public void run() {
1237 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1238 }
1239 });
1240 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001241
1242 private void deliverCallback(final Intent callbackIntent) {
1243 mMainHandler.post(new Runnable() {
1244
1245 @Override
1246 public void run() {
1247 deliverCallbackOnUiThread(callbackIntent);
1248 }
1249 });
1250 }
1251
1252 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1253 // TODO: this assumes that if there are multiple instances of the same
1254 // activity registered, the last one registered is the one waiting for
1255 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001256 for (Listener listener : sListeners) {
1257 if (callbackIntent.getComponent().equals(
1258 ((Activity) listener).getIntent().getComponent())) {
1259 listener.onServiceCompleted(callbackIntent);
1260 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001261 }
1262 }
1263 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001264}