blob: d94be060564837ac409eb1824ee613eadd0d8978 [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;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080040import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070041import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080042import android.provider.ContactsContract.Groups;
Yorke Leee8e3fb82013-09-12 17:53:31 -070043import android.provider.ContactsContract.PinnedPositions;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070044import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070045import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070046import android.provider.ContactsContract.RawContactsEntity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070047import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080048import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070049
Chiao Chengd7ca03e2012-10-24 15:14:08 -070050import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080051import com.android.contacts.common.model.AccountTypeManager;
Yorke Leecd321f62013-10-28 15:20:15 -070052import com.android.contacts.common.model.RawContactDelta;
53import com.android.contacts.common.model.RawContactDeltaList;
54import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080055import com.android.contacts.common.model.account.AccountWithDataSet;
Yorke Lee637a38e2013-09-14 08:36:33 -070056import com.android.contacts.util.ContactPhotoUtils;
57
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070058import com.google.common.collect.Lists;
59import com.google.common.collect.Sets;
60
Josh Garguse692e012012-01-18 14:53:11 -080061import java.io.File;
62import java.io.FileInputStream;
63import java.io.FileOutputStream;
64import java.io.IOException;
Yorke Lee637a38e2013-09-14 08:36:33 -070065import java.io.InputStream;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080066import java.util.ArrayList;
67import java.util.HashSet;
68import java.util.List;
69import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070070
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080071/**
72 * A service responsible for saving changes to the content provider.
73 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070074public class ContactSaveService extends IntentService {
75 private static final String TAG = "ContactSaveService";
76
Katherine Kuana007e442011-07-07 09:25:34 -070077 /** Set to true in order to view logs on content provider operations */
78 private static final boolean DEBUG = false;
79
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070080 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
81
82 public static final String EXTRA_ACCOUNT_NAME = "accountName";
83 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070084 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070085 public static final String EXTRA_CONTENT_VALUES = "contentValues";
86 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
87
Dmitri Plotnikova0114142011-02-15 13:53:21 -080088 public static final String ACTION_SAVE_CONTACT = "saveContact";
89 public static final String EXTRA_CONTACT_STATE = "state";
90 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070091 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070092 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080093 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070094
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080095 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080096 public static final String ACTION_RENAME_GROUP = "renameGroup";
97 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070098 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080099 public static final String EXTRA_GROUP_ID = "groupId";
100 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700101 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
102 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800103
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800104 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800105 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800106 public static final String EXTRA_CONTACT_URI = "contactUri";
107 public static final String EXTRA_STARRED_FLAG = "starred";
108
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800109 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
110 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
111 public static final String EXTRA_DATA_ID = "dataId";
112
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800113 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
114 public static final String EXTRA_CONTACT_ID1 = "contactId1";
115 public static final String EXTRA_CONTACT_ID2 = "contactId2";
116 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
117
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700118 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
119 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
120
121 public static final String ACTION_SET_RINGTONE = "setRingtone";
122 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
123
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700124 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
125 Data.MIMETYPE,
126 Data.IS_PRIMARY,
127 Data.DATA1,
128 Data.DATA2,
129 Data.DATA3,
130 Data.DATA4,
131 Data.DATA5,
132 Data.DATA6,
133 Data.DATA7,
134 Data.DATA8,
135 Data.DATA9,
136 Data.DATA10,
137 Data.DATA11,
138 Data.DATA12,
139 Data.DATA13,
140 Data.DATA14,
141 Data.DATA15
142 );
143
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800144 private static final int PERSIST_TRIES = 3;
145
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800146 public interface Listener {
147 public void onServiceCompleted(Intent callbackIntent);
148 }
149
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100150 private static final CopyOnWriteArrayList<Listener> sListeners =
151 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800152
153 private Handler mMainHandler;
154
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700155 public ContactSaveService() {
156 super(TAG);
157 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800158 mMainHandler = new Handler(Looper.getMainLooper());
159 }
160
161 public static void registerListener(Listener listener) {
162 if (!(listener instanceof Activity)) {
163 throw new ClassCastException("Only activities can be registered to"
164 + " receive callback from " + ContactSaveService.class.getName());
165 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100166 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800167 }
168
169 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100170 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700171 }
172
173 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800174 public Object getSystemService(String name) {
175 Object service = super.getSystemService(name);
176 if (service != null) {
177 return service;
178 }
179
180 return getApplicationContext().getSystemService(name);
181 }
182
183 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700184 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800185 if (intent == null) {
186 Log.d(TAG, "onHandleIntent: could not handle null intent");
187 return;
188 }
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700189 // Call an appropriate method. If we're sure it affects how incoming phone calls are
190 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700191 String action = intent.getAction();
192 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
193 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800194 } else if (ACTION_SAVE_CONTACT.equals(action)) {
195 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800196 } else if (ACTION_CREATE_GROUP.equals(action)) {
197 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800198 } else if (ACTION_RENAME_GROUP.equals(action)) {
199 renameGroup(intent);
200 } else if (ACTION_DELETE_GROUP.equals(action)) {
201 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700202 } else if (ACTION_UPDATE_GROUP.equals(action)) {
203 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800204 } else if (ACTION_SET_STARRED.equals(action)) {
205 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800206 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
207 setSuperPrimary(intent);
208 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
209 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800210 } else if (ACTION_DELETE_CONTACT.equals(action)) {
211 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800212 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
213 joinContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700214 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
215 setSendToVoicemail(intent);
216 } else if (ACTION_SET_RINGTONE.equals(action)) {
217 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700218 }
219 }
220
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800221 /**
222 * Creates an intent that can be sent to this service to create a new raw contact
223 * using data presented as a set of ContentValues.
224 */
225 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700226 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700227 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800228 Intent serviceIntent = new Intent(
229 context, ContactSaveService.class);
230 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
231 if (account != null) {
232 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
233 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700234 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800235 }
236 serviceIntent.putParcelableArrayListExtra(
237 ContactSaveService.EXTRA_CONTENT_VALUES, values);
238
239 // Callback intent will be invoked by the service once the new contact is
240 // created. The service will put the URI of the new contact as "data" on
241 // the callback intent.
242 Intent callbackIntent = new Intent(context, callbackActivity);
243 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800244 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
245 return serviceIntent;
246 }
247
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700248 private void createRawContact(Intent intent) {
249 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
250 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700251 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700252 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
253 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
254
255 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
256 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
257 .withValue(RawContacts.ACCOUNT_NAME, accountName)
258 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700259 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700260 .build());
261
262 int size = valueList.size();
263 for (int i = 0; i < size; i++) {
264 ContentValues values = valueList.get(i);
265 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
266 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
267 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
268 .withValues(values)
269 .build());
270 }
271
272 ContentResolver resolver = getContentResolver();
273 ContentProviderResult[] results;
274 try {
275 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
276 } catch (Exception e) {
277 throw new RuntimeException("Failed to store new contact", e);
278 }
279
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700280 Uri rawContactUri = results[0].uri;
281 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
282
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800283 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700284 }
285
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700286 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800287 * Creates an intent that can be sent to this service to create a new raw contact
288 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800289 * This variant is more convenient to use when there is only one photo that can
290 * possibly be updated, as in the Contact Details screen.
291 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
292 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800293 */
Maurice Chu851222a2012-06-21 11:43:08 -0700294 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700295 String saveModeExtraKey, int saveMode, boolean isProfile,
296 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700297 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800298 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700299 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800300 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
301 callbackActivity, callbackAction, bundle);
302 }
303
304 /**
305 * Creates an intent that can be sent to this service to create a new raw contact
306 * using data presented as a set of ContentValues.
307 * This variant is used when multiple contacts' photos may be updated, as in the
308 * Contact Editor.
309 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
310 */
Maurice Chu851222a2012-06-21 11:43:08 -0700311 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700312 String saveModeExtraKey, int saveMode, boolean isProfile,
313 Class<? extends Activity> callbackActivity, String callbackAction,
314 Bundle updatedPhotos) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800315 Intent serviceIntent = new Intent(
316 context, ContactSaveService.class);
317 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
318 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700319 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Josh Garguse692e012012-01-18 14:53:11 -0800320 if (updatedPhotos != null) {
321 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
322 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800323
Josh Garguse5d3f892012-04-11 11:56:15 -0700324 if (callbackActivity != null) {
325 // Callback intent will be invoked by the service once the contact is
326 // saved. The service will put the URI of the new contact as "data" on
327 // the callback intent.
328 Intent callbackIntent = new Intent(context, callbackActivity);
329 callbackIntent.putExtra(saveModeExtraKey, saveMode);
330 callbackIntent.setAction(callbackAction);
331 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
332 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800333 return serviceIntent;
334 }
335
336 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700337 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700338 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800339 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800340
341 // Trim any empty fields, and RawContacts, before persisting
342 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700343 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800344
345 Uri lookupUri = null;
346
347 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800348 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800349
Josh Gargusef15c8e2012-01-30 16:42:02 -0800350 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
351 long insertedRawContactId = -1;
352
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800353 // Attempt to persist changes
354 int tries = 0;
355 while (tries++ < PERSIST_TRIES) {
356 try {
357 // Build operations and try applying
358 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700359 if (DEBUG) {
360 Log.v(TAG, "Content Provider Operations:");
361 for (ContentProviderOperation operation : diff) {
362 Log.v(TAG, operation.toString());
363 }
364 }
365
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800366 ContentProviderResult[] results = null;
367 if (!diff.isEmpty()) {
368 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
369 }
370
371 final long rawContactId = getRawContactId(state, diff, results);
372 if (rawContactId == -1) {
373 throw new IllegalStateException("Could not determine RawContact ID after save");
374 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800375 // We don't have to check to see if the value is still -1. If we reach here,
376 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
377 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700378 if (isProfile) {
379 // Since the profile supports local raw contacts, which may have been completely
380 // removed if all information was removed, we need to do a special query to
381 // get the lookup URI for the profile contact (if it still exists).
382 Cursor c = resolver.query(Profile.CONTENT_URI,
383 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
384 null, null, null);
385 try {
Erik162b7e32011-09-20 15:23:55 -0700386 if (c.moveToFirst()) {
387 final long contactId = c.getLong(0);
388 final String lookupKey = c.getString(1);
389 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
390 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700391 } finally {
392 c.close();
393 }
394 } else {
395 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
396 rawContactId);
397 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
398 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800399 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
Josh Garguse692e012012-01-18 14:53:11 -0800400
401 // We can change this back to false later, if we fail to save the contact photo.
402 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800403 break;
404
405 } catch (RemoteException e) {
406 // Something went wrong, bail without success
407 Log.e(TAG, "Problem persisting user edits", e);
408 break;
409
Jay Shrauner57fca182014-01-17 14:20:50 -0800410 } catch (IllegalArgumentException e) {
411 // This is thrown by applyBatch on malformed requests
412 Log.e(TAG, "Problem persisting user edits", e);
413 showToast(R.string.contactSavedErrorToast);
414 break;
415
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800416 } catch (OperationApplicationException e) {
417 // Version consistency failed, re-parent change and try again
418 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
419 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
420 boolean first = true;
421 final int count = state.size();
422 for (int i = 0; i < count; i++) {
423 Long rawContactId = state.getRawContactId(i);
424 if (rawContactId != null && rawContactId != -1) {
425 if (!first) {
426 sb.append(',');
427 }
428 sb.append(rawContactId);
429 first = false;
430 }
431 }
432 sb.append(")");
433
434 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800435 throw new IllegalStateException(
436 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800437 }
438
Maurice Chu851222a2012-06-21 11:43:08 -0700439 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700440 isProfile
441 ? RawContactsEntity.PROFILE_CONTENT_URI
442 : RawContactsEntity.CONTENT_URI,
443 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700444 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700445
446 // Update the new state to use profile URIs if appropriate.
447 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700448 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700449 delta.setProfileQueryUri();
450 }
451 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800452 }
453 }
454
Josh Garguse692e012012-01-18 14:53:11 -0800455 // Now save any updated photos. We do this at the end to ensure that
456 // the ContactProvider already knows about newly-created contacts.
457 if (updatedPhotos != null) {
458 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700459 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800460 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800461
462 // If the raw-contact ID is negative, we are saving a new raw-contact;
463 // replace the bogus ID with the new one that we actually saved the contact at.
464 if (rawContactId < 0) {
465 rawContactId = insertedRawContactId;
466 if (rawContactId == -1) {
467 throw new IllegalStateException(
468 "Could not determine RawContact ID for image insertion");
469 }
470 }
471
Yorke Lee637a38e2013-09-14 08:36:33 -0700472 if (!saveUpdatedPhoto(rawContactId, photoUri)) succeeded = false;
Josh Garguse692e012012-01-18 14:53:11 -0800473 }
474 }
475
Josh Garguse5d3f892012-04-11 11:56:15 -0700476 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
477 if (callbackIntent != null) {
478 if (succeeded) {
479 // Mark the intent to indicate that the save was successful (even if the lookup URI
480 // is now null). For local contacts or the local profile, it's possible that the
481 // save triggered removal of the contact, so no lookup URI would exist..
482 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
483 }
484 callbackIntent.setData(lookupUri);
485 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800486 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800487 }
488
Josh Garguse692e012012-01-18 14:53:11 -0800489 /**
490 * Save updated photo for the specified raw-contact.
491 * @return true for success, false for failure
492 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700493 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800494 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800495 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
496 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
497
Yorke Lee637a38e2013-09-14 08:36:33 -0700498 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
Josh Garguse692e012012-01-18 14:53:11 -0800499 }
500
Josh Gargusef15c8e2012-01-30 16:42:02 -0800501 /**
502 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
503 */
Maurice Chu851222a2012-06-21 11:43:08 -0700504 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800505 final ArrayList<ContentProviderOperation> diff,
506 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800507 long existingRawContactId = state.findRawContactId();
508 if (existingRawContactId != -1) {
509 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800510 }
511
Josh Gargusef15c8e2012-01-30 16:42:02 -0800512 return getInsertedRawContactId(diff, results);
513 }
514
515 /**
516 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
517 */
518 private long getInsertedRawContactId(
519 final ArrayList<ContentProviderOperation> diff,
520 final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800521 if (results == null) {
522 return -1;
523 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800524 final int diffSize = diff.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800525 final int numResults = results.length;
526 for (int i = 0; i < diffSize && i < numResults; i++) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800527 ContentProviderOperation operation = diff.get(i);
Brian Attwell13f94e12015-01-22 16:27:48 -0800528 if (operation.isInsert() && operation.getUri().getEncodedPath().contains(
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800529 RawContacts.CONTENT_URI.getEncodedPath())) {
530 return ContentUris.parseId(results[i].uri);
531 }
532 }
533 return -1;
534 }
535
536 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700537 * Creates an intent that can be sent to this service to create a new group as
538 * well as add new members at the same time.
539 *
540 * @param context of the application
541 * @param account in which the group should be created
542 * @param label is the name of the group (cannot be null)
543 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
544 * should be added to the group
545 * @param callbackActivity is the activity to send the callback intent to
546 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700547 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700548 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700549 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700550 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800551 Intent serviceIntent = new Intent(context, ContactSaveService.class);
552 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
553 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
554 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700555 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800556 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700557 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700558
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800559 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700560 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800561 Intent callbackIntent = new Intent(context, callbackActivity);
562 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700563 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800564
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700565 return serviceIntent;
566 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800567
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800568 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700569 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
570 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
571 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
572 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700573 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800574
575 ContentValues values = new ContentValues();
576 values.put(Groups.ACCOUNT_TYPE, accountType);
577 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700578 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800579 values.put(Groups.TITLE, label);
580
Katherine Kuan717e3432011-07-13 17:03:24 -0700581 final ContentResolver resolver = getContentResolver();
582
583 // Create the new group
584 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
585
586 // If there's no URI, then the insertion failed. Abort early because group members can't be
587 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800588 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700589 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800590 return;
591 }
592
Katherine Kuan717e3432011-07-13 17:03:24 -0700593 // Add new group members
594 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
595
596 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
597 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800598 values.clear();
599 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
600 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
601
602 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700603 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700604 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800605 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800606 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800607 }
608
609 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800610 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800611 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700612 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700613 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800614 Intent serviceIntent = new Intent(context, ContactSaveService.class);
615 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
616 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
617 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700618
619 // Callback intent will be invoked by the service once the group is renamed.
620 Intent callbackIntent = new Intent(context, callbackActivity);
621 callbackIntent.setAction(callbackAction);
622 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
623
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800624 return serviceIntent;
625 }
626
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800627 private void renameGroup(Intent intent) {
628 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
629 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
630
631 if (groupId == -1) {
632 Log.e(TAG, "Invalid arguments for renameGroup request");
633 return;
634 }
635
636 ContentValues values = new ContentValues();
637 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700638 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
639 getContentResolver().update(groupUri, values, null, null);
640
641 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
642 callbackIntent.setData(groupUri);
643 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800644 }
645
646 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800647 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800648 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800649 public static Intent createGroupDeletionIntent(Context context, long groupId) {
650 Intent serviceIntent = new Intent(context, ContactSaveService.class);
651 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800652 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800653 return serviceIntent;
654 }
655
656 private void deleteGroup(Intent intent) {
657 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
658 if (groupId == -1) {
659 Log.e(TAG, "Invalid arguments for deleteGroup request");
660 return;
661 }
662
663 getContentResolver().delete(
664 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
665 }
666
667 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700668 * Creates an intent that can be sent to this service to rename a group as
669 * well as add and remove members from the group.
670 *
671 * @param context of the application
672 * @param groupId of the group that should be modified
673 * @param newLabel is the updated name of the group (can be null if the name
674 * should not be updated)
675 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
676 * should be added to the group
677 * @param rawContactsToRemove is an array of raw contact IDs for contacts
678 * that should be removed from the group
679 * @param callbackActivity is the activity to send the callback intent to
680 * @param callbackAction is the intent action for the callback intent
681 */
682 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
683 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700684 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700685 Intent serviceIntent = new Intent(context, ContactSaveService.class);
686 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
687 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
688 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
689 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
690 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
691 rawContactsToRemove);
692
693 // Callback intent will be invoked by the service once the group is updated
694 Intent callbackIntent = new Intent(context, callbackActivity);
695 callbackIntent.setAction(callbackAction);
696 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
697
698 return serviceIntent;
699 }
700
701 private void updateGroup(Intent intent) {
702 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
703 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
704 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
705 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
706
707 if (groupId == -1) {
708 Log.e(TAG, "Invalid arguments for updateGroup request");
709 return;
710 }
711
712 final ContentResolver resolver = getContentResolver();
713 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
714
715 // Update group name if necessary
716 if (label != null) {
717 ContentValues values = new ContentValues();
718 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700719 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700720 }
721
Katherine Kuan717e3432011-07-13 17:03:24 -0700722 // Add and remove members if necessary
723 addMembersToGroup(resolver, rawContactsToAdd, groupId);
724 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
725
726 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
727 callbackIntent.setData(groupUri);
728 deliverCallback(callbackIntent);
729 }
730
Daniel Lehmann18958a22012-02-28 17:45:25 -0800731 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700732 long groupId) {
733 if (rawContactsToAdd == null) {
734 return;
735 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700736 for (long rawContactId : rawContactsToAdd) {
737 try {
738 final ArrayList<ContentProviderOperation> rawContactOperations =
739 new ArrayList<ContentProviderOperation>();
740
741 // Build an assert operation to ensure the contact is not already in the group
742 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
743 .newAssertQuery(Data.CONTENT_URI);
744 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
745 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
746 new String[] { String.valueOf(rawContactId),
747 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
748 assertBuilder.withExpectedCount(0);
749 rawContactOperations.add(assertBuilder.build());
750
751 // Build an insert operation to add the contact to the group
752 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
753 .newInsert(Data.CONTENT_URI);
754 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
755 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
756 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
757 rawContactOperations.add(insertBuilder.build());
758
759 if (DEBUG) {
760 for (ContentProviderOperation operation : rawContactOperations) {
761 Log.v(TAG, operation.toString());
762 }
763 }
764
765 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700766 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800767 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700768 }
769 } catch (RemoteException e) {
770 // Something went wrong, bail without success
771 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
772 String.valueOf(rawContactId), e);
773 } catch (OperationApplicationException e) {
774 // The assert could have failed because the contact is already in the group,
775 // just continue to the next contact
776 Log.w(TAG, "Assert failed in adding raw contact ID " +
777 String.valueOf(rawContactId) + ". Already exists in group " +
778 String.valueOf(groupId), e);
779 }
780 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700781 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700782
Daniel Lehmann18958a22012-02-28 17:45:25 -0800783 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700784 long groupId) {
785 if (rawContactsToRemove == null) {
786 return;
787 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700788 for (long rawContactId : rawContactsToRemove) {
789 // Apply the delete operation on the data row for the given raw contact's
790 // membership in the given group. If no contact matches the provided selection, then
791 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800792 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700793 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
794 new String[] { String.valueOf(rawContactId),
795 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
796 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700797 }
798
799 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800800 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800801 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800802 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
803 Intent serviceIntent = new Intent(context, ContactSaveService.class);
804 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
805 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
806 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
807
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800808 return serviceIntent;
809 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800810
811 private void setStarred(Intent intent) {
812 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
813 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
814 if (contactUri == null) {
815 Log.e(TAG, "Invalid arguments for setStarred request");
816 return;
817 }
818
819 final ContentValues values = new ContentValues(1);
820 values.put(Contacts.STARRED, value);
821 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700822
823 // Undemote the contact if necessary
824 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
825 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800826 if (c == null) {
827 return;
828 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700829 try {
830 if (c.moveToFirst()) {
831 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700832
833 // Don't bother undemoting if this contact is the user's profile.
834 if (id < Profile.MIN_ID) {
Brian Attwell2d88efa2014-12-17 21:49:56 -0800835 PinnedPositions.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700836 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700837 }
838 } finally {
839 c.close();
840 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800841 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800842
843 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700844 * Creates an intent that can be sent to this service to set the redirect to voicemail.
845 */
846 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
847 boolean value) {
848 Intent serviceIntent = new Intent(context, ContactSaveService.class);
849 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
850 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
851 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
852
853 return serviceIntent;
854 }
855
856 private void setSendToVoicemail(Intent intent) {
857 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
858 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
859 if (contactUri == null) {
860 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
861 return;
862 }
863
864 final ContentValues values = new ContentValues(1);
865 values.put(Contacts.SEND_TO_VOICEMAIL, value);
866 getContentResolver().update(contactUri, values, null, null);
867 }
868
869 /**
870 * Creates an intent that can be sent to this service to save the contact's ringtone.
871 */
872 public static Intent createSetRingtone(Context context, Uri contactUri,
873 String value) {
874 Intent serviceIntent = new Intent(context, ContactSaveService.class);
875 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
876 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
877 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
878
879 return serviceIntent;
880 }
881
882 private void setRingtone(Intent intent) {
883 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
884 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
885 if (contactUri == null) {
886 Log.e(TAG, "Invalid arguments for setRingtone");
887 return;
888 }
889 ContentValues values = new ContentValues(1);
890 values.put(Contacts.CUSTOM_RINGTONE, value);
891 getContentResolver().update(contactUri, values, null, null);
892 }
893
894 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800895 * Creates an intent that sets the selected data item as super primary (default)
896 */
897 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
898 Intent serviceIntent = new Intent(context, ContactSaveService.class);
899 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
900 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
901 return serviceIntent;
902 }
903
904 private void setSuperPrimary(Intent intent) {
905 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
906 if (dataId == -1) {
907 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
908 return;
909 }
910
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700911 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800912 }
913
914 /**
915 * Creates an intent that clears the primary flag of all data items that belong to the same
916 * raw_contact as the given data item. Will only clear, if the data item was primary before
917 * this call
918 */
919 public static Intent createClearPrimaryIntent(Context context, long dataId) {
920 Intent serviceIntent = new Intent(context, ContactSaveService.class);
921 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
922 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
923 return serviceIntent;
924 }
925
926 private void clearPrimary(Intent intent) {
927 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
928 if (dataId == -1) {
929 Log.e(TAG, "Invalid arguments for clearPrimary request");
930 return;
931 }
932
933 // Update the primary values in the data record.
934 ContentValues values = new ContentValues(1);
935 values.put(Data.IS_SUPER_PRIMARY, 0);
936 values.put(Data.IS_PRIMARY, 0);
937
938 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
939 values, null, null);
940 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800941
942 /**
943 * Creates an intent that can be sent to this service to delete a contact.
944 */
945 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
946 Intent serviceIntent = new Intent(context, ContactSaveService.class);
947 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
948 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
949 return serviceIntent;
950 }
951
952 private void deleteContact(Intent intent) {
953 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
954 if (contactUri == null) {
955 Log.e(TAG, "Invalid arguments for deleteContact request");
956 return;
957 }
958
959 getContentResolver().delete(contactUri, null, null);
960 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800961
962 /**
963 * Creates an intent that can be sent to this service to join two contacts.
964 */
965 public static Intent createJoinContactsIntent(Context context, long contactId1,
966 long contactId2, boolean contactWritable,
Josh Garguse5d3f892012-04-11 11:56:15 -0700967 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800968 Intent serviceIntent = new Intent(context, ContactSaveService.class);
969 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
970 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
971 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
972 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
973
974 // Callback intent will be invoked by the service once the contacts are joined.
975 Intent callbackIntent = new Intent(context, callbackActivity);
976 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800977 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
978
979 return serviceIntent;
980 }
981
982
983 private interface JoinContactQuery {
984 String[] PROJECTION = {
985 RawContacts._ID,
986 RawContacts.CONTACT_ID,
987 RawContacts.NAME_VERIFIED,
988 RawContacts.DISPLAY_NAME_SOURCE,
989 };
990
991 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
992
993 int _ID = 0;
994 int CONTACT_ID = 1;
995 int NAME_VERIFIED = 2;
996 int DISPLAY_NAME_SOURCE = 3;
997 }
998
999 private void joinContacts(Intent intent) {
1000 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1001 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
1002 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
1003 if (contactId1 == -1 || contactId2 == -1) {
1004 Log.e(TAG, "Invalid arguments for joinContacts request");
1005 return;
1006 }
1007
1008 final ContentResolver resolver = getContentResolver();
1009
1010 // Load raw contact IDs for all raw contacts involved - currently edited and selected
1011 // in the join UIs
1012 Cursor c = resolver.query(RawContacts.CONTENT_URI,
1013 JoinContactQuery.PROJECTION,
1014 JoinContactQuery.SELECTION,
1015 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -08001016 if (c == null) {
1017 Log.e(TAG, "Unable to open Contacts DB cursor");
1018 showToast(R.string.contactSavedErrorToast);
1019 return;
1020 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001021
1022 long rawContactIds[];
1023 long verifiedNameRawContactId = -1;
1024 try {
Jay Shrauner7f42b902013-01-09 14:43:09 -08001025 if (c.getCount() == 0) {
1026 return;
1027 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001028 int maxDisplayNameSource = -1;
1029 rawContactIds = new long[c.getCount()];
1030 for (int i = 0; i < rawContactIds.length; i++) {
1031 c.moveToPosition(i);
1032 long rawContactId = c.getLong(JoinContactQuery._ID);
1033 rawContactIds[i] = rawContactId;
1034 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1035 if (nameSource > maxDisplayNameSource) {
1036 maxDisplayNameSource = nameSource;
1037 }
1038 }
1039
1040 // Find an appropriate display name for the joined contact:
1041 // if should have a higher DisplayNameSource or be the name
1042 // of the original contact that we are joining with another.
1043 if (writable) {
1044 for (int i = 0; i < rawContactIds.length; i++) {
1045 c.moveToPosition(i);
1046 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1047 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1048 if (nameSource == maxDisplayNameSource
1049 && (verifiedNameRawContactId == -1
1050 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1051 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1052 }
1053 }
1054 }
1055 }
1056 } finally {
1057 c.close();
1058 }
1059
1060 // For each pair of raw contacts, insert an aggregation exception
1061 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1062 for (int i = 0; i < rawContactIds.length; i++) {
1063 for (int j = 0; j < rawContactIds.length; j++) {
1064 if (i != j) {
1065 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1066 }
1067 }
1068 }
1069
1070 // Mark the original contact as "name verified" to make sure that the contact
1071 // display name does not change as a result of the join
1072 if (verifiedNameRawContactId != -1) {
1073 Builder builder = ContentProviderOperation.newUpdate(
1074 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1075 builder.withValue(RawContacts.NAME_VERIFIED, 1);
1076 operations.add(builder.build());
1077 }
1078
1079 boolean success = false;
1080 // Apply all aggregation exceptions as one batch
1081 try {
1082 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001083 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001084 success = true;
1085 } catch (RemoteException e) {
1086 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001087 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001088 } catch (OperationApplicationException e) {
1089 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001090 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001091 }
1092
1093 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1094 if (success) {
1095 Uri uri = RawContacts.getContactLookupUri(resolver,
1096 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1097 callbackIntent.setData(uri);
1098 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001099 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001100 }
1101
1102 /**
1103 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1104 */
1105 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1106 long rawContactId1, long rawContactId2) {
1107 Builder builder =
1108 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1109 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1110 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1111 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1112 operations.add(builder.build());
1113 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001114
1115 /**
1116 * Shows a toast on the UI thread.
1117 */
1118 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001119 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001120
1121 @Override
1122 public void run() {
1123 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1124 }
1125 });
1126 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001127
1128 private void deliverCallback(final Intent callbackIntent) {
1129 mMainHandler.post(new Runnable() {
1130
1131 @Override
1132 public void run() {
1133 deliverCallbackOnUiThread(callbackIntent);
1134 }
1135 });
1136 }
1137
1138 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1139 // TODO: this assumes that if there are multiple instances of the same
1140 // activity registered, the last one registered is the one waiting for
1141 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001142 for (Listener listener : sListeners) {
1143 if (callbackIntent.getComponent().equals(
1144 ((Activity) listener).getIntent().getComponent())) {
1145 listener.onServiceCompleted(callbackIntent);
1146 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001147 }
1148 }
1149 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001150}