blob: 7d355cda4f4ab1c9795988aa6e03f600b64bd100 [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);
528 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
529 && operation.getUri().getEncodedPath().contains(
530 RawContacts.CONTENT_URI.getEncodedPath())) {
531 return ContentUris.parseId(results[i].uri);
532 }
533 }
534 return -1;
535 }
536
537 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700538 * Creates an intent that can be sent to this service to create a new group as
539 * well as add new members at the same time.
540 *
541 * @param context of the application
542 * @param account in which the group should be created
543 * @param label is the name of the group (cannot be null)
544 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
545 * should be added to the group
546 * @param callbackActivity is the activity to send the callback intent to
547 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700548 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700549 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700550 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700551 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800552 Intent serviceIntent = new Intent(context, ContactSaveService.class);
553 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
554 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
555 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700556 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800557 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700558 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700559
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800560 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700561 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800562 Intent callbackIntent = new Intent(context, callbackActivity);
563 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700564 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800565
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700566 return serviceIntent;
567 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800568
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800569 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700570 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
571 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
572 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
573 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700574 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800575
576 ContentValues values = new ContentValues();
577 values.put(Groups.ACCOUNT_TYPE, accountType);
578 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700579 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800580 values.put(Groups.TITLE, label);
581
Katherine Kuan717e3432011-07-13 17:03:24 -0700582 final ContentResolver resolver = getContentResolver();
583
584 // Create the new group
585 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
586
587 // If there's no URI, then the insertion failed. Abort early because group members can't be
588 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800589 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700590 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800591 return;
592 }
593
Katherine Kuan717e3432011-07-13 17:03:24 -0700594 // Add new group members
595 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
596
597 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
598 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800599 values.clear();
600 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
601 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
602
603 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700604 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700605 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800606 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800607 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800608 }
609
610 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800611 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800612 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700613 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700614 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800615 Intent serviceIntent = new Intent(context, ContactSaveService.class);
616 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
617 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
618 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700619
620 // Callback intent will be invoked by the service once the group is renamed.
621 Intent callbackIntent = new Intent(context, callbackActivity);
622 callbackIntent.setAction(callbackAction);
623 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
624
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800625 return serviceIntent;
626 }
627
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800628 private void renameGroup(Intent intent) {
629 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
630 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
631
632 if (groupId == -1) {
633 Log.e(TAG, "Invalid arguments for renameGroup request");
634 return;
635 }
636
637 ContentValues values = new ContentValues();
638 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700639 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
640 getContentResolver().update(groupUri, values, null, null);
641
642 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
643 callbackIntent.setData(groupUri);
644 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800645 }
646
647 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800648 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800649 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800650 public static Intent createGroupDeletionIntent(Context context, long groupId) {
651 Intent serviceIntent = new Intent(context, ContactSaveService.class);
652 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800653 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800654 return serviceIntent;
655 }
656
657 private void deleteGroup(Intent intent) {
658 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
659 if (groupId == -1) {
660 Log.e(TAG, "Invalid arguments for deleteGroup request");
661 return;
662 }
663
664 getContentResolver().delete(
665 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
666 }
667
668 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700669 * Creates an intent that can be sent to this service to rename a group as
670 * well as add and remove members from the group.
671 *
672 * @param context of the application
673 * @param groupId of the group that should be modified
674 * @param newLabel is the updated name of the group (can be null if the name
675 * should not be updated)
676 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
677 * should be added to the group
678 * @param rawContactsToRemove is an array of raw contact IDs for contacts
679 * that should be removed from the group
680 * @param callbackActivity is the activity to send the callback intent to
681 * @param callbackAction is the intent action for the callback intent
682 */
683 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
684 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700685 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700686 Intent serviceIntent = new Intent(context, ContactSaveService.class);
687 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
688 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
689 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
690 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
691 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
692 rawContactsToRemove);
693
694 // Callback intent will be invoked by the service once the group is updated
695 Intent callbackIntent = new Intent(context, callbackActivity);
696 callbackIntent.setAction(callbackAction);
697 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
698
699 return serviceIntent;
700 }
701
702 private void updateGroup(Intent intent) {
703 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
704 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
705 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
706 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
707
708 if (groupId == -1) {
709 Log.e(TAG, "Invalid arguments for updateGroup request");
710 return;
711 }
712
713 final ContentResolver resolver = getContentResolver();
714 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
715
716 // Update group name if necessary
717 if (label != null) {
718 ContentValues values = new ContentValues();
719 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700720 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700721 }
722
Katherine Kuan717e3432011-07-13 17:03:24 -0700723 // Add and remove members if necessary
724 addMembersToGroup(resolver, rawContactsToAdd, groupId);
725 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
726
727 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
728 callbackIntent.setData(groupUri);
729 deliverCallback(callbackIntent);
730 }
731
Daniel Lehmann18958a22012-02-28 17:45:25 -0800732 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700733 long groupId) {
734 if (rawContactsToAdd == null) {
735 return;
736 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700737 for (long rawContactId : rawContactsToAdd) {
738 try {
739 final ArrayList<ContentProviderOperation> rawContactOperations =
740 new ArrayList<ContentProviderOperation>();
741
742 // Build an assert operation to ensure the contact is not already in the group
743 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
744 .newAssertQuery(Data.CONTENT_URI);
745 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
746 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
747 new String[] { String.valueOf(rawContactId),
748 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
749 assertBuilder.withExpectedCount(0);
750 rawContactOperations.add(assertBuilder.build());
751
752 // Build an insert operation to add the contact to the group
753 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
754 .newInsert(Data.CONTENT_URI);
755 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
756 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
757 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
758 rawContactOperations.add(insertBuilder.build());
759
760 if (DEBUG) {
761 for (ContentProviderOperation operation : rawContactOperations) {
762 Log.v(TAG, operation.toString());
763 }
764 }
765
766 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700767 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800768 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700769 }
770 } catch (RemoteException e) {
771 // Something went wrong, bail without success
772 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
773 String.valueOf(rawContactId), e);
774 } catch (OperationApplicationException e) {
775 // The assert could have failed because the contact is already in the group,
776 // just continue to the next contact
777 Log.w(TAG, "Assert failed in adding raw contact ID " +
778 String.valueOf(rawContactId) + ". Already exists in group " +
779 String.valueOf(groupId), e);
780 }
781 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700782 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700783
Daniel Lehmann18958a22012-02-28 17:45:25 -0800784 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700785 long groupId) {
786 if (rawContactsToRemove == null) {
787 return;
788 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700789 for (long rawContactId : rawContactsToRemove) {
790 // Apply the delete operation on the data row for the given raw contact's
791 // membership in the given group. If no contact matches the provided selection, then
792 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800793 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700794 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
795 new String[] { String.valueOf(rawContactId),
796 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
797 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700798 }
799
800 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800801 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800802 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800803 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
804 Intent serviceIntent = new Intent(context, ContactSaveService.class);
805 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
806 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
807 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
808
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800809 return serviceIntent;
810 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800811
812 private void setStarred(Intent intent) {
813 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
814 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
815 if (contactUri == null) {
816 Log.e(TAG, "Invalid arguments for setStarred request");
817 return;
818 }
819
820 final ContentValues values = new ContentValues(1);
821 values.put(Contacts.STARRED, value);
822 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700823
824 // Undemote the contact if necessary
825 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
826 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800827 if (c == null) {
828 return;
829 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700830 try {
831 if (c.moveToFirst()) {
832 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700833
834 // Don't bother undemoting if this contact is the user's profile.
835 if (id < Profile.MIN_ID) {
Yorke Leede1c78c2014-06-19 11:30:55 -0700836 getContentResolver().call(ContactsContract.AUTHORITY_URI,
837 PinnedPositions.UNDEMOTE_METHOD, String.valueOf(id), null);
Yorke Leebbb8c992013-09-23 16:20:53 -0700838 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700839 }
840 } finally {
841 c.close();
842 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800843 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800844
845 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700846 * Creates an intent that can be sent to this service to set the redirect to voicemail.
847 */
848 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
849 boolean value) {
850 Intent serviceIntent = new Intent(context, ContactSaveService.class);
851 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
852 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
853 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
854
855 return serviceIntent;
856 }
857
858 private void setSendToVoicemail(Intent intent) {
859 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
860 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
861 if (contactUri == null) {
862 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
863 return;
864 }
865
866 final ContentValues values = new ContentValues(1);
867 values.put(Contacts.SEND_TO_VOICEMAIL, value);
868 getContentResolver().update(contactUri, values, null, null);
869 }
870
871 /**
872 * Creates an intent that can be sent to this service to save the contact's ringtone.
873 */
874 public static Intent createSetRingtone(Context context, Uri contactUri,
875 String value) {
876 Intent serviceIntent = new Intent(context, ContactSaveService.class);
877 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
878 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
879 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
880
881 return serviceIntent;
882 }
883
884 private void setRingtone(Intent intent) {
885 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
886 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
887 if (contactUri == null) {
888 Log.e(TAG, "Invalid arguments for setRingtone");
889 return;
890 }
891 ContentValues values = new ContentValues(1);
892 values.put(Contacts.CUSTOM_RINGTONE, value);
893 getContentResolver().update(contactUri, values, null, null);
894 }
895
896 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800897 * Creates an intent that sets the selected data item as super primary (default)
898 */
899 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
900 Intent serviceIntent = new Intent(context, ContactSaveService.class);
901 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
902 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
903 return serviceIntent;
904 }
905
906 private void setSuperPrimary(Intent intent) {
907 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
908 if (dataId == -1) {
909 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
910 return;
911 }
912
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700913 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800914 }
915
916 /**
917 * Creates an intent that clears the primary flag of all data items that belong to the same
918 * raw_contact as the given data item. Will only clear, if the data item was primary before
919 * this call
920 */
921 public static Intent createClearPrimaryIntent(Context context, long dataId) {
922 Intent serviceIntent = new Intent(context, ContactSaveService.class);
923 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
924 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
925 return serviceIntent;
926 }
927
928 private void clearPrimary(Intent intent) {
929 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
930 if (dataId == -1) {
931 Log.e(TAG, "Invalid arguments for clearPrimary request");
932 return;
933 }
934
935 // Update the primary values in the data record.
936 ContentValues values = new ContentValues(1);
937 values.put(Data.IS_SUPER_PRIMARY, 0);
938 values.put(Data.IS_PRIMARY, 0);
939
940 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
941 values, null, null);
942 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800943
944 /**
945 * Creates an intent that can be sent to this service to delete a contact.
946 */
947 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
948 Intent serviceIntent = new Intent(context, ContactSaveService.class);
949 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
950 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
951 return serviceIntent;
952 }
953
954 private void deleteContact(Intent intent) {
955 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
956 if (contactUri == null) {
957 Log.e(TAG, "Invalid arguments for deleteContact request");
958 return;
959 }
960
961 getContentResolver().delete(contactUri, null, null);
962 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800963
964 /**
965 * Creates an intent that can be sent to this service to join two contacts.
966 */
967 public static Intent createJoinContactsIntent(Context context, long contactId1,
968 long contactId2, boolean contactWritable,
Josh Garguse5d3f892012-04-11 11:56:15 -0700969 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800970 Intent serviceIntent = new Intent(context, ContactSaveService.class);
971 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
972 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
973 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
974 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
975
976 // Callback intent will be invoked by the service once the contacts are joined.
977 Intent callbackIntent = new Intent(context, callbackActivity);
978 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800979 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
980
981 return serviceIntent;
982 }
983
984
985 private interface JoinContactQuery {
986 String[] PROJECTION = {
987 RawContacts._ID,
988 RawContacts.CONTACT_ID,
989 RawContacts.NAME_VERIFIED,
990 RawContacts.DISPLAY_NAME_SOURCE,
991 };
992
993 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
994
995 int _ID = 0;
996 int CONTACT_ID = 1;
997 int NAME_VERIFIED = 2;
998 int DISPLAY_NAME_SOURCE = 3;
999 }
1000
1001 private void joinContacts(Intent intent) {
1002 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1003 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
1004 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
1005 if (contactId1 == -1 || contactId2 == -1) {
1006 Log.e(TAG, "Invalid arguments for joinContacts request");
1007 return;
1008 }
1009
1010 final ContentResolver resolver = getContentResolver();
1011
1012 // Load raw contact IDs for all raw contacts involved - currently edited and selected
1013 // in the join UIs
1014 Cursor c = resolver.query(RawContacts.CONTENT_URI,
1015 JoinContactQuery.PROJECTION,
1016 JoinContactQuery.SELECTION,
1017 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -08001018 if (c == null) {
1019 Log.e(TAG, "Unable to open Contacts DB cursor");
1020 showToast(R.string.contactSavedErrorToast);
1021 return;
1022 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001023
1024 long rawContactIds[];
1025 long verifiedNameRawContactId = -1;
1026 try {
Jay Shrauner7f42b902013-01-09 14:43:09 -08001027 if (c.getCount() == 0) {
1028 return;
1029 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001030 int maxDisplayNameSource = -1;
1031 rawContactIds = new long[c.getCount()];
1032 for (int i = 0; i < rawContactIds.length; i++) {
1033 c.moveToPosition(i);
1034 long rawContactId = c.getLong(JoinContactQuery._ID);
1035 rawContactIds[i] = rawContactId;
1036 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1037 if (nameSource > maxDisplayNameSource) {
1038 maxDisplayNameSource = nameSource;
1039 }
1040 }
1041
1042 // Find an appropriate display name for the joined contact:
1043 // if should have a higher DisplayNameSource or be the name
1044 // of the original contact that we are joining with another.
1045 if (writable) {
1046 for (int i = 0; i < rawContactIds.length; i++) {
1047 c.moveToPosition(i);
1048 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1049 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1050 if (nameSource == maxDisplayNameSource
1051 && (verifiedNameRawContactId == -1
1052 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1053 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1054 }
1055 }
1056 }
1057 }
1058 } finally {
1059 c.close();
1060 }
1061
1062 // For each pair of raw contacts, insert an aggregation exception
1063 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1064 for (int i = 0; i < rawContactIds.length; i++) {
1065 for (int j = 0; j < rawContactIds.length; j++) {
1066 if (i != j) {
1067 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1068 }
1069 }
1070 }
1071
1072 // Mark the original contact as "name verified" to make sure that the contact
1073 // display name does not change as a result of the join
1074 if (verifiedNameRawContactId != -1) {
1075 Builder builder = ContentProviderOperation.newUpdate(
1076 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1077 builder.withValue(RawContacts.NAME_VERIFIED, 1);
1078 operations.add(builder.build());
1079 }
1080
1081 boolean success = false;
1082 // Apply all aggregation exceptions as one batch
1083 try {
1084 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001085 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001086 success = true;
1087 } catch (RemoteException e) {
1088 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001089 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001090 } catch (OperationApplicationException e) {
1091 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001092 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001093 }
1094
1095 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1096 if (success) {
1097 Uri uri = RawContacts.getContactLookupUri(resolver,
1098 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1099 callbackIntent.setData(uri);
1100 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001101 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001102 }
1103
1104 /**
1105 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1106 */
1107 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1108 long rawContactId1, long rawContactId2) {
1109 Builder builder =
1110 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1111 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1112 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1113 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1114 operations.add(builder.build());
1115 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001116
1117 /**
1118 * Shows a toast on the UI thread.
1119 */
1120 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001121 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001122
1123 @Override
1124 public void run() {
1125 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1126 }
1127 });
1128 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001129
1130 private void deliverCallback(final Intent callbackIntent) {
1131 mMainHandler.post(new Runnable() {
1132
1133 @Override
1134 public void run() {
1135 deliverCallbackOnUiThread(callbackIntent);
1136 }
1137 });
1138 }
1139
1140 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1141 // TODO: this assumes that if there are multiple instances of the same
1142 // activity registered, the last one registered is the one waiting for
1143 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001144 for (Listener listener : sListeners) {
1145 if (callbackIntent.getComponent().equals(
1146 ((Activity) listener).getIntent().getComponent())) {
1147 listener.onServiceCompleted(callbackIntent);
1148 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001149 }
1150 }
1151 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001152}