blob: dc6cdebbb348fa256150ee1e0e6f0fc28bb9e93d [file] [log] [blame]
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080017package com.android.contacts;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070018
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080019import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070020import android.app.IntentService;
21import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080022import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070023import android.content.ContentProviderResult;
24import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080025import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070026import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080027import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070028import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080029import android.content.OperationApplicationException;
30import android.database.Cursor;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070031import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080032import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080033import android.os.Handler;
34import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080035import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080036import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070037import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080038import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080039import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080040import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080041import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070042import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080043import android.provider.ContactsContract.Groups;
Yorke Leee8e3fb82013-09-12 17:53:31 -070044import android.provider.ContactsContract.PinnedPositions;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070045import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070046import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070047import android.provider.ContactsContract.RawContactsEntity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070048import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080049import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070050
Chiao Chengd7ca03e2012-10-24 15:14:08 -070051import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080052import com.android.contacts.common.model.AccountTypeManager;
Yorke Leecd321f62013-10-28 15:20:15 -070053import com.android.contacts.common.model.RawContactDelta;
54import com.android.contacts.common.model.RawContactDeltaList;
55import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080056import com.android.contacts.common.model.account.AccountWithDataSet;
Walter Jang3e764082015-05-22 09:38:42 -070057import com.android.contacts.editor.ContactEditorFragment;
Yorke Lee637a38e2013-09-14 08:36:33 -070058import com.android.contacts.util.ContactPhotoUtils;
59
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070060import com.google.common.collect.Lists;
61import com.google.common.collect.Sets;
62
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080063import java.util.ArrayList;
64import java.util.HashSet;
65import java.util.List;
66import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070067
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080068/**
69 * A service responsible for saving changes to the content provider.
70 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070071public class ContactSaveService extends IntentService {
72 private static final String TAG = "ContactSaveService";
73
Katherine Kuana007e442011-07-07 09:25:34 -070074 /** Set to true in order to view logs on content provider operations */
75 private static final boolean DEBUG = false;
76
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070077 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
78
79 public static final String EXTRA_ACCOUNT_NAME = "accountName";
80 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070081 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070082 public static final String EXTRA_CONTENT_VALUES = "contentValues";
83 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
84
Dmitri Plotnikova0114142011-02-15 13:53:21 -080085 public static final String ACTION_SAVE_CONTACT = "saveContact";
86 public static final String EXTRA_CONTACT_STATE = "state";
87 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070088 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070089 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080090 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070091
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080092 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080093 public static final String ACTION_RENAME_GROUP = "renameGroup";
94 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070095 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080096 public static final String EXTRA_GROUP_ID = "groupId";
97 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070098 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
99 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800100
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800101 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800102 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800103 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800104 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800105 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800106 public static final String EXTRA_STARRED_FLAG = "starred";
107
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800108 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
109 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
110 public static final String EXTRA_DATA_ID = "dataId";
111
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800112 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800113 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800114 public static final String EXTRA_CONTACT_ID1 = "contactId1";
115 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800116
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700117 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
118 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
119
120 public static final String ACTION_SET_RINGTONE = "setRingtone";
121 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
122
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700123 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
124 Data.MIMETYPE,
125 Data.IS_PRIMARY,
126 Data.DATA1,
127 Data.DATA2,
128 Data.DATA3,
129 Data.DATA4,
130 Data.DATA5,
131 Data.DATA6,
132 Data.DATA7,
133 Data.DATA8,
134 Data.DATA9,
135 Data.DATA10,
136 Data.DATA11,
137 Data.DATA12,
138 Data.DATA13,
139 Data.DATA14,
140 Data.DATA15
141 );
142
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800143 private static final int PERSIST_TRIES = 3;
144
Walter Jang0653de32015-07-24 12:12:40 -0700145 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
146
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800147 public interface Listener {
148 public void onServiceCompleted(Intent callbackIntent);
149 }
150
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100151 private static final CopyOnWriteArrayList<Listener> sListeners =
152 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800153
154 private Handler mMainHandler;
155
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700156 public ContactSaveService() {
157 super(TAG);
158 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800159 mMainHandler = new Handler(Looper.getMainLooper());
160 }
161
162 public static void registerListener(Listener listener) {
163 if (!(listener instanceof Activity)) {
164 throw new ClassCastException("Only activities can be registered to"
165 + " receive callback from " + ContactSaveService.class.getName());
166 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100167 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800168 }
169
170 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100171 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700172 }
173
174 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800175 public Object getSystemService(String name) {
176 Object service = super.getSystemService(name);
177 if (service != null) {
178 return service;
179 }
180
181 return getApplicationContext().getSystemService(name);
182 }
183
184 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700185 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800186 if (intent == null) {
187 Log.d(TAG, "onHandleIntent: could not handle null intent");
188 return;
189 }
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700190 // Call an appropriate method. If we're sure it affects how incoming phone calls are
191 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700192 String action = intent.getAction();
193 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
194 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800195 } else if (ACTION_SAVE_CONTACT.equals(action)) {
196 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800197 } else if (ACTION_CREATE_GROUP.equals(action)) {
198 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800199 } else if (ACTION_RENAME_GROUP.equals(action)) {
200 renameGroup(intent);
201 } else if (ACTION_DELETE_GROUP.equals(action)) {
202 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700203 } else if (ACTION_UPDATE_GROUP.equals(action)) {
204 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800205 } else if (ACTION_SET_STARRED.equals(action)) {
206 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800207 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
208 setSuperPrimary(intent);
209 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
210 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800211 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
212 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800213 } else if (ACTION_DELETE_CONTACT.equals(action)) {
214 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800215 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
216 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800217 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
218 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700219 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
220 setSendToVoicemail(intent);
221 } else if (ACTION_SET_RINGTONE.equals(action)) {
222 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700223 }
224 }
225
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800226 /**
227 * Creates an intent that can be sent to this service to create a new raw contact
228 * using data presented as a set of ContentValues.
229 */
230 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700231 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700232 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800233 Intent serviceIntent = new Intent(
234 context, ContactSaveService.class);
235 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
236 if (account != null) {
237 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
238 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700239 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800240 }
241 serviceIntent.putParcelableArrayListExtra(
242 ContactSaveService.EXTRA_CONTENT_VALUES, values);
243
244 // Callback intent will be invoked by the service once the new contact is
245 // created. The service will put the URI of the new contact as "data" on
246 // the callback intent.
247 Intent callbackIntent = new Intent(context, callbackActivity);
248 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800249 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
250 return serviceIntent;
251 }
252
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700253 private void createRawContact(Intent intent) {
254 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
255 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700256 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700257 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
258 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
259
260 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
261 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
262 .withValue(RawContacts.ACCOUNT_NAME, accountName)
263 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700264 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700265 .build());
266
267 int size = valueList.size();
268 for (int i = 0; i < size; i++) {
269 ContentValues values = valueList.get(i);
270 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
271 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
272 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
273 .withValues(values)
274 .build());
275 }
276
277 ContentResolver resolver = getContentResolver();
278 ContentProviderResult[] results;
279 try {
280 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
281 } catch (Exception e) {
282 throw new RuntimeException("Failed to store new contact", e);
283 }
284
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700285 Uri rawContactUri = results[0].uri;
286 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
287
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800288 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700289 }
290
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700291 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800292 * Creates an intent that can be sent to this service to create a new raw contact
293 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800294 * This variant is more convenient to use when there is only one photo that can
295 * possibly be updated, as in the Contact Details screen.
296 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
297 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800298 */
Maurice Chu851222a2012-06-21 11:43:08 -0700299 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700300 String saveModeExtraKey, int saveMode, boolean isProfile,
301 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700302 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800303 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700304 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800305 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jang3e764082015-05-22 09:38:42 -0700306 callbackActivity, callbackAction, bundle, /* backPressed =*/ false);
Josh Garguse692e012012-01-18 14:53:11 -0800307 }
308
309 /**
310 * Creates an intent that can be sent to this service to create a new raw contact
311 * using data presented as a set of ContentValues.
312 * This variant is used when multiple contacts' photos may be updated, as in the
313 * Contact Editor.
314 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jang3e764082015-05-22 09:38:42 -0700315 * @param backPressed whether the save was initiated as a result of a back button press
316 * or because the framework stopped the editor Activity
Josh Garguse692e012012-01-18 14:53:11 -0800317 */
Maurice Chu851222a2012-06-21 11:43:08 -0700318 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700319 String saveModeExtraKey, int saveMode, boolean isProfile,
320 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jang3e764082015-05-22 09:38:42 -0700321 Bundle updatedPhotos, boolean backPressed) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800322 Intent serviceIntent = new Intent(
323 context, ContactSaveService.class);
324 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
325 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700326 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Josh Garguse692e012012-01-18 14:53:11 -0800327 if (updatedPhotos != null) {
328 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
329 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800330
Josh Garguse5d3f892012-04-11 11:56:15 -0700331 if (callbackActivity != null) {
332 // Callback intent will be invoked by the service once the contact is
333 // saved. The service will put the URI of the new contact as "data" on
334 // the callback intent.
335 Intent callbackIntent = new Intent(context, callbackActivity);
336 callbackIntent.putExtra(saveModeExtraKey, saveMode);
337 callbackIntent.setAction(callbackAction);
Walter Jang1e8801b2015-03-10 15:57:05 -0700338 if (updatedPhotos != null) {
339 callbackIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
340 }
Walter Jang3e764082015-05-22 09:38:42 -0700341 callbackIntent.putExtra(ContactEditorFragment.INTENT_EXTRA_SAVE_BACK_PRESSED,
342 backPressed);
Josh Garguse5d3f892012-04-11 11:56:15 -0700343 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
344 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800345 return serviceIntent;
346 }
347
348 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700349 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700350 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800351 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800352
Jay Shrauner08099782015-03-25 14:17:11 -0700353 if (state == null) {
354 Log.e(TAG, "Invalid arguments for saveContact request");
355 return;
356 }
357
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800358 // Trim any empty fields, and RawContacts, before persisting
359 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700360 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800361
362 Uri lookupUri = null;
363
364 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800365 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800366
Josh Gargusef15c8e2012-01-30 16:42:02 -0800367 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
368 long insertedRawContactId = -1;
369
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800370 // Attempt to persist changes
371 int tries = 0;
372 while (tries++ < PERSIST_TRIES) {
373 try {
374 // Build operations and try applying
375 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700376 if (DEBUG) {
377 Log.v(TAG, "Content Provider Operations:");
378 for (ContentProviderOperation operation : diff) {
379 Log.v(TAG, operation.toString());
380 }
381 }
382
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800383 ContentProviderResult[] results = null;
384 if (!diff.isEmpty()) {
385 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
Jay Shrauner511561d2015-04-02 10:35:33 -0700386 if (results == null) {
387 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
388 // Retry save
389 continue;
390 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800391 }
392
393 final long rawContactId = getRawContactId(state, diff, results);
394 if (rawContactId == -1) {
395 throw new IllegalStateException("Could not determine RawContact ID after save");
396 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800397 // We don't have to check to see if the value is still -1. If we reach here,
398 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
399 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700400 if (isProfile) {
401 // Since the profile supports local raw contacts, which may have been completely
402 // removed if all information was removed, we need to do a special query to
403 // get the lookup URI for the profile contact (if it still exists).
404 Cursor c = resolver.query(Profile.CONTENT_URI,
405 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
406 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800407 if (c == null) {
408 continue;
409 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700410 try {
Erik162b7e32011-09-20 15:23:55 -0700411 if (c.moveToFirst()) {
412 final long contactId = c.getLong(0);
413 final String lookupKey = c.getString(1);
414 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
415 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700416 } finally {
417 c.close();
418 }
419 } else {
420 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
421 rawContactId);
422 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
423 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800424 if (lookupUri != null) {
425 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
426 }
Josh Garguse692e012012-01-18 14:53:11 -0800427
428 // We can change this back to false later, if we fail to save the contact photo.
429 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800430 break;
431
432 } catch (RemoteException e) {
433 // Something went wrong, bail without success
434 Log.e(TAG, "Problem persisting user edits", e);
435 break;
436
Jay Shrauner57fca182014-01-17 14:20:50 -0800437 } catch (IllegalArgumentException e) {
438 // This is thrown by applyBatch on malformed requests
439 Log.e(TAG, "Problem persisting user edits", e);
440 showToast(R.string.contactSavedErrorToast);
441 break;
442
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800443 } catch (OperationApplicationException e) {
444 // Version consistency failed, re-parent change and try again
445 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
446 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
447 boolean first = true;
448 final int count = state.size();
449 for (int i = 0; i < count; i++) {
450 Long rawContactId = state.getRawContactId(i);
451 if (rawContactId != null && rawContactId != -1) {
452 if (!first) {
453 sb.append(',');
454 }
455 sb.append(rawContactId);
456 first = false;
457 }
458 }
459 sb.append(")");
460
461 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800462 throw new IllegalStateException(
463 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800464 }
465
Maurice Chu851222a2012-06-21 11:43:08 -0700466 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700467 isProfile
468 ? RawContactsEntity.PROFILE_CONTENT_URI
469 : RawContactsEntity.CONTENT_URI,
470 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700471 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700472
473 // Update the new state to use profile URIs if appropriate.
474 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700475 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700476 delta.setProfileQueryUri();
477 }
478 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800479 }
480 }
481
Josh Garguse692e012012-01-18 14:53:11 -0800482 // Now save any updated photos. We do this at the end to ensure that
483 // the ContactProvider already knows about newly-created contacts.
484 if (updatedPhotos != null) {
485 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700486 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800487 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800488
489 // If the raw-contact ID is negative, we are saving a new raw-contact;
490 // replace the bogus ID with the new one that we actually saved the contact at.
491 if (rawContactId < 0) {
492 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800493 }
494
Jay Shrauner511561d2015-04-02 10:35:33 -0700495 // If the save failed, insertedRawContactId will be -1
496 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri)) {
497 succeeded = false;
498 }
Josh Garguse692e012012-01-18 14:53:11 -0800499 }
500 }
501
Josh Garguse5d3f892012-04-11 11:56:15 -0700502 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
503 if (callbackIntent != null) {
504 if (succeeded) {
505 // Mark the intent to indicate that the save was successful (even if the lookup URI
506 // is now null). For local contacts or the local profile, it's possible that the
507 // save triggered removal of the contact, so no lookup URI would exist..
508 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
509 }
510 callbackIntent.setData(lookupUri);
511 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800512 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800513 }
514
Josh Garguse692e012012-01-18 14:53:11 -0800515 /**
516 * Save updated photo for the specified raw-contact.
517 * @return true for success, false for failure
518 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700519 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800520 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800521 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
522 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
523
Yorke Lee637a38e2013-09-14 08:36:33 -0700524 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
Josh Garguse692e012012-01-18 14:53:11 -0800525 }
526
Josh Gargusef15c8e2012-01-30 16:42:02 -0800527 /**
528 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
529 */
Maurice Chu851222a2012-06-21 11:43:08 -0700530 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800531 final ArrayList<ContentProviderOperation> diff,
532 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800533 long existingRawContactId = state.findRawContactId();
534 if (existingRawContactId != -1) {
535 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800536 }
537
Josh Gargusef15c8e2012-01-30 16:42:02 -0800538 return getInsertedRawContactId(diff, results);
539 }
540
541 /**
542 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
543 */
544 private long getInsertedRawContactId(
545 final ArrayList<ContentProviderOperation> diff,
546 final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800547 if (results == null) {
548 return -1;
549 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800550 final int diffSize = diff.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800551 final int numResults = results.length;
552 for (int i = 0; i < diffSize && i < numResults; i++) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800553 ContentProviderOperation operation = diff.get(i);
Brian Attwell13f94e12015-01-22 16:27:48 -0800554 if (operation.isInsert() && operation.getUri().getEncodedPath().contains(
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800555 RawContacts.CONTENT_URI.getEncodedPath())) {
556 return ContentUris.parseId(results[i].uri);
557 }
558 }
559 return -1;
560 }
561
562 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700563 * Creates an intent that can be sent to this service to create a new group as
564 * well as add new members at the same time.
565 *
566 * @param context of the application
567 * @param account in which the group should be created
568 * @param label is the name of the group (cannot be null)
569 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
570 * should be added to the group
571 * @param callbackActivity is the activity to send the callback intent to
572 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700573 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700574 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700575 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700576 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800577 Intent serviceIntent = new Intent(context, ContactSaveService.class);
578 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
579 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
580 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700581 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800582 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700583 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700584
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800585 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700586 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800587 Intent callbackIntent = new Intent(context, callbackActivity);
588 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700589 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800590
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700591 return serviceIntent;
592 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800593
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800594 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700595 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
596 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
597 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
598 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700599 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800600
601 ContentValues values = new ContentValues();
602 values.put(Groups.ACCOUNT_TYPE, accountType);
603 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700604 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800605 values.put(Groups.TITLE, label);
606
Katherine Kuan717e3432011-07-13 17:03:24 -0700607 final ContentResolver resolver = getContentResolver();
608
609 // Create the new group
610 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
611
612 // If there's no URI, then the insertion failed. Abort early because group members can't be
613 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800614 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700615 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800616 return;
617 }
618
Katherine Kuan717e3432011-07-13 17:03:24 -0700619 // Add new group members
620 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
621
622 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
623 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800624 values.clear();
625 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
626 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
627
628 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700629 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700630 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800631 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800632 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800633 }
634
635 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800636 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800637 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700638 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700639 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800640 Intent serviceIntent = new Intent(context, ContactSaveService.class);
641 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
642 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
643 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700644
645 // Callback intent will be invoked by the service once the group is renamed.
646 Intent callbackIntent = new Intent(context, callbackActivity);
647 callbackIntent.setAction(callbackAction);
648 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
649
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800650 return serviceIntent;
651 }
652
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800653 private void renameGroup(Intent intent) {
654 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
655 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
656
657 if (groupId == -1) {
658 Log.e(TAG, "Invalid arguments for renameGroup request");
659 return;
660 }
661
662 ContentValues values = new ContentValues();
663 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700664 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
665 getContentResolver().update(groupUri, values, null, null);
666
667 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
668 callbackIntent.setData(groupUri);
669 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800670 }
671
672 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800673 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800674 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800675 public static Intent createGroupDeletionIntent(Context context, long groupId) {
676 Intent serviceIntent = new Intent(context, ContactSaveService.class);
677 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800678 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800679 return serviceIntent;
680 }
681
682 private void deleteGroup(Intent intent) {
683 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
684 if (groupId == -1) {
685 Log.e(TAG, "Invalid arguments for deleteGroup request");
686 return;
687 }
688
689 getContentResolver().delete(
690 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
691 }
692
693 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700694 * Creates an intent that can be sent to this service to rename a group as
695 * well as add and remove members from the group.
696 *
697 * @param context of the application
698 * @param groupId of the group that should be modified
699 * @param newLabel is the updated name of the group (can be null if the name
700 * should not be updated)
701 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
702 * should be added to the group
703 * @param rawContactsToRemove is an array of raw contact IDs for contacts
704 * that should be removed from the group
705 * @param callbackActivity is the activity to send the callback intent to
706 * @param callbackAction is the intent action for the callback intent
707 */
708 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
709 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700710 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700711 Intent serviceIntent = new Intent(context, ContactSaveService.class);
712 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
713 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
714 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
715 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
716 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
717 rawContactsToRemove);
718
719 // Callback intent will be invoked by the service once the group is updated
720 Intent callbackIntent = new Intent(context, callbackActivity);
721 callbackIntent.setAction(callbackAction);
722 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
723
724 return serviceIntent;
725 }
726
727 private void updateGroup(Intent intent) {
728 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
729 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
730 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
731 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
732
733 if (groupId == -1) {
734 Log.e(TAG, "Invalid arguments for updateGroup request");
735 return;
736 }
737
738 final ContentResolver resolver = getContentResolver();
739 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
740
741 // Update group name if necessary
742 if (label != null) {
743 ContentValues values = new ContentValues();
744 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700745 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700746 }
747
Katherine Kuan717e3432011-07-13 17:03:24 -0700748 // Add and remove members if necessary
749 addMembersToGroup(resolver, rawContactsToAdd, groupId);
750 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
751
752 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
753 callbackIntent.setData(groupUri);
754 deliverCallback(callbackIntent);
755 }
756
Daniel Lehmann18958a22012-02-28 17:45:25 -0800757 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700758 long groupId) {
759 if (rawContactsToAdd == null) {
760 return;
761 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700762 for (long rawContactId : rawContactsToAdd) {
763 try {
764 final ArrayList<ContentProviderOperation> rawContactOperations =
765 new ArrayList<ContentProviderOperation>();
766
767 // Build an assert operation to ensure the contact is not already in the group
768 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
769 .newAssertQuery(Data.CONTENT_URI);
770 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
771 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
772 new String[] { String.valueOf(rawContactId),
773 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
774 assertBuilder.withExpectedCount(0);
775 rawContactOperations.add(assertBuilder.build());
776
777 // Build an insert operation to add the contact to the group
778 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
779 .newInsert(Data.CONTENT_URI);
780 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
781 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
782 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
783 rawContactOperations.add(insertBuilder.build());
784
785 if (DEBUG) {
786 for (ContentProviderOperation operation : rawContactOperations) {
787 Log.v(TAG, operation.toString());
788 }
789 }
790
791 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700792 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800793 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700794 }
795 } catch (RemoteException e) {
796 // Something went wrong, bail without success
797 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
798 String.valueOf(rawContactId), e);
799 } catch (OperationApplicationException e) {
800 // The assert could have failed because the contact is already in the group,
801 // just continue to the next contact
802 Log.w(TAG, "Assert failed in adding raw contact ID " +
803 String.valueOf(rawContactId) + ". Already exists in group " +
804 String.valueOf(groupId), e);
805 }
806 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700807 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700808
Daniel Lehmann18958a22012-02-28 17:45:25 -0800809 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700810 long groupId) {
811 if (rawContactsToRemove == null) {
812 return;
813 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700814 for (long rawContactId : rawContactsToRemove) {
815 // Apply the delete operation on the data row for the given raw contact's
816 // membership in the given group. If no contact matches the provided selection, then
817 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800818 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700819 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
820 new String[] { String.valueOf(rawContactId),
821 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
822 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700823 }
824
825 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800826 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800827 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800828 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
829 Intent serviceIntent = new Intent(context, ContactSaveService.class);
830 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
831 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
832 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
833
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800834 return serviceIntent;
835 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800836
837 private void setStarred(Intent intent) {
838 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
839 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
840 if (contactUri == null) {
841 Log.e(TAG, "Invalid arguments for setStarred request");
842 return;
843 }
844
845 final ContentValues values = new ContentValues(1);
846 values.put(Contacts.STARRED, value);
847 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700848
849 // Undemote the contact if necessary
850 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
851 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800852 if (c == null) {
853 return;
854 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700855 try {
856 if (c.moveToFirst()) {
857 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700858
859 // Don't bother undemoting if this contact is the user's profile.
860 if (id < Profile.MIN_ID) {
Brian Attwell2d88efa2014-12-17 21:49:56 -0800861 PinnedPositions.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700862 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700863 }
864 } finally {
865 c.close();
866 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800867 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800868
869 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700870 * Creates an intent that can be sent to this service to set the redirect to voicemail.
871 */
872 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
873 boolean value) {
874 Intent serviceIntent = new Intent(context, ContactSaveService.class);
875 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
876 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
877 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
878
879 return serviceIntent;
880 }
881
882 private void setSendToVoicemail(Intent intent) {
883 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
884 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
885 if (contactUri == null) {
886 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
887 return;
888 }
889
890 final ContentValues values = new ContentValues(1);
891 values.put(Contacts.SEND_TO_VOICEMAIL, value);
892 getContentResolver().update(contactUri, values, null, null);
893 }
894
895 /**
896 * Creates an intent that can be sent to this service to save the contact's ringtone.
897 */
898 public static Intent createSetRingtone(Context context, Uri contactUri,
899 String value) {
900 Intent serviceIntent = new Intent(context, ContactSaveService.class);
901 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
902 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
903 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
904
905 return serviceIntent;
906 }
907
908 private void setRingtone(Intent intent) {
909 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
910 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
911 if (contactUri == null) {
912 Log.e(TAG, "Invalid arguments for setRingtone");
913 return;
914 }
915 ContentValues values = new ContentValues(1);
916 values.put(Contacts.CUSTOM_RINGTONE, value);
917 getContentResolver().update(contactUri, values, null, null);
918 }
919
920 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800921 * Creates an intent that sets the selected data item as super primary (default)
922 */
923 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
924 Intent serviceIntent = new Intent(context, ContactSaveService.class);
925 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
926 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
927 return serviceIntent;
928 }
929
930 private void setSuperPrimary(Intent intent) {
931 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
932 if (dataId == -1) {
933 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
934 return;
935 }
936
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700937 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800938 }
939
940 /**
941 * Creates an intent that clears the primary flag of all data items that belong to the same
942 * raw_contact as the given data item. Will only clear, if the data item was primary before
943 * this call
944 */
945 public static Intent createClearPrimaryIntent(Context context, long dataId) {
946 Intent serviceIntent = new Intent(context, ContactSaveService.class);
947 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
948 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
949 return serviceIntent;
950 }
951
952 private void clearPrimary(Intent intent) {
953 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
954 if (dataId == -1) {
955 Log.e(TAG, "Invalid arguments for clearPrimary request");
956 return;
957 }
958
959 // Update the primary values in the data record.
960 ContentValues values = new ContentValues(1);
961 values.put(Data.IS_SUPER_PRIMARY, 0);
962 values.put(Data.IS_PRIMARY, 0);
963
964 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
965 values, null, null);
966 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800967
968 /**
969 * Creates an intent that can be sent to this service to delete a contact.
970 */
971 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
972 Intent serviceIntent = new Intent(context, ContactSaveService.class);
973 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
974 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
975 return serviceIntent;
976 }
977
Brian Attwelld2962a32015-03-02 14:48:50 -0800978 /**
979 * Creates an intent that can be sent to this service to delete multiple contacts.
980 */
981 public static Intent createDeleteMultipleContactsIntent(Context context,
982 long[] contactIds) {
983 Intent serviceIntent = new Intent(context, ContactSaveService.class);
984 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
985 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
986 return serviceIntent;
987 }
988
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800989 private void deleteContact(Intent intent) {
990 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
991 if (contactUri == null) {
992 Log.e(TAG, "Invalid arguments for deleteContact request");
993 return;
994 }
995
996 getContentResolver().delete(contactUri, null, null);
997 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800998
Brian Attwelld2962a32015-03-02 14:48:50 -0800999 private void deleteMultipleContacts(Intent intent) {
1000 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1001 if (contactIds == null) {
1002 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1003 return;
1004 }
1005 for (long contactId : contactIds) {
1006 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1007 getContentResolver().delete(contactUri, null, null);
1008 }
Brian Attwelle986c6b2015-03-05 19:47:30 -08001009 showToast(R.string.contacts_deleted_toast);
Brian Attwelld2962a32015-03-02 14:48:50 -08001010 }
1011
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001012 /**
1013 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001014 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001015 */
1016 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001017 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001018 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1019 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1020 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1021 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001022
1023 // Callback intent will be invoked by the service once the contacts are joined.
1024 Intent callbackIntent = new Intent(context, callbackActivity);
1025 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001026 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1027
1028 return serviceIntent;
1029 }
1030
Brian Attwelld3946ca2015-03-03 11:13:49 -08001031 /**
1032 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1033 * No special attention is paid to where the resulting contact's name is taken from.
1034 */
1035 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1036 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1037 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1038 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1039 return serviceIntent;
1040 }
1041
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001042
1043 private interface JoinContactQuery {
1044 String[] PROJECTION = {
1045 RawContacts._ID,
1046 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001047 RawContacts.DISPLAY_NAME_SOURCE,
1048 };
1049
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001050 int _ID = 0;
1051 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001052 int DISPLAY_NAME_SOURCE = 2;
1053 }
1054
1055 private interface ContactEntityQuery {
1056 String[] PROJECTION = {
1057 Contacts.Entity.DATA_ID,
1058 Contacts.Entity.CONTACT_ID,
1059 Contacts.Entity.IS_SUPER_PRIMARY,
1060 };
1061 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1062 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1063 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1064 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1065
1066 int DATA_ID = 0;
1067 int CONTACT_ID = 1;
1068 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001069 }
1070
Brian Attwelld3946ca2015-03-03 11:13:49 -08001071 private void joinSeveralContacts(Intent intent) {
1072 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001073
Brian Attwelld3946ca2015-03-03 11:13:49 -08001074 // Load raw contact IDs for all contacts involved.
1075 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1076 if (rawContactIds == null) {
1077 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001078 return;
1079 }
1080
Brian Attwelld3946ca2015-03-03 11:13:49 -08001081 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001082 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001083 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1084 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1085 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001086 for (int i = 0; i < rawContactIds.length; i++) {
1087 for (int j = 0; j < rawContactIds.length; j++) {
1088 if (i != j) {
1089 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1090 }
Walter Jang0653de32015-07-24 12:12:40 -07001091 // Before we get to 500 we need to flush the operations list
1092 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1093 if (!applyJoinOperations(resolver, operations)) {
1094 return;
1095 }
1096 operations.clear();
1097 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001098 }
1099 }
Walter Jang0653de32015-07-24 12:12:40 -07001100 if (operations.size() > 0 && !applyJoinOperations(resolver, operations)) {
1101 return;
1102 }
1103 showToast(R.string.contactsJoinedMessage);
1104 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001105
Walter Jang0653de32015-07-24 12:12:40 -07001106 /** Returns true if the batch was successfully applied and false otherwise. */
1107 private boolean applyJoinOperations(ContentResolver resolver,
1108 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001109 try {
1110 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001111 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001112 } catch (RemoteException | OperationApplicationException e) {
1113 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1114 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001115 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001116 }
1117 }
1118
1119
1120 private void joinContacts(Intent intent) {
1121 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1122 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001123
1124 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001125 // in the join UIs.
1126 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1127 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001128 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001129 return;
1130 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001131
Brian Attwell548f5c62015-01-27 17:46:46 -08001132 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001133
1134 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001135 for (int i = 0; i < rawContactIds.length; i++) {
1136 for (int j = 0; j < rawContactIds.length; j++) {
1137 if (i != j) {
1138 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1139 }
1140 }
1141 }
1142
Brian Attwelld3946ca2015-03-03 11:13:49 -08001143 final ContentResolver resolver = getContentResolver();
1144
Brian Attwell548f5c62015-01-27 17:46:46 -08001145 // Use the name for contactId1 as the name for the newly aggregated contact.
1146 final Uri contactId1Uri = ContentUris.withAppendedId(
1147 Contacts.CONTENT_URI, contactId1);
1148 final Uri entityUri = Uri.withAppendedPath(
1149 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1150 Cursor c = resolver.query(entityUri,
1151 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1152 if (c == null) {
1153 Log.e(TAG, "Unable to open Contacts DB cursor");
1154 showToast(R.string.contactSavedErrorToast);
1155 return;
1156 }
1157 long dataIdToAddSuperPrimary = -1;
1158 try {
1159 if (c.moveToFirst()) {
1160 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1161 }
1162 } finally {
1163 c.close();
1164 }
1165
1166 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1167 // display name does not change as a result of the join.
1168 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001169 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001170 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1171 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1172 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001173 operations.add(builder.build());
1174 }
1175
1176 boolean success = false;
1177 // Apply all aggregation exceptions as one batch
1178 try {
1179 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001180 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001181 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001182 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001183 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001184 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001185 }
1186
1187 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1188 if (success) {
1189 Uri uri = RawContacts.getContactLookupUri(resolver,
1190 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1191 callbackIntent.setData(uri);
1192 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001193 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001194 }
1195
Brian Attwelld3946ca2015-03-03 11:13:49 -08001196 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1197 if (contactIds == null) {
1198 return null;
1199 }
1200
Brian Attwell548f5c62015-01-27 17:46:46 -08001201 final ContentResolver resolver = getContentResolver();
1202 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001203
1204 final StringBuilder queryBuilder = new StringBuilder();
1205 final String stringContactIds[] = new String[contactIds.length];
1206 for (int i = 0; i < contactIds.length; i++) {
1207 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1208 stringContactIds[i] = String.valueOf(contactIds[i]);
1209 if (contactIds[i] == -1) {
1210 return null;
1211 }
1212 if (i == contactIds.length -1) {
1213 break;
1214 }
1215 queryBuilder.append(" OR ");
1216 }
1217
Brian Attwell548f5c62015-01-27 17:46:46 -08001218 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1219 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001220 queryBuilder.toString(),
1221 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001222 if (c == null) {
1223 Log.e(TAG, "Unable to open Contacts DB cursor");
1224 showToast(R.string.contactSavedErrorToast);
1225 return null;
1226 }
1227 try {
1228 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001229 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001230 return null;
1231 }
1232 rawContactIds = new long[c.getCount()];
1233 for (int i = 0; i < rawContactIds.length; i++) {
1234 c.moveToPosition(i);
1235 long rawContactId = c.getLong(JoinContactQuery._ID);
1236 rawContactIds[i] = rawContactId;
1237 }
1238 } finally {
1239 c.close();
1240 }
1241 return rawContactIds;
1242 }
1243
Brian Attwelld3946ca2015-03-03 11:13:49 -08001244 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1245 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1246 }
1247
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001248 /**
1249 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1250 */
1251 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1252 long rawContactId1, long rawContactId2) {
1253 Builder builder =
1254 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1255 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1256 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1257 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1258 operations.add(builder.build());
1259 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001260
1261 /**
1262 * Shows a toast on the UI thread.
1263 */
1264 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001265 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001266
1267 @Override
1268 public void run() {
1269 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1270 }
1271 });
1272 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001273
1274 private void deliverCallback(final Intent callbackIntent) {
1275 mMainHandler.post(new Runnable() {
1276
1277 @Override
1278 public void run() {
1279 deliverCallbackOnUiThread(callbackIntent);
1280 }
1281 });
1282 }
1283
1284 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1285 // TODO: this assumes that if there are multiple instances of the same
1286 // activity registered, the last one registered is the one waiting for
1287 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001288 for (Listener listener : sListeners) {
1289 if (callbackIntent.getComponent().equals(
1290 ((Activity) listener).getIntent().getComponent())) {
1291 listener.onServiceCompleted(callbackIntent);
1292 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001293 }
1294 }
1295 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001296}