blob: 32fb190a5e06d684656ac6fbd6a79fce9847d1a6 [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;
Maurice Chu851222a2012-06-21 11:43:08 -070052import com.android.contacts.model.RawContactDelta;
53import com.android.contacts.model.RawContactDeltaList;
Chiao Cheng47b6f702012-09-07 17:28:17 -070054import com.android.contacts.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080055import com.android.contacts.common.model.account.AccountWithDataSet;
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070056import com.android.contacts.util.CallerInfoCacheUtils;
57import com.google.common.collect.Lists;
58import com.google.common.collect.Sets;
59
Josh Garguse692e012012-01-18 14:53:11 -080060import java.io.File;
61import java.io.FileInputStream;
62import java.io.FileOutputStream;
63import java.io.IOException;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080064import java.util.ArrayList;
65import java.util.HashSet;
66import java.util.List;
67import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070068
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080069/**
70 * A service responsible for saving changes to the content provider.
71 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070072public class ContactSaveService extends IntentService {
73 private static final String TAG = "ContactSaveService";
74
Katherine Kuana007e442011-07-07 09:25:34 -070075 /** Set to true in order to view logs on content provider operations */
76 private static final boolean DEBUG = false;
77
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070078 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
79
80 public static final String EXTRA_ACCOUNT_NAME = "accountName";
81 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070082 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070083 public static final String EXTRA_CONTENT_VALUES = "contentValues";
84 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
85
Dmitri Plotnikova0114142011-02-15 13:53:21 -080086 public static final String ACTION_SAVE_CONTACT = "saveContact";
87 public static final String EXTRA_CONTACT_STATE = "state";
88 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070089 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070090 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080091 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070092
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080093 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080094 public static final String ACTION_RENAME_GROUP = "renameGroup";
95 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070096 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080097 public static final String EXTRA_GROUP_ID = "groupId";
98 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070099 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
100 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800101
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800102 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800103 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800104 public static final String EXTRA_CONTACT_URI = "contactUri";
105 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) {
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700183 // Call an appropriate method. If we're sure it affects how incoming phone calls are
184 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700185 String action = intent.getAction();
186 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
187 createRawContact(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700188 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800189 } else if (ACTION_SAVE_CONTACT.equals(action)) {
190 saveContact(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700191 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800192 } else if (ACTION_CREATE_GROUP.equals(action)) {
193 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800194 } else if (ACTION_RENAME_GROUP.equals(action)) {
195 renameGroup(intent);
196 } else if (ACTION_DELETE_GROUP.equals(action)) {
197 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700198 } else if (ACTION_UPDATE_GROUP.equals(action)) {
199 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800200 } else if (ACTION_SET_STARRED.equals(action)) {
201 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800202 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
203 setSuperPrimary(intent);
204 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
205 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800206 } else if (ACTION_DELETE_CONTACT.equals(action)) {
207 deleteContact(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700208 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800209 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
210 joinContacts(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700211 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700212 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
213 setSendToVoicemail(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700214 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700215 } else if (ACTION_SET_RINGTONE.equals(action)) {
216 setRingtone(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700217 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
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,
297 String updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800298 Bundle bundle = new Bundle();
299 bundle.putString(String.valueOf(rawContactId), updatedPhotoPath);
300 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
410 } catch (OperationApplicationException e) {
411 // Version consistency failed, re-parent change and try again
412 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
413 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
414 boolean first = true;
415 final int count = state.size();
416 for (int i = 0; i < count; i++) {
417 Long rawContactId = state.getRawContactId(i);
418 if (rawContactId != null && rawContactId != -1) {
419 if (!first) {
420 sb.append(',');
421 }
422 sb.append(rawContactId);
423 first = false;
424 }
425 }
426 sb.append(")");
427
428 if (first) {
429 throw new IllegalStateException("Version consistency failed for a new contact");
430 }
431
Maurice Chu851222a2012-06-21 11:43:08 -0700432 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700433 isProfile
434 ? RawContactsEntity.PROFILE_CONTENT_URI
435 : RawContactsEntity.CONTENT_URI,
436 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700437 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700438
439 // Update the new state to use profile URIs if appropriate.
440 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700441 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700442 delta.setProfileQueryUri();
443 }
444 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800445 }
446 }
447
Josh Garguse692e012012-01-18 14:53:11 -0800448 // Now save any updated photos. We do this at the end to ensure that
449 // the ContactProvider already knows about newly-created contacts.
450 if (updatedPhotos != null) {
451 for (String key : updatedPhotos.keySet()) {
452 String photoFilePath = updatedPhotos.getString(key);
453 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800454
455 // If the raw-contact ID is negative, we are saving a new raw-contact;
456 // replace the bogus ID with the new one that we actually saved the contact at.
457 if (rawContactId < 0) {
458 rawContactId = insertedRawContactId;
459 if (rawContactId == -1) {
460 throw new IllegalStateException(
461 "Could not determine RawContact ID for image insertion");
462 }
463 }
464
Josh Garguse692e012012-01-18 14:53:11 -0800465 File photoFile = new File(photoFilePath);
466 if (!saveUpdatedPhoto(rawContactId, photoFile)) succeeded = false;
467 }
468 }
469
Josh Garguse5d3f892012-04-11 11:56:15 -0700470 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
471 if (callbackIntent != null) {
472 if (succeeded) {
473 // Mark the intent to indicate that the save was successful (even if the lookup URI
474 // is now null). For local contacts or the local profile, it's possible that the
475 // save triggered removal of the contact, so no lookup URI would exist..
476 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
477 }
478 callbackIntent.setData(lookupUri);
479 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800480 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800481 }
482
Josh Garguse692e012012-01-18 14:53:11 -0800483 /**
484 * Save updated photo for the specified raw-contact.
485 * @return true for success, false for failure
486 */
487 private boolean saveUpdatedPhoto(long rawContactId, File photoFile) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800488 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800489 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
490 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
491
Josh Garguse692e012012-01-18 14:53:11 -0800492 try {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800493 final FileOutputStream outputStream = getContentResolver()
494 .openAssetFileDescriptor(outputUri, "rw").createOutputStream();
Josh Garguse692e012012-01-18 14:53:11 -0800495 try {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800496 final FileInputStream inputStream = new FileInputStream(photoFile);
497 try {
498 final byte[] buffer = new byte[16 * 1024];
499 int length;
500 int totalLength = 0;
501 while ((length = inputStream.read(buffer)) > 0) {
502 outputStream.write(buffer, 0, length);
503 totalLength += length;
504 }
505 Log.v(TAG, "Wrote " + totalLength + " bytes for photo " + photoFile.toString());
506 } finally {
507 inputStream.close();
508 }
509 } finally {
Josh Garguse692e012012-01-18 14:53:11 -0800510 outputStream.close();
Josh Gargusebc17922012-05-04 18:47:09 -0700511 photoFile.delete();
Josh Garguse692e012012-01-18 14:53:11 -0800512 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800513 } catch (IOException e) {
514 Log.e(TAG, "Failed to write photo: " + photoFile.toString() + " because: " + e);
515 return false;
Josh Garguse692e012012-01-18 14:53:11 -0800516 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800517 return true;
Josh Garguse692e012012-01-18 14:53:11 -0800518 }
519
Josh Gargusef15c8e2012-01-30 16:42:02 -0800520 /**
521 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
522 */
Maurice Chu851222a2012-06-21 11:43:08 -0700523 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800524 final ArrayList<ContentProviderOperation> diff,
525 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800526 long existingRawContactId = state.findRawContactId();
527 if (existingRawContactId != -1) {
528 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800529 }
530
Josh Gargusef15c8e2012-01-30 16:42:02 -0800531 return getInsertedRawContactId(diff, results);
532 }
533
534 /**
535 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
536 */
537 private long getInsertedRawContactId(
538 final ArrayList<ContentProviderOperation> diff,
539 final ContentProviderResult[] results) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800540 final int diffSize = diff.size();
541 for (int i = 0; i < diffSize; i++) {
542 ContentProviderOperation operation = diff.get(i);
543 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
544 && operation.getUri().getEncodedPath().contains(
545 RawContacts.CONTENT_URI.getEncodedPath())) {
546 return ContentUris.parseId(results[i].uri);
547 }
548 }
549 return -1;
550 }
551
552 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700553 * Creates an intent that can be sent to this service to create a new group as
554 * well as add new members at the same time.
555 *
556 * @param context of the application
557 * @param account in which the group should be created
558 * @param label is the name of the group (cannot be null)
559 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
560 * should be added to the group
561 * @param callbackActivity is the activity to send the callback intent to
562 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700563 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700564 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700565 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700566 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800567 Intent serviceIntent = new Intent(context, ContactSaveService.class);
568 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
569 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
570 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700571 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800572 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700573 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700574
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800575 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700576 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800577 Intent callbackIntent = new Intent(context, callbackActivity);
578 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700579 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800580
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700581 return serviceIntent;
582 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800583
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800584 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700585 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
586 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
587 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
588 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700589 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800590
591 ContentValues values = new ContentValues();
592 values.put(Groups.ACCOUNT_TYPE, accountType);
593 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700594 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800595 values.put(Groups.TITLE, label);
596
Katherine Kuan717e3432011-07-13 17:03:24 -0700597 final ContentResolver resolver = getContentResolver();
598
599 // Create the new group
600 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
601
602 // If there's no URI, then the insertion failed. Abort early because group members can't be
603 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800604 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700605 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800606 return;
607 }
608
Katherine Kuan717e3432011-07-13 17:03:24 -0700609 // Add new group members
610 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
611
612 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
613 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800614 values.clear();
615 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
616 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
617
618 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700619 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700620 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800621 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800622 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800623 }
624
625 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800626 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800627 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700628 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700629 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800630 Intent serviceIntent = new Intent(context, ContactSaveService.class);
631 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
632 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
633 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700634
635 // Callback intent will be invoked by the service once the group is renamed.
636 Intent callbackIntent = new Intent(context, callbackActivity);
637 callbackIntent.setAction(callbackAction);
638 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
639
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800640 return serviceIntent;
641 }
642
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800643 private void renameGroup(Intent intent) {
644 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
645 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
646
647 if (groupId == -1) {
648 Log.e(TAG, "Invalid arguments for renameGroup request");
649 return;
650 }
651
652 ContentValues values = new ContentValues();
653 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700654 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
655 getContentResolver().update(groupUri, values, null, null);
656
657 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
658 callbackIntent.setData(groupUri);
659 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800660 }
661
662 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800663 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800664 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800665 public static Intent createGroupDeletionIntent(Context context, long groupId) {
666 Intent serviceIntent = new Intent(context, ContactSaveService.class);
667 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800668 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800669 return serviceIntent;
670 }
671
672 private void deleteGroup(Intent intent) {
673 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
674 if (groupId == -1) {
675 Log.e(TAG, "Invalid arguments for deleteGroup request");
676 return;
677 }
678
679 getContentResolver().delete(
680 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
681 }
682
683 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700684 * Creates an intent that can be sent to this service to rename a group as
685 * well as add and remove members from the group.
686 *
687 * @param context of the application
688 * @param groupId of the group that should be modified
689 * @param newLabel is the updated name of the group (can be null if the name
690 * should not be updated)
691 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
692 * should be added to the group
693 * @param rawContactsToRemove is an array of raw contact IDs for contacts
694 * that should be removed from the group
695 * @param callbackActivity is the activity to send the callback intent to
696 * @param callbackAction is the intent action for the callback intent
697 */
698 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
699 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700700 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700701 Intent serviceIntent = new Intent(context, ContactSaveService.class);
702 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
703 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
704 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
705 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
706 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
707 rawContactsToRemove);
708
709 // Callback intent will be invoked by the service once the group is updated
710 Intent callbackIntent = new Intent(context, callbackActivity);
711 callbackIntent.setAction(callbackAction);
712 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
713
714 return serviceIntent;
715 }
716
717 private void updateGroup(Intent intent) {
718 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
719 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
720 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
721 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
722
723 if (groupId == -1) {
724 Log.e(TAG, "Invalid arguments for updateGroup request");
725 return;
726 }
727
728 final ContentResolver resolver = getContentResolver();
729 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
730
731 // Update group name if necessary
732 if (label != null) {
733 ContentValues values = new ContentValues();
734 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700735 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700736 }
737
Katherine Kuan717e3432011-07-13 17:03:24 -0700738 // Add and remove members if necessary
739 addMembersToGroup(resolver, rawContactsToAdd, groupId);
740 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
741
742 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
743 callbackIntent.setData(groupUri);
744 deliverCallback(callbackIntent);
745 }
746
Daniel Lehmann18958a22012-02-28 17:45:25 -0800747 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700748 long groupId) {
749 if (rawContactsToAdd == null) {
750 return;
751 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700752 for (long rawContactId : rawContactsToAdd) {
753 try {
754 final ArrayList<ContentProviderOperation> rawContactOperations =
755 new ArrayList<ContentProviderOperation>();
756
757 // Build an assert operation to ensure the contact is not already in the group
758 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
759 .newAssertQuery(Data.CONTENT_URI);
760 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
761 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
762 new String[] { String.valueOf(rawContactId),
763 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
764 assertBuilder.withExpectedCount(0);
765 rawContactOperations.add(assertBuilder.build());
766
767 // Build an insert operation to add the contact to the group
768 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
769 .newInsert(Data.CONTENT_URI);
770 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
771 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
772 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
773 rawContactOperations.add(insertBuilder.build());
774
775 if (DEBUG) {
776 for (ContentProviderOperation operation : rawContactOperations) {
777 Log.v(TAG, operation.toString());
778 }
779 }
780
781 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700782 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800783 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700784 }
785 } catch (RemoteException e) {
786 // Something went wrong, bail without success
787 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
788 String.valueOf(rawContactId), e);
789 } catch (OperationApplicationException e) {
790 // The assert could have failed because the contact is already in the group,
791 // just continue to the next contact
792 Log.w(TAG, "Assert failed in adding raw contact ID " +
793 String.valueOf(rawContactId) + ". Already exists in group " +
794 String.valueOf(groupId), e);
795 }
796 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700797 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700798
Daniel Lehmann18958a22012-02-28 17:45:25 -0800799 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700800 long groupId) {
801 if (rawContactsToRemove == null) {
802 return;
803 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700804 for (long rawContactId : rawContactsToRemove) {
805 // Apply the delete operation on the data row for the given raw contact's
806 // membership in the given group. If no contact matches the provided selection, then
807 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800808 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700809 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
810 new String[] { String.valueOf(rawContactId),
811 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
812 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700813 }
814
815 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800816 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800817 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800818 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
819 Intent serviceIntent = new Intent(context, ContactSaveService.class);
820 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
821 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
822 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
823
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800824 return serviceIntent;
825 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800826
827 private void setStarred(Intent intent) {
828 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
829 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
830 if (contactUri == null) {
831 Log.e(TAG, "Invalid arguments for setStarred request");
832 return;
833 }
834
835 final ContentValues values = new ContentValues(1);
836 values.put(Contacts.STARRED, value);
837 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700838
839 // Undemote the contact if necessary
840 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
841 null, null, null);
842 try {
843 if (c.moveToFirst()) {
844 final long id = c.getLong(0);
845 values.clear();
846 values.put(String.valueOf(id), PinnedPositions.UNDEMOTE);
847 getContentResolver().update(PinnedPositions.UPDATE_URI, values, null, null);
848 }
849 } finally {
850 c.close();
851 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800852 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800853
854 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700855 * Creates an intent that can be sent to this service to set the redirect to voicemail.
856 */
857 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
858 boolean value) {
859 Intent serviceIntent = new Intent(context, ContactSaveService.class);
860 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
861 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
862 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
863
864 return serviceIntent;
865 }
866
867 private void setSendToVoicemail(Intent intent) {
868 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
869 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
870 if (contactUri == null) {
871 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
872 return;
873 }
874
875 final ContentValues values = new ContentValues(1);
876 values.put(Contacts.SEND_TO_VOICEMAIL, value);
877 getContentResolver().update(contactUri, values, null, null);
878 }
879
880 /**
881 * Creates an intent that can be sent to this service to save the contact's ringtone.
882 */
883 public static Intent createSetRingtone(Context context, Uri contactUri,
884 String value) {
885 Intent serviceIntent = new Intent(context, ContactSaveService.class);
886 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
887 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
888 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
889
890 return serviceIntent;
891 }
892
893 private void setRingtone(Intent intent) {
894 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
895 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
896 if (contactUri == null) {
897 Log.e(TAG, "Invalid arguments for setRingtone");
898 return;
899 }
900 ContentValues values = new ContentValues(1);
901 values.put(Contacts.CUSTOM_RINGTONE, value);
902 getContentResolver().update(contactUri, values, null, null);
903 }
904
905 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800906 * Creates an intent that sets the selected data item as super primary (default)
907 */
908 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
909 Intent serviceIntent = new Intent(context, ContactSaveService.class);
910 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
911 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
912 return serviceIntent;
913 }
914
915 private void setSuperPrimary(Intent intent) {
916 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
917 if (dataId == -1) {
918 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
919 return;
920 }
921
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700922 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800923 }
924
925 /**
926 * Creates an intent that clears the primary flag of all data items that belong to the same
927 * raw_contact as the given data item. Will only clear, if the data item was primary before
928 * this call
929 */
930 public static Intent createClearPrimaryIntent(Context context, long dataId) {
931 Intent serviceIntent = new Intent(context, ContactSaveService.class);
932 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
933 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
934 return serviceIntent;
935 }
936
937 private void clearPrimary(Intent intent) {
938 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
939 if (dataId == -1) {
940 Log.e(TAG, "Invalid arguments for clearPrimary request");
941 return;
942 }
943
944 // Update the primary values in the data record.
945 ContentValues values = new ContentValues(1);
946 values.put(Data.IS_SUPER_PRIMARY, 0);
947 values.put(Data.IS_PRIMARY, 0);
948
949 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
950 values, null, null);
951 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800952
953 /**
954 * Creates an intent that can be sent to this service to delete a contact.
955 */
956 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
957 Intent serviceIntent = new Intent(context, ContactSaveService.class);
958 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
959 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
960 return serviceIntent;
961 }
962
963 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
973 /**
974 * Creates an intent that can be sent to this service to join two contacts.
975 */
976 public static Intent createJoinContactsIntent(Context context, long contactId1,
977 long contactId2, boolean contactWritable,
Josh Garguse5d3f892012-04-11 11:56:15 -0700978 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800979 Intent serviceIntent = new Intent(context, ContactSaveService.class);
980 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
981 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
982 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
983 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
984
985 // Callback intent will be invoked by the service once the contacts are joined.
986 Intent callbackIntent = new Intent(context, callbackActivity);
987 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800988 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
989
990 return serviceIntent;
991 }
992
993
994 private interface JoinContactQuery {
995 String[] PROJECTION = {
996 RawContacts._ID,
997 RawContacts.CONTACT_ID,
998 RawContacts.NAME_VERIFIED,
999 RawContacts.DISPLAY_NAME_SOURCE,
1000 };
1001
1002 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
1003
1004 int _ID = 0;
1005 int CONTACT_ID = 1;
1006 int NAME_VERIFIED = 2;
1007 int DISPLAY_NAME_SOURCE = 3;
1008 }
1009
1010 private void joinContacts(Intent intent) {
1011 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1012 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
1013 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
1014 if (contactId1 == -1 || contactId2 == -1) {
1015 Log.e(TAG, "Invalid arguments for joinContacts request");
1016 return;
1017 }
1018
1019 final ContentResolver resolver = getContentResolver();
1020
1021 // Load raw contact IDs for all raw contacts involved - currently edited and selected
1022 // in the join UIs
1023 Cursor c = resolver.query(RawContacts.CONTENT_URI,
1024 JoinContactQuery.PROJECTION,
1025 JoinContactQuery.SELECTION,
1026 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1027
1028 long rawContactIds[];
1029 long verifiedNameRawContactId = -1;
1030 try {
Jay Shrauner7f42b902013-01-09 14:43:09 -08001031 if (c.getCount() == 0) {
1032 return;
1033 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001034 int maxDisplayNameSource = -1;
1035 rawContactIds = new long[c.getCount()];
1036 for (int i = 0; i < rawContactIds.length; i++) {
1037 c.moveToPosition(i);
1038 long rawContactId = c.getLong(JoinContactQuery._ID);
1039 rawContactIds[i] = rawContactId;
1040 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1041 if (nameSource > maxDisplayNameSource) {
1042 maxDisplayNameSource = nameSource;
1043 }
1044 }
1045
1046 // Find an appropriate display name for the joined contact:
1047 // if should have a higher DisplayNameSource or be the name
1048 // of the original contact that we are joining with another.
1049 if (writable) {
1050 for (int i = 0; i < rawContactIds.length; i++) {
1051 c.moveToPosition(i);
1052 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1053 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1054 if (nameSource == maxDisplayNameSource
1055 && (verifiedNameRawContactId == -1
1056 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1057 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1058 }
1059 }
1060 }
1061 }
1062 } finally {
1063 c.close();
1064 }
1065
1066 // For each pair of raw contacts, insert an aggregation exception
1067 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1068 for (int i = 0; i < rawContactIds.length; i++) {
1069 for (int j = 0; j < rawContactIds.length; j++) {
1070 if (i != j) {
1071 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1072 }
1073 }
1074 }
1075
1076 // Mark the original contact as "name verified" to make sure that the contact
1077 // display name does not change as a result of the join
1078 if (verifiedNameRawContactId != -1) {
1079 Builder builder = ContentProviderOperation.newUpdate(
1080 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1081 builder.withValue(RawContacts.NAME_VERIFIED, 1);
1082 operations.add(builder.build());
1083 }
1084
1085 boolean success = false;
1086 // Apply all aggregation exceptions as one batch
1087 try {
1088 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001089 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001090 success = true;
1091 } catch (RemoteException e) {
1092 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001093 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001094 } catch (OperationApplicationException e) {
1095 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001096 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001097 }
1098
1099 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1100 if (success) {
1101 Uri uri = RawContacts.getContactLookupUri(resolver,
1102 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1103 callbackIntent.setData(uri);
1104 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001105 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001106 }
1107
1108 /**
1109 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1110 */
1111 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1112 long rawContactId1, long rawContactId2) {
1113 Builder builder =
1114 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1115 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1116 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1117 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1118 operations.add(builder.build());
1119 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001120
1121 /**
1122 * Shows a toast on the UI thread.
1123 */
1124 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001125 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001126
1127 @Override
1128 public void run() {
1129 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1130 }
1131 });
1132 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001133
1134 private void deliverCallback(final Intent callbackIntent) {
1135 mMainHandler.post(new Runnable() {
1136
1137 @Override
1138 public void run() {
1139 deliverCallbackOnUiThread(callbackIntent);
1140 }
1141 });
1142 }
1143
1144 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1145 // TODO: this assumes that if there are multiple instances of the same
1146 // activity registered, the last one registered is the one waiting for
1147 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001148 for (Listener listener : sListeners) {
1149 if (callbackIntent.getComponent().equals(
1150 ((Activity) listener).getIntent().getComponent())) {
1151 listener.onServiceCompleted(callbackIntent);
1152 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001153 }
1154 }
1155 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001156}