blob: fd117a4479d62942ce0aa4c36e94bfc4903772be [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
Jay Shrauner615ed9c2015-07-29 11:27:56 -070019import static android.Manifest.permission.WRITE_CONTACTS;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080020import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070021import android.app.IntentService;
22import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080023import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070024import android.content.ContentProviderResult;
25import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080026import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070027import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080028import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070029import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080030import android.content.OperationApplicationException;
31import android.database.Cursor;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070032import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080033import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080034import android.os.Handler;
35import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080036import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080037import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070038import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080039import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080040import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080041import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080042import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070043import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080044import android.provider.ContactsContract.Groups;
Yorke Leee8e3fb82013-09-12 17:53:31 -070045import android.provider.ContactsContract.PinnedPositions;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070046import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070047import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070048import android.provider.ContactsContract.RawContactsEntity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070049import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080050import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070051
Chiao Chengd7ca03e2012-10-24 15:14:08 -070052import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080053import com.android.contacts.common.model.AccountTypeManager;
Yorke Leecd321f62013-10-28 15:20:15 -070054import com.android.contacts.common.model.RawContactDelta;
55import com.android.contacts.common.model.RawContactDeltaList;
56import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080057import com.android.contacts.common.model.account.AccountWithDataSet;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070058import com.android.contacts.common.util.PermissionsUtil;
Walter Jang3e764082015-05-22 09:38:42 -070059import com.android.contacts.editor.ContactEditorFragment;
Yorke Lee637a38e2013-09-14 08:36:33 -070060import com.android.contacts.util.ContactPhotoUtils;
61
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070062import com.google.common.collect.Lists;
63import com.google.common.collect.Sets;
64
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080065import java.util.ArrayList;
66import java.util.HashSet;
67import java.util.List;
68import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070069
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080070/**
71 * A service responsible for saving changes to the content provider.
72 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070073public class ContactSaveService extends IntentService {
74 private static final String TAG = "ContactSaveService";
75
Katherine Kuana007e442011-07-07 09:25:34 -070076 /** Set to true in order to view logs on content provider operations */
77 private static final boolean DEBUG = false;
78
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070079 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
80
81 public static final String EXTRA_ACCOUNT_NAME = "accountName";
82 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070083 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070084 public static final String EXTRA_CONTENT_VALUES = "contentValues";
85 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
86
Dmitri Plotnikova0114142011-02-15 13:53:21 -080087 public static final String ACTION_SAVE_CONTACT = "saveContact";
88 public static final String EXTRA_CONTACT_STATE = "state";
89 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070090 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070091 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080092 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070093
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080094 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080095 public static final String ACTION_RENAME_GROUP = "renameGroup";
96 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070097 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080098 public static final String EXTRA_GROUP_ID = "groupId";
99 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700100 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
101 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800102
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800103 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800104 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800105 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800106 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800107 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800108 public static final String EXTRA_STARRED_FLAG = "starred";
109
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800110 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
111 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
112 public static final String EXTRA_DATA_ID = "dataId";
113
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800114 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800115 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800116 public static final String EXTRA_CONTACT_ID1 = "contactId1";
117 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800118
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700119 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
120 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
121
122 public static final String ACTION_SET_RINGTONE = "setRingtone";
123 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
124
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700125 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
126 Data.MIMETYPE,
127 Data.IS_PRIMARY,
128 Data.DATA1,
129 Data.DATA2,
130 Data.DATA3,
131 Data.DATA4,
132 Data.DATA5,
133 Data.DATA6,
134 Data.DATA7,
135 Data.DATA8,
136 Data.DATA9,
137 Data.DATA10,
138 Data.DATA11,
139 Data.DATA12,
140 Data.DATA13,
141 Data.DATA14,
142 Data.DATA15
143 );
144
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800145 private static final int PERSIST_TRIES = 3;
146
Walter Jang0653de32015-07-24 12:12:40 -0700147 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
148
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800149 public interface Listener {
150 public void onServiceCompleted(Intent callbackIntent);
151 }
152
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100153 private static final CopyOnWriteArrayList<Listener> sListeners =
154 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800155
156 private Handler mMainHandler;
157
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700158 public ContactSaveService() {
159 super(TAG);
160 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800161 mMainHandler = new Handler(Looper.getMainLooper());
162 }
163
164 public static void registerListener(Listener listener) {
165 if (!(listener instanceof Activity)) {
166 throw new ClassCastException("Only activities can be registered to"
167 + " receive callback from " + ContactSaveService.class.getName());
168 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100169 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800170 }
171
172 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100173 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700174 }
175
176 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800177 public Object getSystemService(String name) {
178 Object service = super.getSystemService(name);
179 if (service != null) {
180 return service;
181 }
182
183 return getApplicationContext().getSystemService(name);
184 }
185
186 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700187 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800188 if (intent == null) {
189 Log.d(TAG, "onHandleIntent: could not handle null intent");
190 return;
191 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700192 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
193 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
194 // TODO: add more specific error string such as "Turn on Contacts
195 // permission to update your contacts"
196 showToast(R.string.contactSavedErrorToast);
197 return;
198 }
199
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700200 // Call an appropriate method. If we're sure it affects how incoming phone calls are
201 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700202 String action = intent.getAction();
203 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
204 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800205 } else if (ACTION_SAVE_CONTACT.equals(action)) {
206 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800207 } else if (ACTION_CREATE_GROUP.equals(action)) {
208 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800209 } else if (ACTION_RENAME_GROUP.equals(action)) {
210 renameGroup(intent);
211 } else if (ACTION_DELETE_GROUP.equals(action)) {
212 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700213 } else if (ACTION_UPDATE_GROUP.equals(action)) {
214 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800215 } else if (ACTION_SET_STARRED.equals(action)) {
216 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800217 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
218 setSuperPrimary(intent);
219 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
220 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800221 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
222 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800223 } else if (ACTION_DELETE_CONTACT.equals(action)) {
224 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800225 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
226 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800227 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
228 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700229 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
230 setSendToVoicemail(intent);
231 } else if (ACTION_SET_RINGTONE.equals(action)) {
232 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700233 }
234 }
235
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800236 /**
237 * Creates an intent that can be sent to this service to create a new raw contact
238 * using data presented as a set of ContentValues.
239 */
240 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700241 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700242 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800243 Intent serviceIntent = new Intent(
244 context, ContactSaveService.class);
245 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
246 if (account != null) {
247 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
248 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700249 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800250 }
251 serviceIntent.putParcelableArrayListExtra(
252 ContactSaveService.EXTRA_CONTENT_VALUES, values);
253
254 // Callback intent will be invoked by the service once the new contact is
255 // created. The service will put the URI of the new contact as "data" on
256 // the callback intent.
257 Intent callbackIntent = new Intent(context, callbackActivity);
258 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800259 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
260 return serviceIntent;
261 }
262
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700263 private void createRawContact(Intent intent) {
264 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
265 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700266 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700267 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
268 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
269
270 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
271 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
272 .withValue(RawContacts.ACCOUNT_NAME, accountName)
273 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700274 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700275 .build());
276
277 int size = valueList.size();
278 for (int i = 0; i < size; i++) {
279 ContentValues values = valueList.get(i);
280 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
281 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
282 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
283 .withValues(values)
284 .build());
285 }
286
287 ContentResolver resolver = getContentResolver();
288 ContentProviderResult[] results;
289 try {
290 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
291 } catch (Exception e) {
292 throw new RuntimeException("Failed to store new contact", e);
293 }
294
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700295 Uri rawContactUri = results[0].uri;
296 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
297
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800298 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700299 }
300
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700301 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800302 * Creates an intent that can be sent to this service to create a new raw contact
303 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800304 * This variant is more convenient to use when there is only one photo that can
305 * possibly be updated, as in the Contact Details screen.
306 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
307 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800308 */
Maurice Chu851222a2012-06-21 11:43:08 -0700309 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700310 String saveModeExtraKey, int saveMode, boolean isProfile,
311 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700312 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800313 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700314 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800315 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jang28a27272015-09-19 16:06:08 -0700316 callbackActivity, callbackAction, bundle);
Josh Garguse692e012012-01-18 14:53:11 -0800317 }
318
319 /**
320 * Creates an intent that can be sent to this service to create a new raw contact
321 * using data presented as a set of ContentValues.
322 * This variant is used when multiple contacts' photos may be updated, as in the
323 * Contact Editor.
324 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
325 */
Maurice Chu851222a2012-06-21 11:43:08 -0700326 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700327 String saveModeExtraKey, int saveMode, boolean isProfile,
328 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jang28a27272015-09-19 16:06:08 -0700329 Bundle updatedPhotos) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800330 Intent serviceIntent = new Intent(
331 context, ContactSaveService.class);
332 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
333 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700334 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800335 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
336
Josh Garguse692e012012-01-18 14:53:11 -0800337 if (updatedPhotos != null) {
338 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
339 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800340
Josh Garguse5d3f892012-04-11 11:56:15 -0700341 if (callbackActivity != null) {
342 // Callback intent will be invoked by the service once the contact is
343 // saved. The service will put the URI of the new contact as "data" on
344 // the callback intent.
345 Intent callbackIntent = new Intent(context, callbackActivity);
346 callbackIntent.putExtra(saveModeExtraKey, saveMode);
347 callbackIntent.setAction(callbackAction);
348 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
349 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800350 return serviceIntent;
351 }
352
353 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700354 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700355 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800356 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800357
Jay Shrauner08099782015-03-25 14:17:11 -0700358 if (state == null) {
359 Log.e(TAG, "Invalid arguments for saveContact request");
360 return;
361 }
362
benny.lin3a4e7a22014-01-08 10:58:08 +0800363 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800364 // Trim any empty fields, and RawContacts, before persisting
365 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700366 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800367
368 Uri lookupUri = null;
369
370 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800371 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800372
Josh Gargusef15c8e2012-01-30 16:42:02 -0800373 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
374 long insertedRawContactId = -1;
375
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800376 // Attempt to persist changes
377 int tries = 0;
378 while (tries++ < PERSIST_TRIES) {
379 try {
380 // Build operations and try applying
381 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700382 if (DEBUG) {
383 Log.v(TAG, "Content Provider Operations:");
384 for (ContentProviderOperation operation : diff) {
385 Log.v(TAG, operation.toString());
386 }
387 }
388
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800389 ContentProviderResult[] results = null;
390 if (!diff.isEmpty()) {
391 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
Jay Shrauner511561d2015-04-02 10:35:33 -0700392 if (results == null) {
393 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
394 // Retry save
395 continue;
396 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800397 }
398
399 final long rawContactId = getRawContactId(state, diff, results);
400 if (rawContactId == -1) {
401 throw new IllegalStateException("Could not determine RawContact ID after save");
402 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800403 // We don't have to check to see if the value is still -1. If we reach here,
404 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
405 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700406 if (isProfile) {
407 // Since the profile supports local raw contacts, which may have been completely
408 // removed if all information was removed, we need to do a special query to
409 // get the lookup URI for the profile contact (if it still exists).
410 Cursor c = resolver.query(Profile.CONTENT_URI,
411 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
412 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800413 if (c == null) {
414 continue;
415 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700416 try {
Erik162b7e32011-09-20 15:23:55 -0700417 if (c.moveToFirst()) {
418 final long contactId = c.getLong(0);
419 final String lookupKey = c.getString(1);
420 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
421 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700422 } finally {
423 c.close();
424 }
425 } else {
426 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
427 rawContactId);
428 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
429 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800430 if (lookupUri != null) {
431 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
432 }
Josh Garguse692e012012-01-18 14:53:11 -0800433
434 // We can change this back to false later, if we fail to save the contact photo.
435 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800436 break;
437
438 } catch (RemoteException e) {
439 // Something went wrong, bail without success
440 Log.e(TAG, "Problem persisting user edits", e);
441 break;
442
Jay Shrauner57fca182014-01-17 14:20:50 -0800443 } catch (IllegalArgumentException e) {
444 // This is thrown by applyBatch on malformed requests
445 Log.e(TAG, "Problem persisting user edits", e);
446 showToast(R.string.contactSavedErrorToast);
447 break;
448
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800449 } catch (OperationApplicationException e) {
450 // Version consistency failed, re-parent change and try again
451 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
452 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
453 boolean first = true;
454 final int count = state.size();
455 for (int i = 0; i < count; i++) {
456 Long rawContactId = state.getRawContactId(i);
457 if (rawContactId != null && rawContactId != -1) {
458 if (!first) {
459 sb.append(',');
460 }
461 sb.append(rawContactId);
462 first = false;
463 }
464 }
465 sb.append(")");
466
467 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800468 throw new IllegalStateException(
469 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800470 }
471
Maurice Chu851222a2012-06-21 11:43:08 -0700472 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700473 isProfile
474 ? RawContactsEntity.PROFILE_CONTENT_URI
475 : RawContactsEntity.CONTENT_URI,
476 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700477 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700478
479 // Update the new state to use profile URIs if appropriate.
480 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700481 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700482 delta.setProfileQueryUri();
483 }
484 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800485 }
486 }
487
Josh Garguse692e012012-01-18 14:53:11 -0800488 // Now save any updated photos. We do this at the end to ensure that
489 // the ContactProvider already knows about newly-created contacts.
490 if (updatedPhotos != null) {
491 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700492 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800493 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800494
495 // If the raw-contact ID is negative, we are saving a new raw-contact;
496 // replace the bogus ID with the new one that we actually saved the contact at.
497 if (rawContactId < 0) {
498 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800499 }
500
Jay Shrauner511561d2015-04-02 10:35:33 -0700501 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700502 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700503 succeeded = false;
504 }
Josh Garguse692e012012-01-18 14:53:11 -0800505 }
506 }
507
Josh Garguse5d3f892012-04-11 11:56:15 -0700508 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
509 if (callbackIntent != null) {
510 if (succeeded) {
511 // Mark the intent to indicate that the save was successful (even if the lookup URI
512 // is now null). For local contacts or the local profile, it's possible that the
513 // save triggered removal of the contact, so no lookup URI would exist..
514 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
515 }
516 callbackIntent.setData(lookupUri);
517 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800518 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800519 }
520
Josh Garguse692e012012-01-18 14:53:11 -0800521 /**
522 * Save updated photo for the specified raw-contact.
523 * @return true for success, false for failure
524 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800525 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800526 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800527 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
528 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
529
benny.lin3a4e7a22014-01-08 10:58:08 +0800530 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800531 }
532
Josh Gargusef15c8e2012-01-30 16:42:02 -0800533 /**
534 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
535 */
Maurice Chu851222a2012-06-21 11:43:08 -0700536 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800537 final ArrayList<ContentProviderOperation> diff,
538 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800539 long existingRawContactId = state.findRawContactId();
540 if (existingRawContactId != -1) {
541 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800542 }
543
Josh Gargusef15c8e2012-01-30 16:42:02 -0800544 return getInsertedRawContactId(diff, results);
545 }
546
547 /**
548 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
549 */
550 private long getInsertedRawContactId(
551 final ArrayList<ContentProviderOperation> diff,
552 final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800553 if (results == null) {
554 return -1;
555 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800556 final int diffSize = diff.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800557 final int numResults = results.length;
558 for (int i = 0; i < diffSize && i < numResults; i++) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800559 ContentProviderOperation operation = diff.get(i);
Brian Attwell13f94e12015-01-22 16:27:48 -0800560 if (operation.isInsert() && operation.getUri().getEncodedPath().contains(
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800561 RawContacts.CONTENT_URI.getEncodedPath())) {
562 return ContentUris.parseId(results[i].uri);
563 }
564 }
565 return -1;
566 }
567
568 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700569 * Creates an intent that can be sent to this service to create a new group as
570 * well as add new members at the same time.
571 *
572 * @param context of the application
573 * @param account in which the group should be created
574 * @param label is the name of the group (cannot be null)
575 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
576 * should be added to the group
577 * @param callbackActivity is the activity to send the callback intent to
578 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700579 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700580 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700581 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700582 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800583 Intent serviceIntent = new Intent(context, ContactSaveService.class);
584 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
585 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
586 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700587 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800588 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700589 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700590
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800591 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700592 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800593 Intent callbackIntent = new Intent(context, callbackActivity);
594 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700595 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800596
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700597 return serviceIntent;
598 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800599
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800600 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700601 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
602 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
603 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
604 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700605 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800606
607 ContentValues values = new ContentValues();
608 values.put(Groups.ACCOUNT_TYPE, accountType);
609 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700610 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800611 values.put(Groups.TITLE, label);
612
Katherine Kuan717e3432011-07-13 17:03:24 -0700613 final ContentResolver resolver = getContentResolver();
614
615 // Create the new group
616 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
617
618 // If there's no URI, then the insertion failed. Abort early because group members can't be
619 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800620 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700621 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800622 return;
623 }
624
Katherine Kuan717e3432011-07-13 17:03:24 -0700625 // Add new group members
626 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
627
628 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
629 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800630 values.clear();
631 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
632 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
633
634 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700635 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700636 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800637 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800638 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800639 }
640
641 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800642 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800643 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700644 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700645 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800646 Intent serviceIntent = new Intent(context, ContactSaveService.class);
647 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
648 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
649 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700650
651 // Callback intent will be invoked by the service once the group is renamed.
652 Intent callbackIntent = new Intent(context, callbackActivity);
653 callbackIntent.setAction(callbackAction);
654 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
655
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800656 return serviceIntent;
657 }
658
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800659 private void renameGroup(Intent intent) {
660 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
661 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
662
663 if (groupId == -1) {
664 Log.e(TAG, "Invalid arguments for renameGroup request");
665 return;
666 }
667
668 ContentValues values = new ContentValues();
669 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700670 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
671 getContentResolver().update(groupUri, values, null, null);
672
673 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
674 callbackIntent.setData(groupUri);
675 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800676 }
677
678 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800679 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800680 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800681 public static Intent createGroupDeletionIntent(Context context, long groupId) {
682 Intent serviceIntent = new Intent(context, ContactSaveService.class);
683 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800684 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800685 return serviceIntent;
686 }
687
688 private void deleteGroup(Intent intent) {
689 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
690 if (groupId == -1) {
691 Log.e(TAG, "Invalid arguments for deleteGroup request");
692 return;
693 }
694
695 getContentResolver().delete(
696 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
697 }
698
699 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700700 * Creates an intent that can be sent to this service to rename a group as
701 * well as add and remove members from the group.
702 *
703 * @param context of the application
704 * @param groupId of the group that should be modified
705 * @param newLabel is the updated name of the group (can be null if the name
706 * should not be updated)
707 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
708 * should be added to the group
709 * @param rawContactsToRemove is an array of raw contact IDs for contacts
710 * that should be removed from the group
711 * @param callbackActivity is the activity to send the callback intent to
712 * @param callbackAction is the intent action for the callback intent
713 */
714 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
715 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700716 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700717 Intent serviceIntent = new Intent(context, ContactSaveService.class);
718 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
719 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
720 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
721 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
722 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
723 rawContactsToRemove);
724
725 // Callback intent will be invoked by the service once the group is updated
726 Intent callbackIntent = new Intent(context, callbackActivity);
727 callbackIntent.setAction(callbackAction);
728 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
729
730 return serviceIntent;
731 }
732
733 private void updateGroup(Intent intent) {
734 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
735 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
736 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
737 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
738
739 if (groupId == -1) {
740 Log.e(TAG, "Invalid arguments for updateGroup request");
741 return;
742 }
743
744 final ContentResolver resolver = getContentResolver();
745 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
746
747 // Update group name if necessary
748 if (label != null) {
749 ContentValues values = new ContentValues();
750 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700751 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700752 }
753
Katherine Kuan717e3432011-07-13 17:03:24 -0700754 // Add and remove members if necessary
755 addMembersToGroup(resolver, rawContactsToAdd, groupId);
756 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
757
758 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
759 callbackIntent.setData(groupUri);
760 deliverCallback(callbackIntent);
761 }
762
Daniel Lehmann18958a22012-02-28 17:45:25 -0800763 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700764 long groupId) {
765 if (rawContactsToAdd == null) {
766 return;
767 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700768 for (long rawContactId : rawContactsToAdd) {
769 try {
770 final ArrayList<ContentProviderOperation> rawContactOperations =
771 new ArrayList<ContentProviderOperation>();
772
773 // Build an assert operation to ensure the contact is not already in the group
774 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
775 .newAssertQuery(Data.CONTENT_URI);
776 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
777 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
778 new String[] { String.valueOf(rawContactId),
779 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
780 assertBuilder.withExpectedCount(0);
781 rawContactOperations.add(assertBuilder.build());
782
783 // Build an insert operation to add the contact to the group
784 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
785 .newInsert(Data.CONTENT_URI);
786 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
787 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
788 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
789 rawContactOperations.add(insertBuilder.build());
790
791 if (DEBUG) {
792 for (ContentProviderOperation operation : rawContactOperations) {
793 Log.v(TAG, operation.toString());
794 }
795 }
796
797 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700798 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800799 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700800 }
801 } catch (RemoteException e) {
802 // Something went wrong, bail without success
803 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
804 String.valueOf(rawContactId), e);
805 } catch (OperationApplicationException e) {
806 // The assert could have failed because the contact is already in the group,
807 // just continue to the next contact
808 Log.w(TAG, "Assert failed in adding raw contact ID " +
809 String.valueOf(rawContactId) + ". Already exists in group " +
810 String.valueOf(groupId), e);
811 }
812 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700813 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700814
Daniel Lehmann18958a22012-02-28 17:45:25 -0800815 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700816 long groupId) {
817 if (rawContactsToRemove == null) {
818 return;
819 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700820 for (long rawContactId : rawContactsToRemove) {
821 // Apply the delete operation on the data row for the given raw contact's
822 // membership in the given group. If no contact matches the provided selection, then
823 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800824 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700825 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
826 new String[] { String.valueOf(rawContactId),
827 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
828 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700829 }
830
831 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800832 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800833 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800834 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
835 Intent serviceIntent = new Intent(context, ContactSaveService.class);
836 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
837 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
838 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
839
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800840 return serviceIntent;
841 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800842
843 private void setStarred(Intent intent) {
844 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
845 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
846 if (contactUri == null) {
847 Log.e(TAG, "Invalid arguments for setStarred request");
848 return;
849 }
850
851 final ContentValues values = new ContentValues(1);
852 values.put(Contacts.STARRED, value);
853 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700854
855 // Undemote the contact if necessary
856 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
857 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800858 if (c == null) {
859 return;
860 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700861 try {
862 if (c.moveToFirst()) {
863 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700864
865 // Don't bother undemoting if this contact is the user's profile.
866 if (id < Profile.MIN_ID) {
Brian Attwell2d88efa2014-12-17 21:49:56 -0800867 PinnedPositions.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700868 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700869 }
870 } finally {
871 c.close();
872 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800873 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800874
875 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700876 * Creates an intent that can be sent to this service to set the redirect to voicemail.
877 */
878 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
879 boolean value) {
880 Intent serviceIntent = new Intent(context, ContactSaveService.class);
881 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
882 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
883 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
884
885 return serviceIntent;
886 }
887
888 private void setSendToVoicemail(Intent intent) {
889 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
890 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
891 if (contactUri == null) {
892 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
893 return;
894 }
895
896 final ContentValues values = new ContentValues(1);
897 values.put(Contacts.SEND_TO_VOICEMAIL, value);
898 getContentResolver().update(contactUri, values, null, null);
899 }
900
901 /**
902 * Creates an intent that can be sent to this service to save the contact's ringtone.
903 */
904 public static Intent createSetRingtone(Context context, Uri contactUri,
905 String value) {
906 Intent serviceIntent = new Intent(context, ContactSaveService.class);
907 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
908 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
909 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
910
911 return serviceIntent;
912 }
913
914 private void setRingtone(Intent intent) {
915 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
916 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
917 if (contactUri == null) {
918 Log.e(TAG, "Invalid arguments for setRingtone");
919 return;
920 }
921 ContentValues values = new ContentValues(1);
922 values.put(Contacts.CUSTOM_RINGTONE, value);
923 getContentResolver().update(contactUri, values, null, null);
924 }
925
926 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800927 * Creates an intent that sets the selected data item as super primary (default)
928 */
929 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
930 Intent serviceIntent = new Intent(context, ContactSaveService.class);
931 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
932 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
933 return serviceIntent;
934 }
935
936 private void setSuperPrimary(Intent intent) {
937 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
938 if (dataId == -1) {
939 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
940 return;
941 }
942
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700943 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800944 }
945
946 /**
947 * Creates an intent that clears the primary flag of all data items that belong to the same
948 * raw_contact as the given data item. Will only clear, if the data item was primary before
949 * this call
950 */
951 public static Intent createClearPrimaryIntent(Context context, long dataId) {
952 Intent serviceIntent = new Intent(context, ContactSaveService.class);
953 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
954 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
955 return serviceIntent;
956 }
957
958 private void clearPrimary(Intent intent) {
959 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
960 if (dataId == -1) {
961 Log.e(TAG, "Invalid arguments for clearPrimary request");
962 return;
963 }
964
965 // Update the primary values in the data record.
966 ContentValues values = new ContentValues(1);
967 values.put(Data.IS_SUPER_PRIMARY, 0);
968 values.put(Data.IS_PRIMARY, 0);
969
970 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
971 values, null, null);
972 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800973
974 /**
975 * Creates an intent that can be sent to this service to delete a contact.
976 */
977 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
978 Intent serviceIntent = new Intent(context, ContactSaveService.class);
979 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
980 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
981 return serviceIntent;
982 }
983
Brian Attwelld2962a32015-03-02 14:48:50 -0800984 /**
985 * Creates an intent that can be sent to this service to delete multiple contacts.
986 */
987 public static Intent createDeleteMultipleContactsIntent(Context context,
988 long[] contactIds) {
989 Intent serviceIntent = new Intent(context, ContactSaveService.class);
990 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
991 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
992 return serviceIntent;
993 }
994
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800995 private void deleteContact(Intent intent) {
996 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
997 if (contactUri == null) {
998 Log.e(TAG, "Invalid arguments for deleteContact request");
999 return;
1000 }
1001
1002 getContentResolver().delete(contactUri, null, null);
1003 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001004
Brian Attwelld2962a32015-03-02 14:48:50 -08001005 private void deleteMultipleContacts(Intent intent) {
1006 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1007 if (contactIds == null) {
1008 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1009 return;
1010 }
1011 for (long contactId : contactIds) {
1012 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1013 getContentResolver().delete(contactUri, null, null);
1014 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001015 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1016 .contacts_deleted_toast, contactIds.length);
1017 mMainHandler.post(new Runnable() {
1018 @Override
1019 public void run() {
1020 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1021 .show();
1022 }
1023 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001024 }
1025
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001026 /**
1027 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001028 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001029 */
1030 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001031 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001032 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1033 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1034 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1035 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001036
1037 // Callback intent will be invoked by the service once the contacts are joined.
1038 Intent callbackIntent = new Intent(context, callbackActivity);
1039 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001040 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1041
1042 return serviceIntent;
1043 }
1044
Brian Attwelld3946ca2015-03-03 11:13:49 -08001045 /**
1046 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1047 * No special attention is paid to where the resulting contact's name is taken from.
1048 */
1049 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1050 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1051 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1052 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1053 return serviceIntent;
1054 }
1055
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001056
1057 private interface JoinContactQuery {
1058 String[] PROJECTION = {
1059 RawContacts._ID,
1060 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001061 RawContacts.DISPLAY_NAME_SOURCE,
1062 };
1063
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001064 int _ID = 0;
1065 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001066 int DISPLAY_NAME_SOURCE = 2;
1067 }
1068
1069 private interface ContactEntityQuery {
1070 String[] PROJECTION = {
1071 Contacts.Entity.DATA_ID,
1072 Contacts.Entity.CONTACT_ID,
1073 Contacts.Entity.IS_SUPER_PRIMARY,
1074 };
1075 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1076 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1077 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1078 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1079
1080 int DATA_ID = 0;
1081 int CONTACT_ID = 1;
1082 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001083 }
1084
Brian Attwelld3946ca2015-03-03 11:13:49 -08001085 private void joinSeveralContacts(Intent intent) {
1086 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001087
Brian Attwelld3946ca2015-03-03 11:13:49 -08001088 // Load raw contact IDs for all contacts involved.
1089 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1090 if (rawContactIds == null) {
1091 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001092 return;
1093 }
1094
Brian Attwelld3946ca2015-03-03 11:13:49 -08001095 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001096 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001097 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1098 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1099 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001100 for (int i = 0; i < rawContactIds.length; i++) {
1101 for (int j = 0; j < rawContactIds.length; j++) {
1102 if (i != j) {
1103 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1104 }
Walter Jang0653de32015-07-24 12:12:40 -07001105 // Before we get to 500 we need to flush the operations list
1106 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1107 if (!applyJoinOperations(resolver, operations)) {
1108 return;
1109 }
1110 operations.clear();
1111 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001112 }
1113 }
Walter Jang0653de32015-07-24 12:12:40 -07001114 if (operations.size() > 0 && !applyJoinOperations(resolver, operations)) {
1115 return;
1116 }
1117 showToast(R.string.contactsJoinedMessage);
1118 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001119
Walter Jang0653de32015-07-24 12:12:40 -07001120 /** Returns true if the batch was successfully applied and false otherwise. */
1121 private boolean applyJoinOperations(ContentResolver resolver,
1122 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001123 try {
1124 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001125 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001126 } catch (RemoteException | OperationApplicationException e) {
1127 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1128 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001129 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001130 }
1131 }
1132
1133
1134 private void joinContacts(Intent intent) {
1135 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1136 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001137
1138 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001139 // in the join UIs.
1140 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1141 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001142 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001143 return;
1144 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001145
Brian Attwell548f5c62015-01-27 17:46:46 -08001146 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001147
1148 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001149 for (int i = 0; i < rawContactIds.length; i++) {
1150 for (int j = 0; j < rawContactIds.length; j++) {
1151 if (i != j) {
1152 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1153 }
1154 }
1155 }
1156
Brian Attwelld3946ca2015-03-03 11:13:49 -08001157 final ContentResolver resolver = getContentResolver();
1158
Brian Attwell548f5c62015-01-27 17:46:46 -08001159 // Use the name for contactId1 as the name for the newly aggregated contact.
1160 final Uri contactId1Uri = ContentUris.withAppendedId(
1161 Contacts.CONTENT_URI, contactId1);
1162 final Uri entityUri = Uri.withAppendedPath(
1163 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1164 Cursor c = resolver.query(entityUri,
1165 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1166 if (c == null) {
1167 Log.e(TAG, "Unable to open Contacts DB cursor");
1168 showToast(R.string.contactSavedErrorToast);
1169 return;
1170 }
1171 long dataIdToAddSuperPrimary = -1;
1172 try {
1173 if (c.moveToFirst()) {
1174 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1175 }
1176 } finally {
1177 c.close();
1178 }
1179
1180 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1181 // display name does not change as a result of the join.
1182 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001183 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001184 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1185 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1186 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001187 operations.add(builder.build());
1188 }
1189
1190 boolean success = false;
1191 // Apply all aggregation exceptions as one batch
1192 try {
1193 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001194 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001195 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001196 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001197 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001198 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001199 }
1200
1201 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1202 if (success) {
1203 Uri uri = RawContacts.getContactLookupUri(resolver,
1204 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1205 callbackIntent.setData(uri);
1206 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001207 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001208 }
1209
Brian Attwelld3946ca2015-03-03 11:13:49 -08001210 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1211 if (contactIds == null) {
1212 return null;
1213 }
1214
Brian Attwell548f5c62015-01-27 17:46:46 -08001215 final ContentResolver resolver = getContentResolver();
1216 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001217
1218 final StringBuilder queryBuilder = new StringBuilder();
1219 final String stringContactIds[] = new String[contactIds.length];
1220 for (int i = 0; i < contactIds.length; i++) {
1221 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1222 stringContactIds[i] = String.valueOf(contactIds[i]);
1223 if (contactIds[i] == -1) {
1224 return null;
1225 }
1226 if (i == contactIds.length -1) {
1227 break;
1228 }
1229 queryBuilder.append(" OR ");
1230 }
1231
Brian Attwell548f5c62015-01-27 17:46:46 -08001232 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1233 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001234 queryBuilder.toString(),
1235 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001236 if (c == null) {
1237 Log.e(TAG, "Unable to open Contacts DB cursor");
1238 showToast(R.string.contactSavedErrorToast);
1239 return null;
1240 }
1241 try {
1242 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001243 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001244 return null;
1245 }
1246 rawContactIds = new long[c.getCount()];
1247 for (int i = 0; i < rawContactIds.length; i++) {
1248 c.moveToPosition(i);
1249 long rawContactId = c.getLong(JoinContactQuery._ID);
1250 rawContactIds[i] = rawContactId;
1251 }
1252 } finally {
1253 c.close();
1254 }
1255 return rawContactIds;
1256 }
1257
Brian Attwelld3946ca2015-03-03 11:13:49 -08001258 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1259 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1260 }
1261
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001262 /**
1263 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1264 */
1265 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1266 long rawContactId1, long rawContactId2) {
1267 Builder builder =
1268 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1269 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1270 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1271 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1272 operations.add(builder.build());
1273 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001274
1275 /**
1276 * Shows a toast on the UI thread.
1277 */
1278 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001279 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001280
1281 @Override
1282 public void run() {
1283 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1284 }
1285 });
1286 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001287
1288 private void deliverCallback(final Intent callbackIntent) {
1289 mMainHandler.post(new Runnable() {
1290
1291 @Override
1292 public void run() {
1293 deliverCallbackOnUiThread(callbackIntent);
1294 }
1295 });
1296 }
1297
1298 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1299 // TODO: this assumes that if there are multiple instances of the same
1300 // activity registered, the last one registered is the one waiting for
1301 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001302 for (Listener listener : sListeners) {
1303 if (callbackIntent.getComponent().equals(
1304 ((Activity) listener).getIntent().getComponent())) {
1305 listener.onServiceCompleted(callbackIntent);
1306 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001307 }
1308 }
1309 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001310}