blob: 7c8782f76ee7100ec4103d110f272267330fe576 [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;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070043import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070044import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070045import android.provider.ContactsContract.RawContactsEntity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070046import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080047import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070048
Chiao Chengd7ca03e2012-10-24 15:14:08 -070049import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080050import com.android.contacts.common.model.AccountTypeManager;
Maurice Chu851222a2012-06-21 11:43:08 -070051import com.android.contacts.model.RawContactDelta;
52import com.android.contacts.model.RawContactDeltaList;
Chiao Cheng47b6f702012-09-07 17:28:17 -070053import com.android.contacts.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080054import com.android.contacts.common.model.account.AccountWithDataSet;
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070055import com.android.contacts.util.CallerInfoCacheUtils;
56import com.google.common.collect.Lists;
57import com.google.common.collect.Sets;
58
Josh Garguse692e012012-01-18 14:53:11 -080059import java.io.File;
60import java.io.FileInputStream;
61import java.io.FileOutputStream;
62import java.io.IOException;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080063import java.util.ArrayList;
64import java.util.HashSet;
65import java.util.List;
66import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070067
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080068/**
69 * A service responsible for saving changes to the content provider.
70 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070071public class ContactSaveService extends IntentService {
72 private static final String TAG = "ContactSaveService";
73
Katherine Kuana007e442011-07-07 09:25:34 -070074 /** Set to true in order to view logs on content provider operations */
75 private static final boolean DEBUG = false;
76
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070077 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
78
79 public static final String EXTRA_ACCOUNT_NAME = "accountName";
80 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070081 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070082 public static final String EXTRA_CONTENT_VALUES = "contentValues";
83 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
84
Dmitri Plotnikova0114142011-02-15 13:53:21 -080085 public static final String ACTION_SAVE_CONTACT = "saveContact";
86 public static final String EXTRA_CONTACT_STATE = "state";
87 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070088 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070089 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080090 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070091
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080092 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080093 public static final String ACTION_RENAME_GROUP = "renameGroup";
94 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070095 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080096 public static final String EXTRA_GROUP_ID = "groupId";
97 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070098 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
99 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800100
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800101 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800102 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800103 public static final String EXTRA_CONTACT_URI = "contactUri";
104 public static final String EXTRA_STARRED_FLAG = "starred";
105
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800106 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
107 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
108 public static final String EXTRA_DATA_ID = "dataId";
109
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800110 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
111 public static final String EXTRA_CONTACT_ID1 = "contactId1";
112 public static final String EXTRA_CONTACT_ID2 = "contactId2";
113 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
114
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700115 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
116 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
117
118 public static final String ACTION_SET_RINGTONE = "setRingtone";
119 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
120
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700121 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
122 Data.MIMETYPE,
123 Data.IS_PRIMARY,
124 Data.DATA1,
125 Data.DATA2,
126 Data.DATA3,
127 Data.DATA4,
128 Data.DATA5,
129 Data.DATA6,
130 Data.DATA7,
131 Data.DATA8,
132 Data.DATA9,
133 Data.DATA10,
134 Data.DATA11,
135 Data.DATA12,
136 Data.DATA13,
137 Data.DATA14,
138 Data.DATA15
139 );
140
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800141 private static final int PERSIST_TRIES = 3;
142
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800143 public interface Listener {
144 public void onServiceCompleted(Intent callbackIntent);
145 }
146
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100147 private static final CopyOnWriteArrayList<Listener> sListeners =
148 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800149
150 private Handler mMainHandler;
151
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700152 public ContactSaveService() {
153 super(TAG);
154 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800155 mMainHandler = new Handler(Looper.getMainLooper());
156 }
157
158 public static void registerListener(Listener listener) {
159 if (!(listener instanceof Activity)) {
160 throw new ClassCastException("Only activities can be registered to"
161 + " receive callback from " + ContactSaveService.class.getName());
162 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100163 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800164 }
165
166 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100167 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700168 }
169
170 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800171 public Object getSystemService(String name) {
172 Object service = super.getSystemService(name);
173 if (service != null) {
174 return service;
175 }
176
177 return getApplicationContext().getSystemService(name);
178 }
179
180 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700181 protected void onHandleIntent(Intent intent) {
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700182 // Call an appropriate method. If we're sure it affects how incoming phone calls are
183 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700184 String action = intent.getAction();
185 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
186 createRawContact(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700187 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800188 } else if (ACTION_SAVE_CONTACT.equals(action)) {
189 saveContact(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700190 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800191 } else if (ACTION_CREATE_GROUP.equals(action)) {
192 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800193 } else if (ACTION_RENAME_GROUP.equals(action)) {
194 renameGroup(intent);
195 } else if (ACTION_DELETE_GROUP.equals(action)) {
196 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700197 } else if (ACTION_UPDATE_GROUP.equals(action)) {
198 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800199 } else if (ACTION_SET_STARRED.equals(action)) {
200 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800201 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
202 setSuperPrimary(intent);
203 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
204 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800205 } else if (ACTION_DELETE_CONTACT.equals(action)) {
206 deleteContact(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700207 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800208 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
209 joinContacts(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700210 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700211 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
212 setSendToVoicemail(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700213 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700214 } else if (ACTION_SET_RINGTONE.equals(action)) {
215 setRingtone(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700216 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700217 }
218 }
219
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800220 /**
221 * Creates an intent that can be sent to this service to create a new raw contact
222 * using data presented as a set of ContentValues.
223 */
224 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700225 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700226 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800227 Intent serviceIntent = new Intent(
228 context, ContactSaveService.class);
229 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
230 if (account != null) {
231 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
232 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700233 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800234 }
235 serviceIntent.putParcelableArrayListExtra(
236 ContactSaveService.EXTRA_CONTENT_VALUES, values);
237
238 // Callback intent will be invoked by the service once the new contact is
239 // created. The service will put the URI of the new contact as "data" on
240 // the callback intent.
241 Intent callbackIntent = new Intent(context, callbackActivity);
242 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800243 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
244 return serviceIntent;
245 }
246
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700247 private void createRawContact(Intent intent) {
248 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
249 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700250 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700251 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
252 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
253
254 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
255 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
256 .withValue(RawContacts.ACCOUNT_NAME, accountName)
257 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700258 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700259 .build());
260
261 int size = valueList.size();
262 for (int i = 0; i < size; i++) {
263 ContentValues values = valueList.get(i);
264 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
265 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
266 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
267 .withValues(values)
268 .build());
269 }
270
271 ContentResolver resolver = getContentResolver();
272 ContentProviderResult[] results;
273 try {
274 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
275 } catch (Exception e) {
276 throw new RuntimeException("Failed to store new contact", e);
277 }
278
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700279 Uri rawContactUri = results[0].uri;
280 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
281
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800282 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700283 }
284
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700285 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800286 * Creates an intent that can be sent to this service to create a new raw contact
287 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800288 * This variant is more convenient to use when there is only one photo that can
289 * possibly be updated, as in the Contact Details screen.
290 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
291 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800292 */
Maurice Chu851222a2012-06-21 11:43:08 -0700293 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700294 String saveModeExtraKey, int saveMode, boolean isProfile,
295 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
296 String updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800297 Bundle bundle = new Bundle();
298 bundle.putString(String.valueOf(rawContactId), updatedPhotoPath);
299 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
300 callbackActivity, callbackAction, bundle);
301 }
302
303 /**
304 * Creates an intent that can be sent to this service to create a new raw contact
305 * using data presented as a set of ContentValues.
306 * This variant is used when multiple contacts' photos may be updated, as in the
307 * Contact Editor.
308 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
309 */
Maurice Chu851222a2012-06-21 11:43:08 -0700310 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700311 String saveModeExtraKey, int saveMode, boolean isProfile,
312 Class<? extends Activity> callbackActivity, String callbackAction,
313 Bundle updatedPhotos) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800314 Intent serviceIntent = new Intent(
315 context, ContactSaveService.class);
316 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
317 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700318 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Josh Garguse692e012012-01-18 14:53:11 -0800319 if (updatedPhotos != null) {
320 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
321 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800322
Josh Garguse5d3f892012-04-11 11:56:15 -0700323 if (callbackActivity != null) {
324 // Callback intent will be invoked by the service once the contact is
325 // saved. The service will put the URI of the new contact as "data" on
326 // the callback intent.
327 Intent callbackIntent = new Intent(context, callbackActivity);
328 callbackIntent.putExtra(saveModeExtraKey, saveMode);
329 callbackIntent.setAction(callbackAction);
330 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
331 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800332 return serviceIntent;
333 }
334
335 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700336 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700337 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800338 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800339
340 // Trim any empty fields, and RawContacts, before persisting
341 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700342 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800343
344 Uri lookupUri = null;
345
346 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800347 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800348
Josh Gargusef15c8e2012-01-30 16:42:02 -0800349 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
350 long insertedRawContactId = -1;
351
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800352 // Attempt to persist changes
353 int tries = 0;
354 while (tries++ < PERSIST_TRIES) {
355 try {
356 // Build operations and try applying
357 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700358 if (DEBUG) {
359 Log.v(TAG, "Content Provider Operations:");
360 for (ContentProviderOperation operation : diff) {
361 Log.v(TAG, operation.toString());
362 }
363 }
364
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800365 ContentProviderResult[] results = null;
366 if (!diff.isEmpty()) {
367 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
368 }
369
370 final long rawContactId = getRawContactId(state, diff, results);
371 if (rawContactId == -1) {
372 throw new IllegalStateException("Could not determine RawContact ID after save");
373 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800374 // We don't have to check to see if the value is still -1. If we reach here,
375 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
376 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700377 if (isProfile) {
378 // Since the profile supports local raw contacts, which may have been completely
379 // removed if all information was removed, we need to do a special query to
380 // get the lookup URI for the profile contact (if it still exists).
381 Cursor c = resolver.query(Profile.CONTENT_URI,
382 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
383 null, null, null);
384 try {
Erik162b7e32011-09-20 15:23:55 -0700385 if (c.moveToFirst()) {
386 final long contactId = c.getLong(0);
387 final String lookupKey = c.getString(1);
388 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
389 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700390 } finally {
391 c.close();
392 }
393 } else {
394 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
395 rawContactId);
396 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
397 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800398 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
Josh Garguse692e012012-01-18 14:53:11 -0800399
400 // We can change this back to false later, if we fail to save the contact photo.
401 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800402 break;
403
404 } catch (RemoteException e) {
405 // Something went wrong, bail without success
406 Log.e(TAG, "Problem persisting user edits", e);
407 break;
408
409 } catch (OperationApplicationException e) {
410 // Version consistency failed, re-parent change and try again
411 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
412 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
413 boolean first = true;
414 final int count = state.size();
415 for (int i = 0; i < count; i++) {
416 Long rawContactId = state.getRawContactId(i);
417 if (rawContactId != null && rawContactId != -1) {
418 if (!first) {
419 sb.append(',');
420 }
421 sb.append(rawContactId);
422 first = false;
423 }
424 }
425 sb.append(")");
426
427 if (first) {
428 throw new IllegalStateException("Version consistency failed for a new contact");
429 }
430
Maurice Chu851222a2012-06-21 11:43:08 -0700431 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700432 isProfile
433 ? RawContactsEntity.PROFILE_CONTENT_URI
434 : RawContactsEntity.CONTENT_URI,
435 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700436 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700437
438 // Update the new state to use profile URIs if appropriate.
439 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700440 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700441 delta.setProfileQueryUri();
442 }
443 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800444 }
445 }
446
Josh Garguse692e012012-01-18 14:53:11 -0800447 // Now save any updated photos. We do this at the end to ensure that
448 // the ContactProvider already knows about newly-created contacts.
449 if (updatedPhotos != null) {
450 for (String key : updatedPhotos.keySet()) {
451 String photoFilePath = updatedPhotos.getString(key);
452 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800453
454 // If the raw-contact ID is negative, we are saving a new raw-contact;
455 // replace the bogus ID with the new one that we actually saved the contact at.
456 if (rawContactId < 0) {
457 rawContactId = insertedRawContactId;
458 if (rawContactId == -1) {
459 throw new IllegalStateException(
460 "Could not determine RawContact ID for image insertion");
461 }
462 }
463
Josh Garguse692e012012-01-18 14:53:11 -0800464 File photoFile = new File(photoFilePath);
465 if (!saveUpdatedPhoto(rawContactId, photoFile)) succeeded = false;
466 }
467 }
468
Josh Garguse5d3f892012-04-11 11:56:15 -0700469 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
470 if (callbackIntent != null) {
471 if (succeeded) {
472 // Mark the intent to indicate that the save was successful (even if the lookup URI
473 // is now null). For local contacts or the local profile, it's possible that the
474 // save triggered removal of the contact, so no lookup URI would exist..
475 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
476 }
477 callbackIntent.setData(lookupUri);
478 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800479 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800480 }
481
Josh Garguse692e012012-01-18 14:53:11 -0800482 /**
483 * Save updated photo for the specified raw-contact.
484 * @return true for success, false for failure
485 */
486 private boolean saveUpdatedPhoto(long rawContactId, File photoFile) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800487 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800488 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
489 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
490
Josh Garguse692e012012-01-18 14:53:11 -0800491 try {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800492 final FileOutputStream outputStream = getContentResolver()
493 .openAssetFileDescriptor(outputUri, "rw").createOutputStream();
Josh Garguse692e012012-01-18 14:53:11 -0800494 try {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800495 final FileInputStream inputStream = new FileInputStream(photoFile);
496 try {
497 final byte[] buffer = new byte[16 * 1024];
498 int length;
499 int totalLength = 0;
500 while ((length = inputStream.read(buffer)) > 0) {
501 outputStream.write(buffer, 0, length);
502 totalLength += length;
503 }
504 Log.v(TAG, "Wrote " + totalLength + " bytes for photo " + photoFile.toString());
505 } finally {
506 inputStream.close();
507 }
508 } finally {
Josh Garguse692e012012-01-18 14:53:11 -0800509 outputStream.close();
Josh Gargusebc17922012-05-04 18:47:09 -0700510 photoFile.delete();
Josh Garguse692e012012-01-18 14:53:11 -0800511 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800512 } catch (IOException e) {
513 Log.e(TAG, "Failed to write photo: " + photoFile.toString() + " because: " + e);
514 return false;
Josh Garguse692e012012-01-18 14:53:11 -0800515 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800516 return true;
Josh Garguse692e012012-01-18 14:53:11 -0800517 }
518
Josh Gargusef15c8e2012-01-30 16:42:02 -0800519 /**
520 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
521 */
Maurice Chu851222a2012-06-21 11:43:08 -0700522 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800523 final ArrayList<ContentProviderOperation> diff,
524 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800525 long existingRawContactId = state.findRawContactId();
526 if (existingRawContactId != -1) {
527 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800528 }
529
Josh Gargusef15c8e2012-01-30 16:42:02 -0800530 return getInsertedRawContactId(diff, results);
531 }
532
533 /**
534 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
535 */
536 private long getInsertedRawContactId(
537 final ArrayList<ContentProviderOperation> diff,
538 final ContentProviderResult[] results) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800539 final int diffSize = diff.size();
540 for (int i = 0; i < diffSize; i++) {
541 ContentProviderOperation operation = diff.get(i);
542 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
543 && operation.getUri().getEncodedPath().contains(
544 RawContacts.CONTENT_URI.getEncodedPath())) {
545 return ContentUris.parseId(results[i].uri);
546 }
547 }
548 return -1;
549 }
550
551 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700552 * Creates an intent that can be sent to this service to create a new group as
553 * well as add new members at the same time.
554 *
555 * @param context of the application
556 * @param account in which the group should be created
557 * @param label is the name of the group (cannot be null)
558 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
559 * should be added to the group
560 * @param callbackActivity is the activity to send the callback intent to
561 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700562 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700563 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700564 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700565 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800566 Intent serviceIntent = new Intent(context, ContactSaveService.class);
567 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
568 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
569 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700570 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800571 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700572 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700573
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800574 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700575 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800576 Intent callbackIntent = new Intent(context, callbackActivity);
577 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700578 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800579
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700580 return serviceIntent;
581 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800582
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800583 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700584 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
585 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
586 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
587 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700588 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800589
590 ContentValues values = new ContentValues();
591 values.put(Groups.ACCOUNT_TYPE, accountType);
592 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700593 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800594 values.put(Groups.TITLE, label);
595
Katherine Kuan717e3432011-07-13 17:03:24 -0700596 final ContentResolver resolver = getContentResolver();
597
598 // Create the new group
599 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
600
601 // If there's no URI, then the insertion failed. Abort early because group members can't be
602 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800603 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700604 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800605 return;
606 }
607
Katherine Kuan717e3432011-07-13 17:03:24 -0700608 // Add new group members
609 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
610
611 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
612 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800613 values.clear();
614 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
615 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
616
617 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700618 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700619 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800620 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800621 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800622 }
623
624 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800625 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800626 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700627 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700628 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800629 Intent serviceIntent = new Intent(context, ContactSaveService.class);
630 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
631 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
632 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700633
634 // Callback intent will be invoked by the service once the group is renamed.
635 Intent callbackIntent = new Intent(context, callbackActivity);
636 callbackIntent.setAction(callbackAction);
637 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
638
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800639 return serviceIntent;
640 }
641
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800642 private void renameGroup(Intent intent) {
643 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
644 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
645
646 if (groupId == -1) {
647 Log.e(TAG, "Invalid arguments for renameGroup request");
648 return;
649 }
650
651 ContentValues values = new ContentValues();
652 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700653 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
654 getContentResolver().update(groupUri, values, null, null);
655
656 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
657 callbackIntent.setData(groupUri);
658 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800659 }
660
661 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800662 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800663 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800664 public static Intent createGroupDeletionIntent(Context context, long groupId) {
665 Intent serviceIntent = new Intent(context, ContactSaveService.class);
666 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800667 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800668 return serviceIntent;
669 }
670
671 private void deleteGroup(Intent intent) {
672 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
673 if (groupId == -1) {
674 Log.e(TAG, "Invalid arguments for deleteGroup request");
675 return;
676 }
677
678 getContentResolver().delete(
679 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
680 }
681
682 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700683 * Creates an intent that can be sent to this service to rename a group as
684 * well as add and remove members from the group.
685 *
686 * @param context of the application
687 * @param groupId of the group that should be modified
688 * @param newLabel is the updated name of the group (can be null if the name
689 * should not be updated)
690 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
691 * should be added to the group
692 * @param rawContactsToRemove is an array of raw contact IDs for contacts
693 * that should be removed from the group
694 * @param callbackActivity is the activity to send the callback intent to
695 * @param callbackAction is the intent action for the callback intent
696 */
697 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
698 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700699 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700700 Intent serviceIntent = new Intent(context, ContactSaveService.class);
701 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
702 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
703 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
704 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
705 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
706 rawContactsToRemove);
707
708 // Callback intent will be invoked by the service once the group is updated
709 Intent callbackIntent = new Intent(context, callbackActivity);
710 callbackIntent.setAction(callbackAction);
711 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
712
713 return serviceIntent;
714 }
715
716 private void updateGroup(Intent intent) {
717 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
718 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
719 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
720 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
721
722 if (groupId == -1) {
723 Log.e(TAG, "Invalid arguments for updateGroup request");
724 return;
725 }
726
727 final ContentResolver resolver = getContentResolver();
728 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
729
730 // Update group name if necessary
731 if (label != null) {
732 ContentValues values = new ContentValues();
733 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700734 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700735 }
736
Katherine Kuan717e3432011-07-13 17:03:24 -0700737 // Add and remove members if necessary
738 addMembersToGroup(resolver, rawContactsToAdd, groupId);
739 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
740
741 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
742 callbackIntent.setData(groupUri);
743 deliverCallback(callbackIntent);
744 }
745
Daniel Lehmann18958a22012-02-28 17:45:25 -0800746 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700747 long groupId) {
748 if (rawContactsToAdd == null) {
749 return;
750 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700751 for (long rawContactId : rawContactsToAdd) {
752 try {
753 final ArrayList<ContentProviderOperation> rawContactOperations =
754 new ArrayList<ContentProviderOperation>();
755
756 // Build an assert operation to ensure the contact is not already in the group
757 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
758 .newAssertQuery(Data.CONTENT_URI);
759 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
760 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
761 new String[] { String.valueOf(rawContactId),
762 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
763 assertBuilder.withExpectedCount(0);
764 rawContactOperations.add(assertBuilder.build());
765
766 // Build an insert operation to add the contact to the group
767 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
768 .newInsert(Data.CONTENT_URI);
769 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
770 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
771 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
772 rawContactOperations.add(insertBuilder.build());
773
774 if (DEBUG) {
775 for (ContentProviderOperation operation : rawContactOperations) {
776 Log.v(TAG, operation.toString());
777 }
778 }
779
780 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700781 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800782 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700783 }
784 } catch (RemoteException e) {
785 // Something went wrong, bail without success
786 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
787 String.valueOf(rawContactId), e);
788 } catch (OperationApplicationException e) {
789 // The assert could have failed because the contact is already in the group,
790 // just continue to the next contact
791 Log.w(TAG, "Assert failed in adding raw contact ID " +
792 String.valueOf(rawContactId) + ". Already exists in group " +
793 String.valueOf(groupId), e);
794 }
795 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700796 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700797
Daniel Lehmann18958a22012-02-28 17:45:25 -0800798 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700799 long groupId) {
800 if (rawContactsToRemove == null) {
801 return;
802 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700803 for (long rawContactId : rawContactsToRemove) {
804 // Apply the delete operation on the data row for the given raw contact's
805 // membership in the given group. If no contact matches the provided selection, then
806 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800807 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700808 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
809 new String[] { String.valueOf(rawContactId),
810 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
811 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700812 }
813
814 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800815 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800816 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800817 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
818 Intent serviceIntent = new Intent(context, ContactSaveService.class);
819 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
820 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
821 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
822
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800823 return serviceIntent;
824 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800825
826 private void setStarred(Intent intent) {
827 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
828 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
829 if (contactUri == null) {
830 Log.e(TAG, "Invalid arguments for setStarred request");
831 return;
832 }
833
834 final ContentValues values = new ContentValues(1);
835 values.put(Contacts.STARRED, value);
836 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800837 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800838
839 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700840 * Creates an intent that can be sent to this service to set the redirect to voicemail.
841 */
842 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
843 boolean value) {
844 Intent serviceIntent = new Intent(context, ContactSaveService.class);
845 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
846 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
847 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
848
849 return serviceIntent;
850 }
851
852 private void setSendToVoicemail(Intent intent) {
853 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
854 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
855 if (contactUri == null) {
856 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
857 return;
858 }
859
860 final ContentValues values = new ContentValues(1);
861 values.put(Contacts.SEND_TO_VOICEMAIL, value);
862 getContentResolver().update(contactUri, values, null, null);
863 }
864
865 /**
866 * Creates an intent that can be sent to this service to save the contact's ringtone.
867 */
868 public static Intent createSetRingtone(Context context, Uri contactUri,
869 String value) {
870 Intent serviceIntent = new Intent(context, ContactSaveService.class);
871 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
872 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
873 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
874
875 return serviceIntent;
876 }
877
878 private void setRingtone(Intent intent) {
879 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
880 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
881 if (contactUri == null) {
882 Log.e(TAG, "Invalid arguments for setRingtone");
883 return;
884 }
885 ContentValues values = new ContentValues(1);
886 values.put(Contacts.CUSTOM_RINGTONE, value);
887 getContentResolver().update(contactUri, values, null, null);
888 }
889
890 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800891 * Creates an intent that sets the selected data item as super primary (default)
892 */
893 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
894 Intent serviceIntent = new Intent(context, ContactSaveService.class);
895 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
896 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
897 return serviceIntent;
898 }
899
900 private void setSuperPrimary(Intent intent) {
901 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
902 if (dataId == -1) {
903 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
904 return;
905 }
906
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700907 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800908 }
909
910 /**
911 * Creates an intent that clears the primary flag of all data items that belong to the same
912 * raw_contact as the given data item. Will only clear, if the data item was primary before
913 * this call
914 */
915 public static Intent createClearPrimaryIntent(Context context, long dataId) {
916 Intent serviceIntent = new Intent(context, ContactSaveService.class);
917 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
918 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
919 return serviceIntent;
920 }
921
922 private void clearPrimary(Intent intent) {
923 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
924 if (dataId == -1) {
925 Log.e(TAG, "Invalid arguments for clearPrimary request");
926 return;
927 }
928
929 // Update the primary values in the data record.
930 ContentValues values = new ContentValues(1);
931 values.put(Data.IS_SUPER_PRIMARY, 0);
932 values.put(Data.IS_PRIMARY, 0);
933
934 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
935 values, null, null);
936 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800937
938 /**
939 * Creates an intent that can be sent to this service to delete a contact.
940 */
941 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
942 Intent serviceIntent = new Intent(context, ContactSaveService.class);
943 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
944 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
945 return serviceIntent;
946 }
947
948 private void deleteContact(Intent intent) {
949 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
950 if (contactUri == null) {
951 Log.e(TAG, "Invalid arguments for deleteContact request");
952 return;
953 }
954
955 getContentResolver().delete(contactUri, null, null);
956 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800957
958 /**
959 * Creates an intent that can be sent to this service to join two contacts.
960 */
961 public static Intent createJoinContactsIntent(Context context, long contactId1,
962 long contactId2, boolean contactWritable,
Josh Garguse5d3f892012-04-11 11:56:15 -0700963 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800964 Intent serviceIntent = new Intent(context, ContactSaveService.class);
965 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
966 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
967 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
968 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
969
970 // Callback intent will be invoked by the service once the contacts are joined.
971 Intent callbackIntent = new Intent(context, callbackActivity);
972 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800973 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
974
975 return serviceIntent;
976 }
977
978
979 private interface JoinContactQuery {
980 String[] PROJECTION = {
981 RawContacts._ID,
982 RawContacts.CONTACT_ID,
983 RawContacts.NAME_VERIFIED,
984 RawContacts.DISPLAY_NAME_SOURCE,
985 };
986
987 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
988
989 int _ID = 0;
990 int CONTACT_ID = 1;
991 int NAME_VERIFIED = 2;
992 int DISPLAY_NAME_SOURCE = 3;
993 }
994
995 private void joinContacts(Intent intent) {
996 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
997 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
998 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
999 if (contactId1 == -1 || contactId2 == -1) {
1000 Log.e(TAG, "Invalid arguments for joinContacts request");
1001 return;
1002 }
1003
1004 final ContentResolver resolver = getContentResolver();
1005
1006 // Load raw contact IDs for all raw contacts involved - currently edited and selected
1007 // in the join UIs
1008 Cursor c = resolver.query(RawContacts.CONTENT_URI,
1009 JoinContactQuery.PROJECTION,
1010 JoinContactQuery.SELECTION,
1011 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1012
1013 long rawContactIds[];
1014 long verifiedNameRawContactId = -1;
1015 try {
Jay Shrauner7f42b902013-01-09 14:43:09 -08001016 if (c.getCount() == 0) {
1017 return;
1018 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001019 int maxDisplayNameSource = -1;
1020 rawContactIds = new long[c.getCount()];
1021 for (int i = 0; i < rawContactIds.length; i++) {
1022 c.moveToPosition(i);
1023 long rawContactId = c.getLong(JoinContactQuery._ID);
1024 rawContactIds[i] = rawContactId;
1025 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1026 if (nameSource > maxDisplayNameSource) {
1027 maxDisplayNameSource = nameSource;
1028 }
1029 }
1030
1031 // Find an appropriate display name for the joined contact:
1032 // if should have a higher DisplayNameSource or be the name
1033 // of the original contact that we are joining with another.
1034 if (writable) {
1035 for (int i = 0; i < rawContactIds.length; i++) {
1036 c.moveToPosition(i);
1037 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1038 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1039 if (nameSource == maxDisplayNameSource
1040 && (verifiedNameRawContactId == -1
1041 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1042 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1043 }
1044 }
1045 }
1046 }
1047 } finally {
1048 c.close();
1049 }
1050
1051 // For each pair of raw contacts, insert an aggregation exception
1052 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1053 for (int i = 0; i < rawContactIds.length; i++) {
1054 for (int j = 0; j < rawContactIds.length; j++) {
1055 if (i != j) {
1056 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1057 }
1058 }
1059 }
1060
1061 // Mark the original contact as "name verified" to make sure that the contact
1062 // display name does not change as a result of the join
1063 if (verifiedNameRawContactId != -1) {
1064 Builder builder = ContentProviderOperation.newUpdate(
1065 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1066 builder.withValue(RawContacts.NAME_VERIFIED, 1);
1067 operations.add(builder.build());
1068 }
1069
1070 boolean success = false;
1071 // Apply all aggregation exceptions as one batch
1072 try {
1073 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001074 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001075 success = true;
1076 } catch (RemoteException e) {
1077 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001078 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001079 } catch (OperationApplicationException e) {
1080 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001081 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001082 }
1083
1084 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1085 if (success) {
1086 Uri uri = RawContacts.getContactLookupUri(resolver,
1087 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1088 callbackIntent.setData(uri);
1089 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001090 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001091 }
1092
1093 /**
1094 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1095 */
1096 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1097 long rawContactId1, long rawContactId2) {
1098 Builder builder =
1099 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1100 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1101 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1102 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1103 operations.add(builder.build());
1104 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001105
1106 /**
1107 * Shows a toast on the UI thread.
1108 */
1109 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001110 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001111
1112 @Override
1113 public void run() {
1114 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1115 }
1116 });
1117 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001118
1119 private void deliverCallback(final Intent callbackIntent) {
1120 mMainHandler.post(new Runnable() {
1121
1122 @Override
1123 public void run() {
1124 deliverCallbackOnUiThread(callbackIntent);
1125 }
1126 });
1127 }
1128
1129 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1130 // TODO: this assumes that if there are multiple instances of the same
1131 // activity registered, the last one registered is the one waiting for
1132 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001133 for (Listener listener : sListeners) {
1134 if (callbackIntent.getComponent().equals(
1135 ((Activity) listener).getIntent().getComponent())) {
1136 listener.onServiceCompleted(callbackIntent);
1137 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001138 }
1139 }
1140 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001141}