blob: 2680e837088a131b199cd8fb19fb9ee1e55806e1 [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);
379 }
380
381 final long rawContactId = getRawContactId(state, diff, results);
382 if (rawContactId == -1) {
383 throw new IllegalStateException("Could not determine RawContact ID after save");
384 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800385 // We don't have to check to see if the value is still -1. If we reach here,
386 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
387 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700388 if (isProfile) {
389 // Since the profile supports local raw contacts, which may have been completely
390 // removed if all information was removed, we need to do a special query to
391 // get the lookup URI for the profile contact (if it still exists).
392 Cursor c = resolver.query(Profile.CONTENT_URI,
393 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
394 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800395 if (c == null) {
396 continue;
397 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700398 try {
Erik162b7e32011-09-20 15:23:55 -0700399 if (c.moveToFirst()) {
400 final long contactId = c.getLong(0);
401 final String lookupKey = c.getString(1);
402 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
403 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700404 } finally {
405 c.close();
406 }
407 } else {
408 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
409 rawContactId);
410 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
411 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800412 if (lookupUri != null) {
413 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
414 }
Josh Garguse692e012012-01-18 14:53:11 -0800415
416 // We can change this back to false later, if we fail to save the contact photo.
417 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800418 break;
419
420 } catch (RemoteException e) {
421 // Something went wrong, bail without success
422 Log.e(TAG, "Problem persisting user edits", e);
423 break;
424
Jay Shrauner57fca182014-01-17 14:20:50 -0800425 } catch (IllegalArgumentException e) {
426 // This is thrown by applyBatch on malformed requests
427 Log.e(TAG, "Problem persisting user edits", e);
428 showToast(R.string.contactSavedErrorToast);
429 break;
430
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800431 } catch (OperationApplicationException e) {
432 // Version consistency failed, re-parent change and try again
433 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
434 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
435 boolean first = true;
436 final int count = state.size();
437 for (int i = 0; i < count; i++) {
438 Long rawContactId = state.getRawContactId(i);
439 if (rawContactId != null && rawContactId != -1) {
440 if (!first) {
441 sb.append(',');
442 }
443 sb.append(rawContactId);
444 first = false;
445 }
446 }
447 sb.append(")");
448
449 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800450 throw new IllegalStateException(
451 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800452 }
453
Maurice Chu851222a2012-06-21 11:43:08 -0700454 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700455 isProfile
456 ? RawContactsEntity.PROFILE_CONTENT_URI
457 : RawContactsEntity.CONTENT_URI,
458 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700459 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700460
461 // Update the new state to use profile URIs if appropriate.
462 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700463 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700464 delta.setProfileQueryUri();
465 }
466 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800467 }
468 }
469
Josh Garguse692e012012-01-18 14:53:11 -0800470 // Now save any updated photos. We do this at the end to ensure that
471 // the ContactProvider already knows about newly-created contacts.
472 if (updatedPhotos != null) {
473 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700474 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800475 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800476
477 // If the raw-contact ID is negative, we are saving a new raw-contact;
478 // replace the bogus ID with the new one that we actually saved the contact at.
479 if (rawContactId < 0) {
480 rawContactId = insertedRawContactId;
481 if (rawContactId == -1) {
482 throw new IllegalStateException(
483 "Could not determine RawContact ID for image insertion");
484 }
485 }
486
Yorke Lee637a38e2013-09-14 08:36:33 -0700487 if (!saveUpdatedPhoto(rawContactId, photoUri)) succeeded = false;
Josh Garguse692e012012-01-18 14:53:11 -0800488 }
489 }
490
Josh Garguse5d3f892012-04-11 11:56:15 -0700491 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
492 if (callbackIntent != null) {
493 if (succeeded) {
494 // Mark the intent to indicate that the save was successful (even if the lookup URI
495 // is now null). For local contacts or the local profile, it's possible that the
496 // save triggered removal of the contact, so no lookup URI would exist..
497 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
498 }
499 callbackIntent.setData(lookupUri);
500 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800501 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800502 }
503
Josh Garguse692e012012-01-18 14:53:11 -0800504 /**
505 * Save updated photo for the specified raw-contact.
506 * @return true for success, false for failure
507 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700508 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800509 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800510 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
511 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
512
Yorke Lee637a38e2013-09-14 08:36:33 -0700513 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
Josh Garguse692e012012-01-18 14:53:11 -0800514 }
515
Josh Gargusef15c8e2012-01-30 16:42:02 -0800516 /**
517 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
518 */
Maurice Chu851222a2012-06-21 11:43:08 -0700519 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800520 final ArrayList<ContentProviderOperation> diff,
521 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800522 long existingRawContactId = state.findRawContactId();
523 if (existingRawContactId != -1) {
524 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800525 }
526
Josh Gargusef15c8e2012-01-30 16:42:02 -0800527 return getInsertedRawContactId(diff, results);
528 }
529
530 /**
531 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
532 */
533 private long getInsertedRawContactId(
534 final ArrayList<ContentProviderOperation> diff,
535 final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800536 if (results == null) {
537 return -1;
538 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800539 final int diffSize = diff.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800540 final int numResults = results.length;
541 for (int i = 0; i < diffSize && i < numResults; i++) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800542 ContentProviderOperation operation = diff.get(i);
Brian Attwell13f94e12015-01-22 16:27:48 -0800543 if (operation.isInsert() && operation.getUri().getEncodedPath().contains(
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800544 RawContacts.CONTENT_URI.getEncodedPath())) {
545 return ContentUris.parseId(results[i].uri);
546 }
547 }
548 return -1;
549 }
550
551 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700552 * Creates an intent that can be sent to this service to create a new group as
553 * well as add new members at the same time.
554 *
555 * @param context of the application
556 * @param account in which the group should be created
557 * @param label is the name of the group (cannot be null)
558 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
559 * should be added to the group
560 * @param callbackActivity is the activity to send the callback intent to
561 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700562 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700563 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700564 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700565 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800566 Intent serviceIntent = new Intent(context, ContactSaveService.class);
567 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
568 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
569 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700570 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800571 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700572 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700573
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800574 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700575 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800576 Intent callbackIntent = new Intent(context, callbackActivity);
577 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700578 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800579
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700580 return serviceIntent;
581 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800582
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800583 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700584 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
585 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
586 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
587 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700588 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800589
590 ContentValues values = new ContentValues();
591 values.put(Groups.ACCOUNT_TYPE, accountType);
592 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700593 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800594 values.put(Groups.TITLE, label);
595
Katherine Kuan717e3432011-07-13 17:03:24 -0700596 final ContentResolver resolver = getContentResolver();
597
598 // Create the new group
599 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
600
601 // If there's no URI, then the insertion failed. Abort early because group members can't be
602 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800603 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700604 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800605 return;
606 }
607
Katherine Kuan717e3432011-07-13 17:03:24 -0700608 // Add new group members
609 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
610
611 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
612 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800613 values.clear();
614 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
615 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
616
617 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700618 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700619 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800620 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800621 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800622 }
623
624 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800625 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800626 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700627 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700628 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800629 Intent serviceIntent = new Intent(context, ContactSaveService.class);
630 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
631 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
632 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700633
634 // Callback intent will be invoked by the service once the group is renamed.
635 Intent callbackIntent = new Intent(context, callbackActivity);
636 callbackIntent.setAction(callbackAction);
637 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
638
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800639 return serviceIntent;
640 }
641
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800642 private void renameGroup(Intent intent) {
643 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
644 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
645
646 if (groupId == -1) {
647 Log.e(TAG, "Invalid arguments for renameGroup request");
648 return;
649 }
650
651 ContentValues values = new ContentValues();
652 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700653 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
654 getContentResolver().update(groupUri, values, null, null);
655
656 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
657 callbackIntent.setData(groupUri);
658 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800659 }
660
661 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800662 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800663 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800664 public static Intent createGroupDeletionIntent(Context context, long groupId) {
665 Intent serviceIntent = new Intent(context, ContactSaveService.class);
666 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800667 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800668 return serviceIntent;
669 }
670
671 private void deleteGroup(Intent intent) {
672 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
673 if (groupId == -1) {
674 Log.e(TAG, "Invalid arguments for deleteGroup request");
675 return;
676 }
677
678 getContentResolver().delete(
679 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
680 }
681
682 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700683 * Creates an intent that can be sent to this service to rename a group as
684 * well as add and remove members from the group.
685 *
686 * @param context of the application
687 * @param groupId of the group that should be modified
688 * @param newLabel is the updated name of the group (can be null if the name
689 * should not be updated)
690 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
691 * should be added to the group
692 * @param rawContactsToRemove is an array of raw contact IDs for contacts
693 * that should be removed from the group
694 * @param callbackActivity is the activity to send the callback intent to
695 * @param callbackAction is the intent action for the callback intent
696 */
697 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
698 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700699 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700700 Intent serviceIntent = new Intent(context, ContactSaveService.class);
701 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
702 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
703 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
704 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
705 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
706 rawContactsToRemove);
707
708 // Callback intent will be invoked by the service once the group is updated
709 Intent callbackIntent = new Intent(context, callbackActivity);
710 callbackIntent.setAction(callbackAction);
711 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
712
713 return serviceIntent;
714 }
715
716 private void updateGroup(Intent intent) {
717 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
718 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
719 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
720 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
721
722 if (groupId == -1) {
723 Log.e(TAG, "Invalid arguments for updateGroup request");
724 return;
725 }
726
727 final ContentResolver resolver = getContentResolver();
728 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
729
730 // Update group name if necessary
731 if (label != null) {
732 ContentValues values = new ContentValues();
733 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700734 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700735 }
736
Katherine Kuan717e3432011-07-13 17:03:24 -0700737 // Add and remove members if necessary
738 addMembersToGroup(resolver, rawContactsToAdd, groupId);
739 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
740
741 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
742 callbackIntent.setData(groupUri);
743 deliverCallback(callbackIntent);
744 }
745
Daniel Lehmann18958a22012-02-28 17:45:25 -0800746 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700747 long groupId) {
748 if (rawContactsToAdd == null) {
749 return;
750 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700751 for (long rawContactId : rawContactsToAdd) {
752 try {
753 final ArrayList<ContentProviderOperation> rawContactOperations =
754 new ArrayList<ContentProviderOperation>();
755
756 // Build an assert operation to ensure the contact is not already in the group
757 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
758 .newAssertQuery(Data.CONTENT_URI);
759 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
760 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
761 new String[] { String.valueOf(rawContactId),
762 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
763 assertBuilder.withExpectedCount(0);
764 rawContactOperations.add(assertBuilder.build());
765
766 // Build an insert operation to add the contact to the group
767 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
768 .newInsert(Data.CONTENT_URI);
769 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
770 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
771 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
772 rawContactOperations.add(insertBuilder.build());
773
774 if (DEBUG) {
775 for (ContentProviderOperation operation : rawContactOperations) {
776 Log.v(TAG, operation.toString());
777 }
778 }
779
780 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700781 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800782 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700783 }
784 } catch (RemoteException e) {
785 // Something went wrong, bail without success
786 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
787 String.valueOf(rawContactId), e);
788 } catch (OperationApplicationException e) {
789 // The assert could have failed because the contact is already in the group,
790 // just continue to the next contact
791 Log.w(TAG, "Assert failed in adding raw contact ID " +
792 String.valueOf(rawContactId) + ". Already exists in group " +
793 String.valueOf(groupId), e);
794 }
795 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700796 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700797
Daniel Lehmann18958a22012-02-28 17:45:25 -0800798 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700799 long groupId) {
800 if (rawContactsToRemove == null) {
801 return;
802 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700803 for (long rawContactId : rawContactsToRemove) {
804 // Apply the delete operation on the data row for the given raw contact's
805 // membership in the given group. If no contact matches the provided selection, then
806 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800807 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700808 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
809 new String[] { String.valueOf(rawContactId),
810 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
811 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700812 }
813
814 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800815 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800816 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800817 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
818 Intent serviceIntent = new Intent(context, ContactSaveService.class);
819 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
820 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
821 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
822
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800823 return serviceIntent;
824 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800825
826 private void setStarred(Intent intent) {
827 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
828 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
829 if (contactUri == null) {
830 Log.e(TAG, "Invalid arguments for setStarred request");
831 return;
832 }
833
834 final ContentValues values = new ContentValues(1);
835 values.put(Contacts.STARRED, value);
836 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700837
838 // Undemote the contact if necessary
839 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
840 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800841 if (c == null) {
842 return;
843 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700844 try {
845 if (c.moveToFirst()) {
846 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700847
848 // Don't bother undemoting if this contact is the user's profile.
849 if (id < Profile.MIN_ID) {
Brian Attwell2d88efa2014-12-17 21:49:56 -0800850 PinnedPositions.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700851 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700852 }
853 } finally {
854 c.close();
855 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800856 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800857
858 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700859 * Creates an intent that can be sent to this service to set the redirect to voicemail.
860 */
861 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
862 boolean value) {
863 Intent serviceIntent = new Intent(context, ContactSaveService.class);
864 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
865 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
866 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
867
868 return serviceIntent;
869 }
870
871 private void setSendToVoicemail(Intent intent) {
872 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
873 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
874 if (contactUri == null) {
875 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
876 return;
877 }
878
879 final ContentValues values = new ContentValues(1);
880 values.put(Contacts.SEND_TO_VOICEMAIL, value);
881 getContentResolver().update(contactUri, values, null, null);
882 }
883
884 /**
885 * Creates an intent that can be sent to this service to save the contact's ringtone.
886 */
887 public static Intent createSetRingtone(Context context, Uri contactUri,
888 String value) {
889 Intent serviceIntent = new Intent(context, ContactSaveService.class);
890 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
891 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
892 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
893
894 return serviceIntent;
895 }
896
897 private void setRingtone(Intent intent) {
898 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
899 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
900 if (contactUri == null) {
901 Log.e(TAG, "Invalid arguments for setRingtone");
902 return;
903 }
904 ContentValues values = new ContentValues(1);
905 values.put(Contacts.CUSTOM_RINGTONE, value);
906 getContentResolver().update(contactUri, values, null, null);
907 }
908
909 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800910 * Creates an intent that sets the selected data item as super primary (default)
911 */
912 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
913 Intent serviceIntent = new Intent(context, ContactSaveService.class);
914 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
915 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
916 return serviceIntent;
917 }
918
919 private void setSuperPrimary(Intent intent) {
920 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
921 if (dataId == -1) {
922 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
923 return;
924 }
925
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700926 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800927 }
928
929 /**
930 * Creates an intent that clears the primary flag of all data items that belong to the same
931 * raw_contact as the given data item. Will only clear, if the data item was primary before
932 * this call
933 */
934 public static Intent createClearPrimaryIntent(Context context, long dataId) {
935 Intent serviceIntent = new Intent(context, ContactSaveService.class);
936 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
937 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
938 return serviceIntent;
939 }
940
941 private void clearPrimary(Intent intent) {
942 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
943 if (dataId == -1) {
944 Log.e(TAG, "Invalid arguments for clearPrimary request");
945 return;
946 }
947
948 // Update the primary values in the data record.
949 ContentValues values = new ContentValues(1);
950 values.put(Data.IS_SUPER_PRIMARY, 0);
951 values.put(Data.IS_PRIMARY, 0);
952
953 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
954 values, null, null);
955 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800956
957 /**
958 * Creates an intent that can be sent to this service to delete a contact.
959 */
960 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
961 Intent serviceIntent = new Intent(context, ContactSaveService.class);
962 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
963 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
964 return serviceIntent;
965 }
966
Brian Attwelld2962a32015-03-02 14:48:50 -0800967 /**
968 * Creates an intent that can be sent to this service to delete multiple contacts.
969 */
970 public static Intent createDeleteMultipleContactsIntent(Context context,
971 long[] contactIds) {
972 Intent serviceIntent = new Intent(context, ContactSaveService.class);
973 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
974 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
975 return serviceIntent;
976 }
977
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800978 private void deleteContact(Intent intent) {
979 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
980 if (contactUri == null) {
981 Log.e(TAG, "Invalid arguments for deleteContact request");
982 return;
983 }
984
985 getContentResolver().delete(contactUri, null, null);
986 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800987
Brian Attwelld2962a32015-03-02 14:48:50 -0800988 private void deleteMultipleContacts(Intent intent) {
989 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
990 if (contactIds == null) {
991 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
992 return;
993 }
994 for (long contactId : contactIds) {
995 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
996 getContentResolver().delete(contactUri, null, null);
997 }
Brian Attwelle986c6b2015-03-05 19:47:30 -0800998 showToast(R.string.contacts_deleted_toast);
Brian Attwelld2962a32015-03-02 14:48:50 -0800999 }
1000
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001001 /**
1002 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001003 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001004 */
1005 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001006 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001007 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1008 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1009 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1010 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001011
1012 // Callback intent will be invoked by the service once the contacts are joined.
1013 Intent callbackIntent = new Intent(context, callbackActivity);
1014 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001015 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1016
1017 return serviceIntent;
1018 }
1019
Brian Attwelld3946ca2015-03-03 11:13:49 -08001020 /**
1021 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1022 * No special attention is paid to where the resulting contact's name is taken from.
1023 */
1024 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1025 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1026 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1027 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1028 return serviceIntent;
1029 }
1030
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001031
1032 private interface JoinContactQuery {
1033 String[] PROJECTION = {
1034 RawContacts._ID,
1035 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001036 RawContacts.DISPLAY_NAME_SOURCE,
1037 };
1038
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001039 int _ID = 0;
1040 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001041 int DISPLAY_NAME_SOURCE = 2;
1042 }
1043
1044 private interface ContactEntityQuery {
1045 String[] PROJECTION = {
1046 Contacts.Entity.DATA_ID,
1047 Contacts.Entity.CONTACT_ID,
1048 Contacts.Entity.IS_SUPER_PRIMARY,
1049 };
1050 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1051 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1052 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1053 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1054
1055 int DATA_ID = 0;
1056 int CONTACT_ID = 1;
1057 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001058 }
1059
Brian Attwelld3946ca2015-03-03 11:13:49 -08001060 private void joinSeveralContacts(Intent intent) {
1061 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001062
Brian Attwelld3946ca2015-03-03 11:13:49 -08001063 // Load raw contact IDs for all contacts involved.
1064 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1065 if (rawContactIds == null) {
1066 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001067 return;
1068 }
1069
Brian Attwelld3946ca2015-03-03 11:13:49 -08001070 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001071 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001072 final ArrayList<ContentProviderOperation> operations
1073 = new ArrayList<ContentProviderOperation>();
1074 for (int i = 0; i < rawContactIds.length; i++) {
1075 for (int j = 0; j < rawContactIds.length; j++) {
1076 if (i != j) {
1077 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1078 }
1079 }
1080 }
1081
1082 // Apply all aggregation exceptions as one batch
1083 try {
1084 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1085 showToast(R.string.contactsJoinedMessage);
1086 } catch (RemoteException | OperationApplicationException e) {
1087 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1088 showToast(R.string.contactSavedErrorToast);
1089 }
1090 }
1091
1092
1093 private void joinContacts(Intent intent) {
1094 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1095 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001096
1097 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001098 // in the join UIs.
1099 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1100 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001101 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001102 return;
1103 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001104
Brian Attwell548f5c62015-01-27 17:46:46 -08001105 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001106
1107 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001108 for (int i = 0; i < rawContactIds.length; i++) {
1109 for (int j = 0; j < rawContactIds.length; j++) {
1110 if (i != j) {
1111 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1112 }
1113 }
1114 }
1115
Brian Attwelld3946ca2015-03-03 11:13:49 -08001116 final ContentResolver resolver = getContentResolver();
1117
Brian Attwell548f5c62015-01-27 17:46:46 -08001118 // Use the name for contactId1 as the name for the newly aggregated contact.
1119 final Uri contactId1Uri = ContentUris.withAppendedId(
1120 Contacts.CONTENT_URI, contactId1);
1121 final Uri entityUri = Uri.withAppendedPath(
1122 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1123 Cursor c = resolver.query(entityUri,
1124 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1125 if (c == null) {
1126 Log.e(TAG, "Unable to open Contacts DB cursor");
1127 showToast(R.string.contactSavedErrorToast);
1128 return;
1129 }
1130 long dataIdToAddSuperPrimary = -1;
1131 try {
1132 if (c.moveToFirst()) {
1133 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1134 }
1135 } finally {
1136 c.close();
1137 }
1138
1139 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1140 // display name does not change as a result of the join.
1141 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001142 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001143 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1144 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1145 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001146 operations.add(builder.build());
1147 }
1148
1149 boolean success = false;
1150 // Apply all aggregation exceptions as one batch
1151 try {
1152 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001153 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001154 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001155 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001156 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001157 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001158 }
1159
1160 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1161 if (success) {
1162 Uri uri = RawContacts.getContactLookupUri(resolver,
1163 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1164 callbackIntent.setData(uri);
1165 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001166 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001167 }
1168
Brian Attwelld3946ca2015-03-03 11:13:49 -08001169 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1170 if (contactIds == null) {
1171 return null;
1172 }
1173
Brian Attwell548f5c62015-01-27 17:46:46 -08001174 final ContentResolver resolver = getContentResolver();
1175 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001176
1177 final StringBuilder queryBuilder = new StringBuilder();
1178 final String stringContactIds[] = new String[contactIds.length];
1179 for (int i = 0; i < contactIds.length; i++) {
1180 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1181 stringContactIds[i] = String.valueOf(contactIds[i]);
1182 if (contactIds[i] == -1) {
1183 return null;
1184 }
1185 if (i == contactIds.length -1) {
1186 break;
1187 }
1188 queryBuilder.append(" OR ");
1189 }
1190
Brian Attwell548f5c62015-01-27 17:46:46 -08001191 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1192 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001193 queryBuilder.toString(),
1194 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001195 if (c == null) {
1196 Log.e(TAG, "Unable to open Contacts DB cursor");
1197 showToast(R.string.contactSavedErrorToast);
1198 return null;
1199 }
1200 try {
1201 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001202 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001203 return null;
1204 }
1205 rawContactIds = new long[c.getCount()];
1206 for (int i = 0; i < rawContactIds.length; i++) {
1207 c.moveToPosition(i);
1208 long rawContactId = c.getLong(JoinContactQuery._ID);
1209 rawContactIds[i] = rawContactId;
1210 }
1211 } finally {
1212 c.close();
1213 }
1214 return rawContactIds;
1215 }
1216
Brian Attwelld3946ca2015-03-03 11:13:49 -08001217 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1218 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1219 }
1220
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001221 /**
1222 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1223 */
1224 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1225 long rawContactId1, long rawContactId2) {
1226 Builder builder =
1227 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1228 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1229 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1230 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1231 operations.add(builder.build());
1232 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001233
1234 /**
1235 * Shows a toast on the UI thread.
1236 */
1237 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001238 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001239
1240 @Override
1241 public void run() {
1242 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1243 }
1244 });
1245 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001246
1247 private void deliverCallback(final Intent callbackIntent) {
1248 mMainHandler.post(new Runnable() {
1249
1250 @Override
1251 public void run() {
1252 deliverCallbackOnUiThread(callbackIntent);
1253 }
1254 });
1255 }
1256
1257 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1258 // TODO: this assumes that if there are multiple instances of the same
1259 // activity registered, the last one registered is the one waiting for
1260 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001261 for (Listener listener : sListeners) {
1262 if (callbackIntent.getComponent().equals(
1263 ((Activity) listener).getIntent().getComponent())) {
1264 listener.onServiceCompleted(callbackIntent);
1265 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001266 }
1267 }
1268 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001269}