blob: 6178e9de413121fb84aa5d8bf05fd14ffa254f34 [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 Jang3e764082015-05-22 09:38:42 -0700316 callbackActivity, callbackAction, bundle, /* backPressed =*/ false);
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.
Walter Jang3e764082015-05-22 09:38:42 -0700325 * @param backPressed whether the save was initiated as a result of a back button press
326 * or because the framework stopped the editor Activity
Josh Garguse692e012012-01-18 14:53:11 -0800327 */
Maurice Chu851222a2012-06-21 11:43:08 -0700328 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700329 String saveModeExtraKey, int saveMode, boolean isProfile,
330 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jang3e764082015-05-22 09:38:42 -0700331 Bundle updatedPhotos, boolean backPressed) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800332 Intent serviceIntent = new Intent(
333 context, ContactSaveService.class);
334 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
335 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700336 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
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);
Walter Jang1e8801b2015-03-10 15:57:05 -0700348 if (updatedPhotos != null) {
349 callbackIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
350 }
Walter Jang3e764082015-05-22 09:38:42 -0700351 callbackIntent.putExtra(ContactEditorFragment.INTENT_EXTRA_SAVE_BACK_PRESSED,
352 backPressed);
Josh Garguse5d3f892012-04-11 11:56:15 -0700353 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
354 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800355 return serviceIntent;
356 }
357
358 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700359 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700360 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800361 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800362
Jay Shrauner08099782015-03-25 14:17:11 -0700363 if (state == null) {
364 Log.e(TAG, "Invalid arguments for saveContact request");
365 return;
366 }
367
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800368 // Trim any empty fields, and RawContacts, before persisting
369 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700370 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800371
372 Uri lookupUri = null;
373
374 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800375 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800376
Josh Gargusef15c8e2012-01-30 16:42:02 -0800377 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
378 long insertedRawContactId = -1;
379
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800380 // Attempt to persist changes
381 int tries = 0;
382 while (tries++ < PERSIST_TRIES) {
383 try {
384 // Build operations and try applying
385 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700386 if (DEBUG) {
387 Log.v(TAG, "Content Provider Operations:");
388 for (ContentProviderOperation operation : diff) {
389 Log.v(TAG, operation.toString());
390 }
391 }
392
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800393 ContentProviderResult[] results = null;
394 if (!diff.isEmpty()) {
395 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
Jay Shrauner511561d2015-04-02 10:35:33 -0700396 if (results == null) {
397 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
398 // Retry save
399 continue;
400 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800401 }
402
403 final long rawContactId = getRawContactId(state, diff, results);
404 if (rawContactId == -1) {
405 throw new IllegalStateException("Could not determine RawContact ID after save");
406 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800407 // We don't have to check to see if the value is still -1. If we reach here,
408 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
409 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700410 if (isProfile) {
411 // Since the profile supports local raw contacts, which may have been completely
412 // removed if all information was removed, we need to do a special query to
413 // get the lookup URI for the profile contact (if it still exists).
414 Cursor c = resolver.query(Profile.CONTENT_URI,
415 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
416 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800417 if (c == null) {
418 continue;
419 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700420 try {
Erik162b7e32011-09-20 15:23:55 -0700421 if (c.moveToFirst()) {
422 final long contactId = c.getLong(0);
423 final String lookupKey = c.getString(1);
424 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
425 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700426 } finally {
427 c.close();
428 }
429 } else {
430 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
431 rawContactId);
432 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
433 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800434 if (lookupUri != null) {
435 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
436 }
Josh Garguse692e012012-01-18 14:53:11 -0800437
438 // We can change this back to false later, if we fail to save the contact photo.
439 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800440 break;
441
442 } catch (RemoteException e) {
443 // Something went wrong, bail without success
444 Log.e(TAG, "Problem persisting user edits", e);
445 break;
446
Jay Shrauner57fca182014-01-17 14:20:50 -0800447 } catch (IllegalArgumentException e) {
448 // This is thrown by applyBatch on malformed requests
449 Log.e(TAG, "Problem persisting user edits", e);
450 showToast(R.string.contactSavedErrorToast);
451 break;
452
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800453 } catch (OperationApplicationException e) {
454 // Version consistency failed, re-parent change and try again
455 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
456 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
457 boolean first = true;
458 final int count = state.size();
459 for (int i = 0; i < count; i++) {
460 Long rawContactId = state.getRawContactId(i);
461 if (rawContactId != null && rawContactId != -1) {
462 if (!first) {
463 sb.append(',');
464 }
465 sb.append(rawContactId);
466 first = false;
467 }
468 }
469 sb.append(")");
470
471 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800472 throw new IllegalStateException(
473 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800474 }
475
Maurice Chu851222a2012-06-21 11:43:08 -0700476 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700477 isProfile
478 ? RawContactsEntity.PROFILE_CONTENT_URI
479 : RawContactsEntity.CONTENT_URI,
480 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700481 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700482
483 // Update the new state to use profile URIs if appropriate.
484 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700485 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700486 delta.setProfileQueryUri();
487 }
488 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800489 }
490 }
491
Josh Garguse692e012012-01-18 14:53:11 -0800492 // Now save any updated photos. We do this at the end to ensure that
493 // the ContactProvider already knows about newly-created contacts.
494 if (updatedPhotos != null) {
495 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700496 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800497 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800498
499 // If the raw-contact ID is negative, we are saving a new raw-contact;
500 // replace the bogus ID with the new one that we actually saved the contact at.
501 if (rawContactId < 0) {
502 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800503 }
504
Jay Shrauner511561d2015-04-02 10:35:33 -0700505 // If the save failed, insertedRawContactId will be -1
506 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri)) {
507 succeeded = false;
508 }
Josh Garguse692e012012-01-18 14:53:11 -0800509 }
510 }
511
Josh Garguse5d3f892012-04-11 11:56:15 -0700512 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
513 if (callbackIntent != null) {
514 if (succeeded) {
515 // Mark the intent to indicate that the save was successful (even if the lookup URI
516 // is now null). For local contacts or the local profile, it's possible that the
517 // save triggered removal of the contact, so no lookup URI would exist..
518 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
519 }
520 callbackIntent.setData(lookupUri);
521 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800522 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800523 }
524
Josh Garguse692e012012-01-18 14:53:11 -0800525 /**
526 * Save updated photo for the specified raw-contact.
527 * @return true for success, false for failure
528 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700529 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800530 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800531 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
532 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
533
Yorke Lee637a38e2013-09-14 08:36:33 -0700534 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
Josh Garguse692e012012-01-18 14:53:11 -0800535 }
536
Josh Gargusef15c8e2012-01-30 16:42:02 -0800537 /**
538 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
539 */
Maurice Chu851222a2012-06-21 11:43:08 -0700540 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800541 final ArrayList<ContentProviderOperation> diff,
542 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800543 long existingRawContactId = state.findRawContactId();
544 if (existingRawContactId != -1) {
545 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800546 }
547
Josh Gargusef15c8e2012-01-30 16:42:02 -0800548 return getInsertedRawContactId(diff, results);
549 }
550
551 /**
552 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
553 */
554 private long getInsertedRawContactId(
555 final ArrayList<ContentProviderOperation> diff,
556 final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800557 if (results == null) {
558 return -1;
559 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800560 final int diffSize = diff.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800561 final int numResults = results.length;
562 for (int i = 0; i < diffSize && i < numResults; i++) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800563 ContentProviderOperation operation = diff.get(i);
Brian Attwell13f94e12015-01-22 16:27:48 -0800564 if (operation.isInsert() && operation.getUri().getEncodedPath().contains(
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800565 RawContacts.CONTENT_URI.getEncodedPath())) {
566 return ContentUris.parseId(results[i].uri);
567 }
568 }
569 return -1;
570 }
571
572 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700573 * Creates an intent that can be sent to this service to create a new group as
574 * well as add new members at the same time.
575 *
576 * @param context of the application
577 * @param account in which the group should be created
578 * @param label is the name of the group (cannot be null)
579 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
580 * should be added to the group
581 * @param callbackActivity is the activity to send the callback intent to
582 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700583 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700584 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700585 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700586 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800587 Intent serviceIntent = new Intent(context, ContactSaveService.class);
588 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
589 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
590 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700591 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800592 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700593 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700594
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800595 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700596 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800597 Intent callbackIntent = new Intent(context, callbackActivity);
598 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700599 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800600
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700601 return serviceIntent;
602 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800603
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800604 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700605 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
606 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
607 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
608 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700609 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800610
611 ContentValues values = new ContentValues();
612 values.put(Groups.ACCOUNT_TYPE, accountType);
613 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700614 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800615 values.put(Groups.TITLE, label);
616
Katherine Kuan717e3432011-07-13 17:03:24 -0700617 final ContentResolver resolver = getContentResolver();
618
619 // Create the new group
620 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
621
622 // If there's no URI, then the insertion failed. Abort early because group members can't be
623 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800624 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700625 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800626 return;
627 }
628
Katherine Kuan717e3432011-07-13 17:03:24 -0700629 // Add new group members
630 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
631
632 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
633 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800634 values.clear();
635 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
636 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
637
638 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700639 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700640 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800641 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800642 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800643 }
644
645 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800646 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800647 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700648 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700649 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800650 Intent serviceIntent = new Intent(context, ContactSaveService.class);
651 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
652 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
653 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700654
655 // Callback intent will be invoked by the service once the group is renamed.
656 Intent callbackIntent = new Intent(context, callbackActivity);
657 callbackIntent.setAction(callbackAction);
658 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
659
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800660 return serviceIntent;
661 }
662
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800663 private void renameGroup(Intent intent) {
664 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
665 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
666
667 if (groupId == -1) {
668 Log.e(TAG, "Invalid arguments for renameGroup request");
669 return;
670 }
671
672 ContentValues values = new ContentValues();
673 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700674 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
675 getContentResolver().update(groupUri, values, null, null);
676
677 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
678 callbackIntent.setData(groupUri);
679 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800680 }
681
682 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800683 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800684 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800685 public static Intent createGroupDeletionIntent(Context context, long groupId) {
686 Intent serviceIntent = new Intent(context, ContactSaveService.class);
687 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800688 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800689 return serviceIntent;
690 }
691
692 private void deleteGroup(Intent intent) {
693 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
694 if (groupId == -1) {
695 Log.e(TAG, "Invalid arguments for deleteGroup request");
696 return;
697 }
698
699 getContentResolver().delete(
700 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
701 }
702
703 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700704 * Creates an intent that can be sent to this service to rename a group as
705 * well as add and remove members from the group.
706 *
707 * @param context of the application
708 * @param groupId of the group that should be modified
709 * @param newLabel is the updated name of the group (can be null if the name
710 * should not be updated)
711 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
712 * should be added to the group
713 * @param rawContactsToRemove is an array of raw contact IDs for contacts
714 * that should be removed from the group
715 * @param callbackActivity is the activity to send the callback intent to
716 * @param callbackAction is the intent action for the callback intent
717 */
718 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
719 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700720 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700721 Intent serviceIntent = new Intent(context, ContactSaveService.class);
722 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
723 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
724 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
725 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
726 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
727 rawContactsToRemove);
728
729 // Callback intent will be invoked by the service once the group is updated
730 Intent callbackIntent = new Intent(context, callbackActivity);
731 callbackIntent.setAction(callbackAction);
732 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
733
734 return serviceIntent;
735 }
736
737 private void updateGroup(Intent intent) {
738 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
739 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
740 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
741 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
742
743 if (groupId == -1) {
744 Log.e(TAG, "Invalid arguments for updateGroup request");
745 return;
746 }
747
748 final ContentResolver resolver = getContentResolver();
749 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
750
751 // Update group name if necessary
752 if (label != null) {
753 ContentValues values = new ContentValues();
754 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700755 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700756 }
757
Katherine Kuan717e3432011-07-13 17:03:24 -0700758 // Add and remove members if necessary
759 addMembersToGroup(resolver, rawContactsToAdd, groupId);
760 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
761
762 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
763 callbackIntent.setData(groupUri);
764 deliverCallback(callbackIntent);
765 }
766
Daniel Lehmann18958a22012-02-28 17:45:25 -0800767 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700768 long groupId) {
769 if (rawContactsToAdd == null) {
770 return;
771 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700772 for (long rawContactId : rawContactsToAdd) {
773 try {
774 final ArrayList<ContentProviderOperation> rawContactOperations =
775 new ArrayList<ContentProviderOperation>();
776
777 // Build an assert operation to ensure the contact is not already in the group
778 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
779 .newAssertQuery(Data.CONTENT_URI);
780 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
781 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
782 new String[] { String.valueOf(rawContactId),
783 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
784 assertBuilder.withExpectedCount(0);
785 rawContactOperations.add(assertBuilder.build());
786
787 // Build an insert operation to add the contact to the group
788 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
789 .newInsert(Data.CONTENT_URI);
790 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
791 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
792 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
793 rawContactOperations.add(insertBuilder.build());
794
795 if (DEBUG) {
796 for (ContentProviderOperation operation : rawContactOperations) {
797 Log.v(TAG, operation.toString());
798 }
799 }
800
801 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700802 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800803 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700804 }
805 } catch (RemoteException e) {
806 // Something went wrong, bail without success
807 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
808 String.valueOf(rawContactId), e);
809 } catch (OperationApplicationException e) {
810 // The assert could have failed because the contact is already in the group,
811 // just continue to the next contact
812 Log.w(TAG, "Assert failed in adding raw contact ID " +
813 String.valueOf(rawContactId) + ". Already exists in group " +
814 String.valueOf(groupId), e);
815 }
816 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700817 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700818
Daniel Lehmann18958a22012-02-28 17:45:25 -0800819 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700820 long groupId) {
821 if (rawContactsToRemove == null) {
822 return;
823 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700824 for (long rawContactId : rawContactsToRemove) {
825 // Apply the delete operation on the data row for the given raw contact's
826 // membership in the given group. If no contact matches the provided selection, then
827 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800828 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700829 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
830 new String[] { String.valueOf(rawContactId),
831 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
832 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700833 }
834
835 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800836 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800837 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800838 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
839 Intent serviceIntent = new Intent(context, ContactSaveService.class);
840 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
841 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
842 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
843
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800844 return serviceIntent;
845 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800846
847 private void setStarred(Intent intent) {
848 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
849 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
850 if (contactUri == null) {
851 Log.e(TAG, "Invalid arguments for setStarred request");
852 return;
853 }
854
855 final ContentValues values = new ContentValues(1);
856 values.put(Contacts.STARRED, value);
857 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700858
859 // Undemote the contact if necessary
860 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
861 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800862 if (c == null) {
863 return;
864 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700865 try {
866 if (c.moveToFirst()) {
867 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700868
869 // Don't bother undemoting if this contact is the user's profile.
870 if (id < Profile.MIN_ID) {
Brian Attwell2d88efa2014-12-17 21:49:56 -0800871 PinnedPositions.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700872 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700873 }
874 } finally {
875 c.close();
876 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800877 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800878
879 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700880 * Creates an intent that can be sent to this service to set the redirect to voicemail.
881 */
882 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
883 boolean value) {
884 Intent serviceIntent = new Intent(context, ContactSaveService.class);
885 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
886 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
887 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
888
889 return serviceIntent;
890 }
891
892 private void setSendToVoicemail(Intent intent) {
893 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
894 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
895 if (contactUri == null) {
896 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
897 return;
898 }
899
900 final ContentValues values = new ContentValues(1);
901 values.put(Contacts.SEND_TO_VOICEMAIL, value);
902 getContentResolver().update(contactUri, values, null, null);
903 }
904
905 /**
906 * Creates an intent that can be sent to this service to save the contact's ringtone.
907 */
908 public static Intent createSetRingtone(Context context, Uri contactUri,
909 String value) {
910 Intent serviceIntent = new Intent(context, ContactSaveService.class);
911 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
912 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
913 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
914
915 return serviceIntent;
916 }
917
918 private void setRingtone(Intent intent) {
919 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
920 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
921 if (contactUri == null) {
922 Log.e(TAG, "Invalid arguments for setRingtone");
923 return;
924 }
925 ContentValues values = new ContentValues(1);
926 values.put(Contacts.CUSTOM_RINGTONE, value);
927 getContentResolver().update(contactUri, values, null, null);
928 }
929
930 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800931 * Creates an intent that sets the selected data item as super primary (default)
932 */
933 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
934 Intent serviceIntent = new Intent(context, ContactSaveService.class);
935 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
936 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
937 return serviceIntent;
938 }
939
940 private void setSuperPrimary(Intent intent) {
941 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
942 if (dataId == -1) {
943 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
944 return;
945 }
946
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700947 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800948 }
949
950 /**
951 * Creates an intent that clears the primary flag of all data items that belong to the same
952 * raw_contact as the given data item. Will only clear, if the data item was primary before
953 * this call
954 */
955 public static Intent createClearPrimaryIntent(Context context, long dataId) {
956 Intent serviceIntent = new Intent(context, ContactSaveService.class);
957 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
958 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
959 return serviceIntent;
960 }
961
962 private void clearPrimary(Intent intent) {
963 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
964 if (dataId == -1) {
965 Log.e(TAG, "Invalid arguments for clearPrimary request");
966 return;
967 }
968
969 // Update the primary values in the data record.
970 ContentValues values = new ContentValues(1);
971 values.put(Data.IS_SUPER_PRIMARY, 0);
972 values.put(Data.IS_PRIMARY, 0);
973
974 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
975 values, null, null);
976 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800977
978 /**
979 * Creates an intent that can be sent to this service to delete a contact.
980 */
981 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
982 Intent serviceIntent = new Intent(context, ContactSaveService.class);
983 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
984 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
985 return serviceIntent;
986 }
987
Brian Attwelld2962a32015-03-02 14:48:50 -0800988 /**
989 * Creates an intent that can be sent to this service to delete multiple contacts.
990 */
991 public static Intent createDeleteMultipleContactsIntent(Context context,
992 long[] contactIds) {
993 Intent serviceIntent = new Intent(context, ContactSaveService.class);
994 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
995 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
996 return serviceIntent;
997 }
998
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800999 private void deleteContact(Intent intent) {
1000 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1001 if (contactUri == null) {
1002 Log.e(TAG, "Invalid arguments for deleteContact request");
1003 return;
1004 }
1005
1006 getContentResolver().delete(contactUri, null, null);
1007 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001008
Brian Attwelld2962a32015-03-02 14:48:50 -08001009 private void deleteMultipleContacts(Intent intent) {
1010 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1011 if (contactIds == null) {
1012 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1013 return;
1014 }
1015 for (long contactId : contactIds) {
1016 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1017 getContentResolver().delete(contactUri, null, null);
1018 }
Brian Attwelle986c6b2015-03-05 19:47:30 -08001019 showToast(R.string.contacts_deleted_toast);
Brian Attwelld2962a32015-03-02 14:48:50 -08001020 }
1021
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001022 /**
1023 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001024 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001025 */
1026 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001027 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001028 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1029 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1030 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1031 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001032
1033 // Callback intent will be invoked by the service once the contacts are joined.
1034 Intent callbackIntent = new Intent(context, callbackActivity);
1035 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001036 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1037
1038 return serviceIntent;
1039 }
1040
Brian Attwelld3946ca2015-03-03 11:13:49 -08001041 /**
1042 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1043 * No special attention is paid to where the resulting contact's name is taken from.
1044 */
1045 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1046 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1047 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1048 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1049 return serviceIntent;
1050 }
1051
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001052
1053 private interface JoinContactQuery {
1054 String[] PROJECTION = {
1055 RawContacts._ID,
1056 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001057 RawContacts.DISPLAY_NAME_SOURCE,
1058 };
1059
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001060 int _ID = 0;
1061 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001062 int DISPLAY_NAME_SOURCE = 2;
1063 }
1064
1065 private interface ContactEntityQuery {
1066 String[] PROJECTION = {
1067 Contacts.Entity.DATA_ID,
1068 Contacts.Entity.CONTACT_ID,
1069 Contacts.Entity.IS_SUPER_PRIMARY,
1070 };
1071 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1072 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1073 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1074 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1075
1076 int DATA_ID = 0;
1077 int CONTACT_ID = 1;
1078 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001079 }
1080
Brian Attwelld3946ca2015-03-03 11:13:49 -08001081 private void joinSeveralContacts(Intent intent) {
1082 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001083
Brian Attwelld3946ca2015-03-03 11:13:49 -08001084 // Load raw contact IDs for all contacts involved.
1085 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1086 if (rawContactIds == null) {
1087 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001088 return;
1089 }
1090
Brian Attwelld3946ca2015-03-03 11:13:49 -08001091 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001092 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001093 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1094 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1095 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001096 for (int i = 0; i < rawContactIds.length; i++) {
1097 for (int j = 0; j < rawContactIds.length; j++) {
1098 if (i != j) {
1099 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1100 }
Walter Jang0653de32015-07-24 12:12:40 -07001101 // Before we get to 500 we need to flush the operations list
1102 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1103 if (!applyJoinOperations(resolver, operations)) {
1104 return;
1105 }
1106 operations.clear();
1107 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001108 }
1109 }
Walter Jang0653de32015-07-24 12:12:40 -07001110 if (operations.size() > 0 && !applyJoinOperations(resolver, operations)) {
1111 return;
1112 }
1113 showToast(R.string.contactsJoinedMessage);
1114 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001115
Walter Jang0653de32015-07-24 12:12:40 -07001116 /** Returns true if the batch was successfully applied and false otherwise. */
1117 private boolean applyJoinOperations(ContentResolver resolver,
1118 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001119 try {
1120 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001121 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001122 } catch (RemoteException | OperationApplicationException e) {
1123 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1124 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001125 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001126 }
1127 }
1128
1129
1130 private void joinContacts(Intent intent) {
1131 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1132 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001133
1134 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001135 // in the join UIs.
1136 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1137 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001138 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001139 return;
1140 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001141
Brian Attwell548f5c62015-01-27 17:46:46 -08001142 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001143
1144 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001145 for (int i = 0; i < rawContactIds.length; i++) {
1146 for (int j = 0; j < rawContactIds.length; j++) {
1147 if (i != j) {
1148 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1149 }
1150 }
1151 }
1152
Brian Attwelld3946ca2015-03-03 11:13:49 -08001153 final ContentResolver resolver = getContentResolver();
1154
Brian Attwell548f5c62015-01-27 17:46:46 -08001155 // Use the name for contactId1 as the name for the newly aggregated contact.
1156 final Uri contactId1Uri = ContentUris.withAppendedId(
1157 Contacts.CONTENT_URI, contactId1);
1158 final Uri entityUri = Uri.withAppendedPath(
1159 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1160 Cursor c = resolver.query(entityUri,
1161 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1162 if (c == null) {
1163 Log.e(TAG, "Unable to open Contacts DB cursor");
1164 showToast(R.string.contactSavedErrorToast);
1165 return;
1166 }
1167 long dataIdToAddSuperPrimary = -1;
1168 try {
1169 if (c.moveToFirst()) {
1170 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1171 }
1172 } finally {
1173 c.close();
1174 }
1175
1176 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1177 // display name does not change as a result of the join.
1178 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001179 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001180 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1181 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1182 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001183 operations.add(builder.build());
1184 }
1185
1186 boolean success = false;
1187 // Apply all aggregation exceptions as one batch
1188 try {
1189 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001190 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001191 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001192 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001193 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001194 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001195 }
1196
1197 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1198 if (success) {
1199 Uri uri = RawContacts.getContactLookupUri(resolver,
1200 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1201 callbackIntent.setData(uri);
1202 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001203 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001204 }
1205
Brian Attwelld3946ca2015-03-03 11:13:49 -08001206 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1207 if (contactIds == null) {
1208 return null;
1209 }
1210
Brian Attwell548f5c62015-01-27 17:46:46 -08001211 final ContentResolver resolver = getContentResolver();
1212 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001213
1214 final StringBuilder queryBuilder = new StringBuilder();
1215 final String stringContactIds[] = new String[contactIds.length];
1216 for (int i = 0; i < contactIds.length; i++) {
1217 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1218 stringContactIds[i] = String.valueOf(contactIds[i]);
1219 if (contactIds[i] == -1) {
1220 return null;
1221 }
1222 if (i == contactIds.length -1) {
1223 break;
1224 }
1225 queryBuilder.append(" OR ");
1226 }
1227
Brian Attwell548f5c62015-01-27 17:46:46 -08001228 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1229 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001230 queryBuilder.toString(),
1231 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001232 if (c == null) {
1233 Log.e(TAG, "Unable to open Contacts DB cursor");
1234 showToast(R.string.contactSavedErrorToast);
1235 return null;
1236 }
1237 try {
1238 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001239 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001240 return null;
1241 }
1242 rawContactIds = new long[c.getCount()];
1243 for (int i = 0; i < rawContactIds.length; i++) {
1244 c.moveToPosition(i);
1245 long rawContactId = c.getLong(JoinContactQuery._ID);
1246 rawContactIds[i] = rawContactId;
1247 }
1248 } finally {
1249 c.close();
1250 }
1251 return rawContactIds;
1252 }
1253
Brian Attwelld3946ca2015-03-03 11:13:49 -08001254 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1255 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1256 }
1257
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001258 /**
1259 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1260 */
1261 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1262 long rawContactId1, long rawContactId2) {
1263 Builder builder =
1264 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1265 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1266 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1267 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1268 operations.add(builder.build());
1269 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001270
1271 /**
1272 * Shows a toast on the UI thread.
1273 */
1274 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001275 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001276
1277 @Override
1278 public void run() {
1279 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1280 }
1281 });
1282 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001283
1284 private void deliverCallback(final Intent callbackIntent) {
1285 mMainHandler.post(new Runnable() {
1286
1287 @Override
1288 public void run() {
1289 deliverCallbackOnUiThread(callbackIntent);
1290 }
1291 });
1292 }
1293
1294 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1295 // TODO: this assumes that if there are multiple instances of the same
1296 // activity registered, the last one registered is the one waiting for
1297 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001298 for (Listener listener : sListeners) {
1299 if (callbackIntent.getComponent().equals(
1300 ((Activity) listener).getIntent().getComponent())) {
1301 listener.onServiceCompleted(callbackIntent);
1302 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001303 }
1304 }
1305 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001306}