blob: 16685218812ab8cbded4f90827062af1c693f976 [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";
Brian Attwelld2962a32015-03-02 14:48:50 -0800102 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800103 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800104 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800105 public static final String EXTRA_STARRED_FLAG = "starred";
106
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800107 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
108 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
109 public static final String EXTRA_DATA_ID = "dataId";
110
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800111 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
112 public static final String EXTRA_CONTACT_ID1 = "contactId1";
113 public static final String EXTRA_CONTACT_ID2 = "contactId2";
114 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
115
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700116 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
117 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
118
119 public static final String ACTION_SET_RINGTONE = "setRingtone";
120 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
121
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700122 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
123 Data.MIMETYPE,
124 Data.IS_PRIMARY,
125 Data.DATA1,
126 Data.DATA2,
127 Data.DATA3,
128 Data.DATA4,
129 Data.DATA5,
130 Data.DATA6,
131 Data.DATA7,
132 Data.DATA8,
133 Data.DATA9,
134 Data.DATA10,
135 Data.DATA11,
136 Data.DATA12,
137 Data.DATA13,
138 Data.DATA14,
139 Data.DATA15
140 );
141
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800142 private static final int PERSIST_TRIES = 3;
143
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800144 public interface Listener {
145 public void onServiceCompleted(Intent callbackIntent);
146 }
147
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100148 private static final CopyOnWriteArrayList<Listener> sListeners =
149 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800150
151 private Handler mMainHandler;
152
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700153 public ContactSaveService() {
154 super(TAG);
155 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800156 mMainHandler = new Handler(Looper.getMainLooper());
157 }
158
159 public static void registerListener(Listener listener) {
160 if (!(listener instanceof Activity)) {
161 throw new ClassCastException("Only activities can be registered to"
162 + " receive callback from " + ContactSaveService.class.getName());
163 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100164 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800165 }
166
167 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100168 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700169 }
170
171 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800172 public Object getSystemService(String name) {
173 Object service = super.getSystemService(name);
174 if (service != null) {
175 return service;
176 }
177
178 return getApplicationContext().getSystemService(name);
179 }
180
181 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700182 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800183 if (intent == null) {
184 Log.d(TAG, "onHandleIntent: could not handle null intent");
185 return;
186 }
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700187 // Call an appropriate method. If we're sure it affects how incoming phone calls are
188 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700189 String action = intent.getAction();
190 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
191 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800192 } else if (ACTION_SAVE_CONTACT.equals(action)) {
193 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800194 } else if (ACTION_CREATE_GROUP.equals(action)) {
195 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800196 } else if (ACTION_RENAME_GROUP.equals(action)) {
197 renameGroup(intent);
198 } else if (ACTION_DELETE_GROUP.equals(action)) {
199 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700200 } else if (ACTION_UPDATE_GROUP.equals(action)) {
201 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800202 } else if (ACTION_SET_STARRED.equals(action)) {
203 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800204 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
205 setSuperPrimary(intent);
206 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
207 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800208 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
209 deleteMultipleContacts(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
Brian Attwelld2962a32015-03-02 14:48:50 -0800952 /**
953 * Creates an intent that can be sent to this service to delete multiple contacts.
954 */
955 public static Intent createDeleteMultipleContactsIntent(Context context,
956 long[] contactIds) {
957 Intent serviceIntent = new Intent(context, ContactSaveService.class);
958 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
959 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
960 return serviceIntent;
961 }
962
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800963 private void deleteContact(Intent intent) {
964 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
965 if (contactUri == null) {
966 Log.e(TAG, "Invalid arguments for deleteContact request");
967 return;
968 }
969
970 getContentResolver().delete(contactUri, null, null);
971 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800972
Brian Attwelld2962a32015-03-02 14:48:50 -0800973 private void deleteMultipleContacts(Intent intent) {
974 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
975 if (contactIds == null) {
976 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
977 return;
978 }
979 for (long contactId : contactIds) {
980 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
981 getContentResolver().delete(contactUri, null, null);
982 }
983
984 }
985
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800986 /**
987 * Creates an intent that can be sent to this service to join two contacts.
988 */
989 public static Intent createJoinContactsIntent(Context context, long contactId1,
990 long contactId2, boolean contactWritable,
Josh Garguse5d3f892012-04-11 11:56:15 -0700991 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800992 Intent serviceIntent = new Intent(context, ContactSaveService.class);
993 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
994 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
995 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
996 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
997
998 // Callback intent will be invoked by the service once the contacts are joined.
999 Intent callbackIntent = new Intent(context, callbackActivity);
1000 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001001 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1002
1003 return serviceIntent;
1004 }
1005
1006
1007 private interface JoinContactQuery {
1008 String[] PROJECTION = {
1009 RawContacts._ID,
1010 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001011 RawContacts.DISPLAY_NAME_SOURCE,
1012 };
1013
1014 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
1015
1016 int _ID = 0;
1017 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001018 int DISPLAY_NAME_SOURCE = 2;
1019 }
1020
1021 private interface ContactEntityQuery {
1022 String[] PROJECTION = {
1023 Contacts.Entity.DATA_ID,
1024 Contacts.Entity.CONTACT_ID,
1025 Contacts.Entity.IS_SUPER_PRIMARY,
1026 };
1027 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1028 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1029 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1030 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1031
1032 int DATA_ID = 0;
1033 int CONTACT_ID = 1;
1034 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001035 }
1036
1037 private void joinContacts(Intent intent) {
1038 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1039 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Brian Attwell548f5c62015-01-27 17:46:46 -08001040
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001041 if (contactId1 == -1 || contactId2 == -1) {
1042 Log.e(TAG, "Invalid arguments for joinContacts request");
1043 return;
1044 }
1045
1046 final ContentResolver resolver = getContentResolver();
1047
1048 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001049 // in the join UIs.
1050 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1051 if (rawContactIds == null) {
1052 // Error.
Jay Shraunerc12a2802014-11-24 10:07:31 -08001053 return;
1054 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001055
Brian Attwell548f5c62015-01-27 17:46:46 -08001056 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001057
1058 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001059 for (int i = 0; i < rawContactIds.length; i++) {
1060 for (int j = 0; j < rawContactIds.length; j++) {
1061 if (i != j) {
1062 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1063 }
1064 }
1065 }
1066
Brian Attwell548f5c62015-01-27 17:46:46 -08001067 // Use the name for contactId1 as the name for the newly aggregated contact.
1068 final Uri contactId1Uri = ContentUris.withAppendedId(
1069 Contacts.CONTENT_URI, contactId1);
1070 final Uri entityUri = Uri.withAppendedPath(
1071 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1072 Cursor c = resolver.query(entityUri,
1073 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1074 if (c == null) {
1075 Log.e(TAG, "Unable to open Contacts DB cursor");
1076 showToast(R.string.contactSavedErrorToast);
1077 return;
1078 }
1079 long dataIdToAddSuperPrimary = -1;
1080 try {
1081 if (c.moveToFirst()) {
1082 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1083 }
1084 } finally {
1085 c.close();
1086 }
1087
1088 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1089 // display name does not change as a result of the join.
1090 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001091 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001092 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1093 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1094 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001095 operations.add(builder.build());
1096 }
1097
1098 boolean success = false;
1099 // Apply all aggregation exceptions as one batch
1100 try {
1101 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001102 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001103 success = true;
1104 } catch (RemoteException e) {
1105 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001106 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001107 } catch (OperationApplicationException e) {
1108 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001109 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001110 }
1111
1112 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1113 if (success) {
1114 Uri uri = RawContacts.getContactLookupUri(resolver,
1115 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1116 callbackIntent.setData(uri);
1117 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001118 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001119 }
1120
Brian Attwell548f5c62015-01-27 17:46:46 -08001121 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1122 final ContentResolver resolver = getContentResolver();
1123 long rawContactIds[];
1124 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1125 JoinContactQuery.PROJECTION,
1126 JoinContactQuery.SELECTION,
1127 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1128 if (c == null) {
1129 Log.e(TAG, "Unable to open Contacts DB cursor");
1130 showToast(R.string.contactSavedErrorToast);
1131 return null;
1132 }
1133 try {
1134 if (c.getCount() < 2) {
1135 Log.e(TAG, "Not enough raw contacts to aggregate toghether.");
1136 return null;
1137 }
1138 rawContactIds = new long[c.getCount()];
1139 for (int i = 0; i < rawContactIds.length; i++) {
1140 c.moveToPosition(i);
1141 long rawContactId = c.getLong(JoinContactQuery._ID);
1142 rawContactIds[i] = rawContactId;
1143 }
1144 } finally {
1145 c.close();
1146 }
1147 return rawContactIds;
1148 }
1149
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001150 /**
1151 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1152 */
1153 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1154 long rawContactId1, long rawContactId2) {
1155 Builder builder =
1156 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1157 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1158 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1159 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1160 operations.add(builder.build());
1161 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001162
1163 /**
1164 * Shows a toast on the UI thread.
1165 */
1166 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001167 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001168
1169 @Override
1170 public void run() {
1171 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1172 }
1173 });
1174 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001175
1176 private void deliverCallback(final Intent callbackIntent) {
1177 mMainHandler.post(new Runnable() {
1178
1179 @Override
1180 public void run() {
1181 deliverCallbackOnUiThread(callbackIntent);
1182 }
1183 });
1184 }
1185
1186 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1187 // TODO: this assumes that if there are multiple instances of the same
1188 // activity registered, the last one registered is the one waiting for
1189 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001190 for (Listener listener : sListeners) {
1191 if (callbackIntent.getComponent().equals(
1192 ((Activity) listener).getIntent().getComponent())) {
1193 listener.onServiceCompleted(callbackIntent);
1194 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001195 }
1196 }
1197 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001198}