blob: c43941ff5c9f1fff245442e287be2fc1e716dcc7 [file] [log] [blame]
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080017package com.android.contacts;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070018
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080019import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070020import android.app.IntentService;
21import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080022import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070023import android.content.ContentProviderResult;
24import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080025import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070026import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080027import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070028import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080029import android.content.OperationApplicationException;
30import android.database.Cursor;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070031import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080032import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080033import android.os.Handler;
34import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080035import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080036import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070037import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080038import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080039import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080040import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080041import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070042import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080043import android.provider.ContactsContract.Groups;
Yorke Leee8e3fb82013-09-12 17:53:31 -070044import android.provider.ContactsContract.PinnedPositions;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070045import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070046import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070047import android.provider.ContactsContract.RawContactsEntity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070048import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080049import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070050
Chiao Chengd7ca03e2012-10-24 15:14:08 -070051import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080052import com.android.contacts.common.model.AccountTypeManager;
Yorke Leecd321f62013-10-28 15:20:15 -070053import com.android.contacts.common.model.RawContactDelta;
54import com.android.contacts.common.model.RawContactDeltaList;
55import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080056import com.android.contacts.common.model.account.AccountWithDataSet;
Yorke Lee637a38e2013-09-14 08:36:33 -070057import com.android.contacts.util.ContactPhotoUtils;
58
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070059import com.google.common.collect.Lists;
60import com.google.common.collect.Sets;
61
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080062import java.util.ArrayList;
63import java.util.HashSet;
64import java.util.List;
65import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070066
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080067/**
68 * A service responsible for saving changes to the content provider.
69 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070070public class ContactSaveService extends IntentService {
71 private static final String TAG = "ContactSaveService";
72
Katherine Kuana007e442011-07-07 09:25:34 -070073 /** Set to true in order to view logs on content provider operations */
74 private static final boolean DEBUG = false;
75
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070076 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
77
78 public static final String EXTRA_ACCOUNT_NAME = "accountName";
79 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070080 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070081 public static final String EXTRA_CONTENT_VALUES = "contentValues";
82 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
83
Dmitri Plotnikova0114142011-02-15 13:53:21 -080084 public static final String ACTION_SAVE_CONTACT = "saveContact";
85 public static final String EXTRA_CONTACT_STATE = "state";
86 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070087 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070088 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080089 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070090
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080091 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080092 public static final String ACTION_RENAME_GROUP = "renameGroup";
93 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070094 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080095 public static final String EXTRA_GROUP_ID = "groupId";
96 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070097 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
98 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080099
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800100 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800101 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800102 public static final String EXTRA_CONTACT_URI = "contactUri";
103 public static final String EXTRA_STARRED_FLAG = "starred";
104
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800105 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
106 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
107 public static final String EXTRA_DATA_ID = "dataId";
108
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800109 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
110 public static final String EXTRA_CONTACT_ID1 = "contactId1";
111 public static final String EXTRA_CONTACT_ID2 = "contactId2";
112 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
113
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700114 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
115 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
116
117 public static final String ACTION_SET_RINGTONE = "setRingtone";
118 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
119
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700120 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
121 Data.MIMETYPE,
122 Data.IS_PRIMARY,
123 Data.DATA1,
124 Data.DATA2,
125 Data.DATA3,
126 Data.DATA4,
127 Data.DATA5,
128 Data.DATA6,
129 Data.DATA7,
130 Data.DATA8,
131 Data.DATA9,
132 Data.DATA10,
133 Data.DATA11,
134 Data.DATA12,
135 Data.DATA13,
136 Data.DATA14,
137 Data.DATA15
138 );
139
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800140 private static final int PERSIST_TRIES = 3;
141
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800142 public interface Listener {
143 public void onServiceCompleted(Intent callbackIntent);
144 }
145
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100146 private static final CopyOnWriteArrayList<Listener> sListeners =
147 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800148
149 private Handler mMainHandler;
150
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700151 public ContactSaveService() {
152 super(TAG);
153 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800154 mMainHandler = new Handler(Looper.getMainLooper());
155 }
156
157 public static void registerListener(Listener listener) {
158 if (!(listener instanceof Activity)) {
159 throw new ClassCastException("Only activities can be registered to"
160 + " receive callback from " + ContactSaveService.class.getName());
161 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100162 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800163 }
164
165 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100166 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700167 }
168
169 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800170 public Object getSystemService(String name) {
171 Object service = super.getSystemService(name);
172 if (service != null) {
173 return service;
174 }
175
176 return getApplicationContext().getSystemService(name);
177 }
178
179 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700180 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800181 if (intent == null) {
182 Log.d(TAG, "onHandleIntent: could not handle null intent");
183 return;
184 }
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700185 // Call an appropriate method. If we're sure it affects how incoming phone calls are
186 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700187 String action = intent.getAction();
188 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
189 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800190 } else if (ACTION_SAVE_CONTACT.equals(action)) {
191 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800192 } else if (ACTION_CREATE_GROUP.equals(action)) {
193 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800194 } else if (ACTION_RENAME_GROUP.equals(action)) {
195 renameGroup(intent);
196 } else if (ACTION_DELETE_GROUP.equals(action)) {
197 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700198 } else if (ACTION_UPDATE_GROUP.equals(action)) {
199 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800200 } else if (ACTION_SET_STARRED.equals(action)) {
201 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800202 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
203 setSuperPrimary(intent);
204 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
205 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800206 } else if (ACTION_DELETE_CONTACT.equals(action)) {
207 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800208 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
209 joinContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700210 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
211 setSendToVoicemail(intent);
212 } else if (ACTION_SET_RINGTONE.equals(action)) {
213 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700214 }
215 }
216
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800217 /**
218 * Creates an intent that can be sent to this service to create a new raw contact
219 * using data presented as a set of ContentValues.
220 */
221 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700222 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700223 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800224 Intent serviceIntent = new Intent(
225 context, ContactSaveService.class);
226 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
227 if (account != null) {
228 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
229 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700230 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800231 }
232 serviceIntent.putParcelableArrayListExtra(
233 ContactSaveService.EXTRA_CONTENT_VALUES, values);
234
235 // Callback intent will be invoked by the service once the new contact is
236 // created. The service will put the URI of the new contact as "data" on
237 // the callback intent.
238 Intent callbackIntent = new Intent(context, callbackActivity);
239 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800240 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
241 return serviceIntent;
242 }
243
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700244 private void createRawContact(Intent intent) {
245 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
246 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700247 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700248 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
249 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
250
251 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
252 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
253 .withValue(RawContacts.ACCOUNT_NAME, accountName)
254 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700255 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700256 .build());
257
258 int size = valueList.size();
259 for (int i = 0; i < size; i++) {
260 ContentValues values = valueList.get(i);
261 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
262 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
263 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
264 .withValues(values)
265 .build());
266 }
267
268 ContentResolver resolver = getContentResolver();
269 ContentProviderResult[] results;
270 try {
271 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
272 } catch (Exception e) {
273 throw new RuntimeException("Failed to store new contact", e);
274 }
275
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700276 Uri rawContactUri = results[0].uri;
277 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
278
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800279 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700280 }
281
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700282 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800283 * Creates an intent that can be sent to this service to create a new raw contact
284 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800285 * This variant is more convenient to use when there is only one photo that can
286 * possibly be updated, as in the Contact Details screen.
287 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
288 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800289 */
Maurice Chu851222a2012-06-21 11:43:08 -0700290 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700291 String saveModeExtraKey, int saveMode, boolean isProfile,
292 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700293 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800294 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700295 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800296 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
297 callbackActivity, callbackAction, bundle);
298 }
299
300 /**
301 * Creates an intent that can be sent to this service to create a new raw contact
302 * using data presented as a set of ContentValues.
303 * This variant is used when multiple contacts' photos may be updated, as in the
304 * Contact Editor.
305 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
306 */
Maurice Chu851222a2012-06-21 11:43:08 -0700307 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700308 String saveModeExtraKey, int saveMode, boolean isProfile,
309 Class<? extends Activity> callbackActivity, String callbackAction,
310 Bundle updatedPhotos) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800311 Intent serviceIntent = new Intent(
312 context, ContactSaveService.class);
313 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
314 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700315 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Josh Garguse692e012012-01-18 14:53:11 -0800316 if (updatedPhotos != null) {
317 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
318 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800319
Josh Garguse5d3f892012-04-11 11:56:15 -0700320 if (callbackActivity != null) {
321 // Callback intent will be invoked by the service once the contact is
322 // saved. The service will put the URI of the new contact as "data" on
323 // the callback intent.
324 Intent callbackIntent = new Intent(context, callbackActivity);
325 callbackIntent.putExtra(saveModeExtraKey, saveMode);
326 callbackIntent.setAction(callbackAction);
327 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
328 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800329 return serviceIntent;
330 }
331
332 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700333 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700334 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800335 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800336
337 // Trim any empty fields, and RawContacts, before persisting
338 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700339 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800340
341 Uri lookupUri = null;
342
343 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800344 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800345
Josh Gargusef15c8e2012-01-30 16:42:02 -0800346 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
347 long insertedRawContactId = -1;
348
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800349 // Attempt to persist changes
350 int tries = 0;
351 while (tries++ < PERSIST_TRIES) {
352 try {
353 // Build operations and try applying
354 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700355 if (DEBUG) {
356 Log.v(TAG, "Content Provider Operations:");
357 for (ContentProviderOperation operation : diff) {
358 Log.v(TAG, operation.toString());
359 }
360 }
361
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800362 ContentProviderResult[] results = null;
363 if (!diff.isEmpty()) {
364 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
365 }
366
367 final long rawContactId = getRawContactId(state, diff, results);
368 if (rawContactId == -1) {
369 throw new IllegalStateException("Could not determine RawContact ID after save");
370 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800371 // We don't have to check to see if the value is still -1. If we reach here,
372 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
373 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700374 if (isProfile) {
375 // Since the profile supports local raw contacts, which may have been completely
376 // removed if all information was removed, we need to do a special query to
377 // get the lookup URI for the profile contact (if it still exists).
378 Cursor c = resolver.query(Profile.CONTENT_URI,
379 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
380 null, null, null);
381 try {
Erik162b7e32011-09-20 15:23:55 -0700382 if (c.moveToFirst()) {
383 final long contactId = c.getLong(0);
384 final String lookupKey = c.getString(1);
385 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
386 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700387 } finally {
388 c.close();
389 }
390 } else {
391 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
392 rawContactId);
393 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
394 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800395 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
Josh Garguse692e012012-01-18 14:53:11 -0800396
397 // We can change this back to false later, if we fail to save the contact photo.
398 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800399 break;
400
401 } catch (RemoteException e) {
402 // Something went wrong, bail without success
403 Log.e(TAG, "Problem persisting user edits", e);
404 break;
405
Jay Shrauner57fca182014-01-17 14:20:50 -0800406 } catch (IllegalArgumentException e) {
407 // This is thrown by applyBatch on malformed requests
408 Log.e(TAG, "Problem persisting user edits", e);
409 showToast(R.string.contactSavedErrorToast);
410 break;
411
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800412 } catch (OperationApplicationException e) {
413 // Version consistency failed, re-parent change and try again
414 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
415 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
416 boolean first = true;
417 final int count = state.size();
418 for (int i = 0; i < count; i++) {
419 Long rawContactId = state.getRawContactId(i);
420 if (rawContactId != null && rawContactId != -1) {
421 if (!first) {
422 sb.append(',');
423 }
424 sb.append(rawContactId);
425 first = false;
426 }
427 }
428 sb.append(")");
429
430 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800431 throw new IllegalStateException(
432 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800433 }
434
Maurice Chu851222a2012-06-21 11:43:08 -0700435 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700436 isProfile
437 ? RawContactsEntity.PROFILE_CONTENT_URI
438 : RawContactsEntity.CONTENT_URI,
439 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700440 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700441
442 // Update the new state to use profile URIs if appropriate.
443 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700444 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700445 delta.setProfileQueryUri();
446 }
447 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800448 }
449 }
450
Josh Garguse692e012012-01-18 14:53:11 -0800451 // Now save any updated photos. We do this at the end to ensure that
452 // the ContactProvider already knows about newly-created contacts.
453 if (updatedPhotos != null) {
454 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700455 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800456 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800457
458 // If the raw-contact ID is negative, we are saving a new raw-contact;
459 // replace the bogus ID with the new one that we actually saved the contact at.
460 if (rawContactId < 0) {
461 rawContactId = insertedRawContactId;
462 if (rawContactId == -1) {
463 throw new IllegalStateException(
464 "Could not determine RawContact ID for image insertion");
465 }
466 }
467
Yorke Lee637a38e2013-09-14 08:36:33 -0700468 if (!saveUpdatedPhoto(rawContactId, photoUri)) succeeded = false;
Josh Garguse692e012012-01-18 14:53:11 -0800469 }
470 }
471
Josh Garguse5d3f892012-04-11 11:56:15 -0700472 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
473 if (callbackIntent != null) {
474 if (succeeded) {
475 // Mark the intent to indicate that the save was successful (even if the lookup URI
476 // is now null). For local contacts or the local profile, it's possible that the
477 // save triggered removal of the contact, so no lookup URI would exist..
478 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
479 }
480 callbackIntent.setData(lookupUri);
481 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800482 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800483 }
484
Josh Garguse692e012012-01-18 14:53:11 -0800485 /**
486 * Save updated photo for the specified raw-contact.
487 * @return true for success, false for failure
488 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700489 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800490 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800491 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
492 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
493
Yorke Lee637a38e2013-09-14 08:36:33 -0700494 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
Josh Garguse692e012012-01-18 14:53:11 -0800495 }
496
Josh Gargusef15c8e2012-01-30 16:42:02 -0800497 /**
498 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
499 */
Maurice Chu851222a2012-06-21 11:43:08 -0700500 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800501 final ArrayList<ContentProviderOperation> diff,
502 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800503 long existingRawContactId = state.findRawContactId();
504 if (existingRawContactId != -1) {
505 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800506 }
507
Josh Gargusef15c8e2012-01-30 16:42:02 -0800508 return getInsertedRawContactId(diff, results);
509 }
510
511 /**
512 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
513 */
514 private long getInsertedRawContactId(
515 final ArrayList<ContentProviderOperation> diff,
516 final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800517 if (results == null) {
518 return -1;
519 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800520 final int diffSize = diff.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800521 final int numResults = results.length;
522 for (int i = 0; i < diffSize && i < numResults; i++) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800523 ContentProviderOperation operation = diff.get(i);
Brian Attwell13f94e12015-01-22 16:27:48 -0800524 if (operation.isInsert() && operation.getUri().getEncodedPath().contains(
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800525 RawContacts.CONTENT_URI.getEncodedPath())) {
526 return ContentUris.parseId(results[i].uri);
527 }
528 }
529 return -1;
530 }
531
532 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700533 * Creates an intent that can be sent to this service to create a new group as
534 * well as add new members at the same time.
535 *
536 * @param context of the application
537 * @param account in which the group should be created
538 * @param label is the name of the group (cannot be null)
539 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
540 * should be added to the group
541 * @param callbackActivity is the activity to send the callback intent to
542 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700543 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700544 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700545 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700546 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800547 Intent serviceIntent = new Intent(context, ContactSaveService.class);
548 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
549 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
550 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700551 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800552 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700553 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700554
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800555 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700556 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800557 Intent callbackIntent = new Intent(context, callbackActivity);
558 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700559 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800560
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700561 return serviceIntent;
562 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800563
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800564 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700565 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
566 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
567 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
568 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700569 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800570
571 ContentValues values = new ContentValues();
572 values.put(Groups.ACCOUNT_TYPE, accountType);
573 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700574 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800575 values.put(Groups.TITLE, label);
576
Katherine Kuan717e3432011-07-13 17:03:24 -0700577 final ContentResolver resolver = getContentResolver();
578
579 // Create the new group
580 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
581
582 // If there's no URI, then the insertion failed. Abort early because group members can't be
583 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800584 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700585 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800586 return;
587 }
588
Katherine Kuan717e3432011-07-13 17:03:24 -0700589 // Add new group members
590 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
591
592 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
593 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800594 values.clear();
595 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
596 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
597
598 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700599 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700600 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800601 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800602 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800603 }
604
605 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800606 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800607 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700608 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700609 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800610 Intent serviceIntent = new Intent(context, ContactSaveService.class);
611 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
612 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
613 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700614
615 // Callback intent will be invoked by the service once the group is renamed.
616 Intent callbackIntent = new Intent(context, callbackActivity);
617 callbackIntent.setAction(callbackAction);
618 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
619
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800620 return serviceIntent;
621 }
622
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800623 private void renameGroup(Intent intent) {
624 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
625 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
626
627 if (groupId == -1) {
628 Log.e(TAG, "Invalid arguments for renameGroup request");
629 return;
630 }
631
632 ContentValues values = new ContentValues();
633 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700634 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
635 getContentResolver().update(groupUri, values, null, null);
636
637 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
638 callbackIntent.setData(groupUri);
639 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800640 }
641
642 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800643 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800644 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800645 public static Intent createGroupDeletionIntent(Context context, long groupId) {
646 Intent serviceIntent = new Intent(context, ContactSaveService.class);
647 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800648 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800649 return serviceIntent;
650 }
651
652 private void deleteGroup(Intent intent) {
653 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
654 if (groupId == -1) {
655 Log.e(TAG, "Invalid arguments for deleteGroup request");
656 return;
657 }
658
659 getContentResolver().delete(
660 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
661 }
662
663 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700664 * Creates an intent that can be sent to this service to rename a group as
665 * well as add and remove members from the group.
666 *
667 * @param context of the application
668 * @param groupId of the group that should be modified
669 * @param newLabel is the updated name of the group (can be null if the name
670 * should not be updated)
671 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
672 * should be added to the group
673 * @param rawContactsToRemove is an array of raw contact IDs for contacts
674 * that should be removed from the group
675 * @param callbackActivity is the activity to send the callback intent to
676 * @param callbackAction is the intent action for the callback intent
677 */
678 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
679 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700680 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700681 Intent serviceIntent = new Intent(context, ContactSaveService.class);
682 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
683 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
684 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
685 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
686 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
687 rawContactsToRemove);
688
689 // Callback intent will be invoked by the service once the group is updated
690 Intent callbackIntent = new Intent(context, callbackActivity);
691 callbackIntent.setAction(callbackAction);
692 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
693
694 return serviceIntent;
695 }
696
697 private void updateGroup(Intent intent) {
698 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
699 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
700 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
701 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
702
703 if (groupId == -1) {
704 Log.e(TAG, "Invalid arguments for updateGroup request");
705 return;
706 }
707
708 final ContentResolver resolver = getContentResolver();
709 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
710
711 // Update group name if necessary
712 if (label != null) {
713 ContentValues values = new ContentValues();
714 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700715 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700716 }
717
Katherine Kuan717e3432011-07-13 17:03:24 -0700718 // Add and remove members if necessary
719 addMembersToGroup(resolver, rawContactsToAdd, groupId);
720 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
721
722 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
723 callbackIntent.setData(groupUri);
724 deliverCallback(callbackIntent);
725 }
726
Daniel Lehmann18958a22012-02-28 17:45:25 -0800727 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700728 long groupId) {
729 if (rawContactsToAdd == null) {
730 return;
731 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700732 for (long rawContactId : rawContactsToAdd) {
733 try {
734 final ArrayList<ContentProviderOperation> rawContactOperations =
735 new ArrayList<ContentProviderOperation>();
736
737 // Build an assert operation to ensure the contact is not already in the group
738 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
739 .newAssertQuery(Data.CONTENT_URI);
740 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
741 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
742 new String[] { String.valueOf(rawContactId),
743 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
744 assertBuilder.withExpectedCount(0);
745 rawContactOperations.add(assertBuilder.build());
746
747 // Build an insert operation to add the contact to the group
748 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
749 .newInsert(Data.CONTENT_URI);
750 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
751 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
752 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
753 rawContactOperations.add(insertBuilder.build());
754
755 if (DEBUG) {
756 for (ContentProviderOperation operation : rawContactOperations) {
757 Log.v(TAG, operation.toString());
758 }
759 }
760
761 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700762 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800763 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700764 }
765 } catch (RemoteException e) {
766 // Something went wrong, bail without success
767 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
768 String.valueOf(rawContactId), e);
769 } catch (OperationApplicationException e) {
770 // The assert could have failed because the contact is already in the group,
771 // just continue to the next contact
772 Log.w(TAG, "Assert failed in adding raw contact ID " +
773 String.valueOf(rawContactId) + ". Already exists in group " +
774 String.valueOf(groupId), e);
775 }
776 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700777 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700778
Daniel Lehmann18958a22012-02-28 17:45:25 -0800779 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700780 long groupId) {
781 if (rawContactsToRemove == null) {
782 return;
783 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700784 for (long rawContactId : rawContactsToRemove) {
785 // Apply the delete operation on the data row for the given raw contact's
786 // membership in the given group. If no contact matches the provided selection, then
787 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800788 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700789 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
790 new String[] { String.valueOf(rawContactId),
791 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
792 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700793 }
794
795 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800796 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800797 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800798 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
799 Intent serviceIntent = new Intent(context, ContactSaveService.class);
800 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
801 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
802 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
803
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800804 return serviceIntent;
805 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800806
807 private void setStarred(Intent intent) {
808 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
809 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
810 if (contactUri == null) {
811 Log.e(TAG, "Invalid arguments for setStarred request");
812 return;
813 }
814
815 final ContentValues values = new ContentValues(1);
816 values.put(Contacts.STARRED, value);
817 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700818
819 // Undemote the contact if necessary
820 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
821 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800822 if (c == null) {
823 return;
824 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700825 try {
826 if (c.moveToFirst()) {
827 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700828
829 // Don't bother undemoting if this contact is the user's profile.
830 if (id < Profile.MIN_ID) {
Brian Attwell2d88efa2014-12-17 21:49:56 -0800831 PinnedPositions.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700832 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700833 }
834 } finally {
835 c.close();
836 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800837 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800838
839 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700840 * Creates an intent that can be sent to this service to set the redirect to voicemail.
841 */
842 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
843 boolean value) {
844 Intent serviceIntent = new Intent(context, ContactSaveService.class);
845 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
846 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
847 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
848
849 return serviceIntent;
850 }
851
852 private void setSendToVoicemail(Intent intent) {
853 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
854 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
855 if (contactUri == null) {
856 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
857 return;
858 }
859
860 final ContentValues values = new ContentValues(1);
861 values.put(Contacts.SEND_TO_VOICEMAIL, value);
862 getContentResolver().update(contactUri, values, null, null);
863 }
864
865 /**
866 * Creates an intent that can be sent to this service to save the contact's ringtone.
867 */
868 public static Intent createSetRingtone(Context context, Uri contactUri,
869 String value) {
870 Intent serviceIntent = new Intent(context, ContactSaveService.class);
871 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
872 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
873 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
874
875 return serviceIntent;
876 }
877
878 private void setRingtone(Intent intent) {
879 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
880 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
881 if (contactUri == null) {
882 Log.e(TAG, "Invalid arguments for setRingtone");
883 return;
884 }
885 ContentValues values = new ContentValues(1);
886 values.put(Contacts.CUSTOM_RINGTONE, value);
887 getContentResolver().update(contactUri, values, null, null);
888 }
889
890 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800891 * Creates an intent that sets the selected data item as super primary (default)
892 */
893 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
894 Intent serviceIntent = new Intent(context, ContactSaveService.class);
895 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
896 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
897 return serviceIntent;
898 }
899
900 private void setSuperPrimary(Intent intent) {
901 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
902 if (dataId == -1) {
903 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
904 return;
905 }
906
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700907 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800908 }
909
910 /**
911 * Creates an intent that clears the primary flag of all data items that belong to the same
912 * raw_contact as the given data item. Will only clear, if the data item was primary before
913 * this call
914 */
915 public static Intent createClearPrimaryIntent(Context context, long dataId) {
916 Intent serviceIntent = new Intent(context, ContactSaveService.class);
917 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
918 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
919 return serviceIntent;
920 }
921
922 private void clearPrimary(Intent intent) {
923 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
924 if (dataId == -1) {
925 Log.e(TAG, "Invalid arguments for clearPrimary request");
926 return;
927 }
928
929 // Update the primary values in the data record.
930 ContentValues values = new ContentValues(1);
931 values.put(Data.IS_SUPER_PRIMARY, 0);
932 values.put(Data.IS_PRIMARY, 0);
933
934 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
935 values, null, null);
936 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800937
938 /**
939 * Creates an intent that can be sent to this service to delete a contact.
940 */
941 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
942 Intent serviceIntent = new Intent(context, ContactSaveService.class);
943 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
944 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
945 return serviceIntent;
946 }
947
948 private void deleteContact(Intent intent) {
949 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
950 if (contactUri == null) {
951 Log.e(TAG, "Invalid arguments for deleteContact request");
952 return;
953 }
954
955 getContentResolver().delete(contactUri, null, null);
956 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800957
958 /**
959 * Creates an intent that can be sent to this service to join two contacts.
960 */
961 public static Intent createJoinContactsIntent(Context context, long contactId1,
962 long contactId2, boolean contactWritable,
Josh Garguse5d3f892012-04-11 11:56:15 -0700963 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800964 Intent serviceIntent = new Intent(context, ContactSaveService.class);
965 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
966 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
967 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
968 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
969
970 // Callback intent will be invoked by the service once the contacts are joined.
971 Intent callbackIntent = new Intent(context, callbackActivity);
972 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800973 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
974
975 return serviceIntent;
976 }
977
978
979 private interface JoinContactQuery {
980 String[] PROJECTION = {
981 RawContacts._ID,
982 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800983 RawContacts.DISPLAY_NAME_SOURCE,
984 };
985
986 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
987
988 int _ID = 0;
989 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -0800990 int DISPLAY_NAME_SOURCE = 2;
991 }
992
993 private interface ContactEntityQuery {
994 String[] PROJECTION = {
995 Contacts.Entity.DATA_ID,
996 Contacts.Entity.CONTACT_ID,
997 Contacts.Entity.IS_SUPER_PRIMARY,
998 };
999 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1000 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1001 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1002 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1003
1004 int DATA_ID = 0;
1005 int CONTACT_ID = 1;
1006 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001007 }
1008
1009 private void joinContacts(Intent intent) {
1010 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1011 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Brian Attwell548f5c62015-01-27 17:46:46 -08001012
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001013 if (contactId1 == -1 || contactId2 == -1) {
1014 Log.e(TAG, "Invalid arguments for joinContacts request");
1015 return;
1016 }
1017
1018 final ContentResolver resolver = getContentResolver();
1019
1020 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001021 // in the join UIs.
1022 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1023 if (rawContactIds == null) {
1024 // Error.
Jay Shraunerc12a2802014-11-24 10:07:31 -08001025 return;
1026 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001027
Brian Attwell548f5c62015-01-27 17:46:46 -08001028 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001029
1030 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001031 for (int i = 0; i < rawContactIds.length; i++) {
1032 for (int j = 0; j < rawContactIds.length; j++) {
1033 if (i != j) {
1034 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1035 }
1036 }
1037 }
1038
Brian Attwell548f5c62015-01-27 17:46:46 -08001039 // Use the name for contactId1 as the name for the newly aggregated contact.
1040 final Uri contactId1Uri = ContentUris.withAppendedId(
1041 Contacts.CONTENT_URI, contactId1);
1042 final Uri entityUri = Uri.withAppendedPath(
1043 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1044 Cursor c = resolver.query(entityUri,
1045 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1046 if (c == null) {
1047 Log.e(TAG, "Unable to open Contacts DB cursor");
1048 showToast(R.string.contactSavedErrorToast);
1049 return;
1050 }
1051 long dataIdToAddSuperPrimary = -1;
1052 try {
1053 if (c.moveToFirst()) {
1054 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1055 }
1056 } finally {
1057 c.close();
1058 }
1059
1060 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1061 // display name does not change as a result of the join.
1062 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001063 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001064 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1065 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1066 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001067 operations.add(builder.build());
1068 }
1069
1070 boolean success = false;
1071 // Apply all aggregation exceptions as one batch
1072 try {
1073 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001074 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001075 success = true;
1076 } catch (RemoteException e) {
1077 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001078 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001079 } catch (OperationApplicationException e) {
1080 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001081 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001082 }
1083
1084 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1085 if (success) {
1086 Uri uri = RawContacts.getContactLookupUri(resolver,
1087 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1088 callbackIntent.setData(uri);
1089 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001090 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001091 }
1092
Brian Attwell548f5c62015-01-27 17:46:46 -08001093 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1094 final ContentResolver resolver = getContentResolver();
1095 long rawContactIds[];
1096 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1097 JoinContactQuery.PROJECTION,
1098 JoinContactQuery.SELECTION,
1099 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1100 if (c == null) {
1101 Log.e(TAG, "Unable to open Contacts DB cursor");
1102 showToast(R.string.contactSavedErrorToast);
1103 return null;
1104 }
1105 try {
1106 if (c.getCount() < 2) {
1107 Log.e(TAG, "Not enough raw contacts to aggregate toghether.");
1108 return null;
1109 }
1110 rawContactIds = new long[c.getCount()];
1111 for (int i = 0; i < rawContactIds.length; i++) {
1112 c.moveToPosition(i);
1113 long rawContactId = c.getLong(JoinContactQuery._ID);
1114 rawContactIds[i] = rawContactId;
1115 }
1116 } finally {
1117 c.close();
1118 }
1119 return rawContactIds;
1120 }
1121
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001122 /**
1123 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1124 */
1125 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1126 long rawContactId1, long rawContactId2) {
1127 Builder builder =
1128 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1129 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1130 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1131 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1132 operations.add(builder.build());
1133 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001134
1135 /**
1136 * Shows a toast on the UI thread.
1137 */
1138 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001139 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001140
1141 @Override
1142 public void run() {
1143 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1144 }
1145 });
1146 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001147
1148 private void deliverCallback(final Intent callbackIntent) {
1149 mMainHandler.post(new Runnable() {
1150
1151 @Override
1152 public void run() {
1153 deliverCallbackOnUiThread(callbackIntent);
1154 }
1155 });
1156 }
1157
1158 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1159 // TODO: this assumes that if there are multiple instances of the same
1160 // activity registered, the last one registered is the one waiting for
1161 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001162 for (Listener listener : sListeners) {
1163 if (callbackIntent.getComponent().equals(
1164 ((Activity) listener).getIntent().getComponent())) {
1165 listener.onServiceCompleted(callbackIntent);
1166 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001167 }
1168 }
1169 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001170}