blob: 3fbca549b15d70acba3049b1450b7ac957deaf5b [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 Plotnikova0114142011-02-15 13:53:21 -080019import com.android.contacts.model.AccountTypeManager;
Dave Santoro2b3f3c52011-07-26 17:35:42 -070020import com.android.contacts.model.AccountWithDataSet;
Dave Santoroc90f95e2011-09-07 17:47:15 -070021import com.android.contacts.model.EntityDelta;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080022import com.android.contacts.model.EntityDeltaList;
23import com.android.contacts.model.EntityModifier;
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -070024import com.android.contacts.util.CallerInfoCacheUtils;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080025import com.google.android.collect.Lists;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070026import com.google.android.collect.Sets;
27
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080028import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070029import android.app.IntentService;
30import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080031import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070032import android.content.ContentProviderResult;
33import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080034import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070035import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080036import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070037import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080038import android.content.OperationApplicationException;
39import android.database.Cursor;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070040import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080041import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080042import android.os.Handler;
43import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080044import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080045import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070046import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080047import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080048import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080049import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070050import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080051import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070052import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070053import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070054import android.provider.ContactsContract.RawContactsEntity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070055import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080056import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070057
Josh Garguse692e012012-01-18 14:53:11 -080058import java.io.File;
59import java.io.FileInputStream;
60import java.io.FileOutputStream;
61import java.io.IOException;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080062import java.util.ArrayList;
63import java.util.HashSet;
64import java.util.List;
65import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070066
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080067/**
68 * A service responsible for saving changes to the content provider.
69 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070070public class ContactSaveService extends IntentService {
71 private static final String TAG = "ContactSaveService";
72
Katherine Kuana007e442011-07-07 09:25:34 -070073 /** Set to true in order to view logs on content provider operations */
74 private static final boolean DEBUG = false;
75
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070076 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
77
78 public static final String EXTRA_ACCOUNT_NAME = "accountName";
79 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070080 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070081 public static final String EXTRA_CONTENT_VALUES = "contentValues";
82 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
83
Dmitri Plotnikova0114142011-02-15 13:53:21 -080084 public static final String ACTION_SAVE_CONTACT = "saveContact";
85 public static final String EXTRA_CONTACT_STATE = "state";
86 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070087 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070088 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080089 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070090
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080091 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080092 public static final String ACTION_RENAME_GROUP = "renameGroup";
93 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070094 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080095 public static final String EXTRA_GROUP_ID = "groupId";
96 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070097 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
98 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080099
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800100 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800101 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800102 public static final String EXTRA_CONTACT_URI = "contactUri";
103 public static final String EXTRA_STARRED_FLAG = "starred";
104
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800105 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
106 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
107 public static final String EXTRA_DATA_ID = "dataId";
108
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800109 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
110 public static final String EXTRA_CONTACT_ID1 = "contactId1";
111 public static final String EXTRA_CONTACT_ID2 = "contactId2";
112 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
113
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700114 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
115 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
116
117 public static final String ACTION_SET_RINGTONE = "setRingtone";
118 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
119
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700120 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
121 Data.MIMETYPE,
122 Data.IS_PRIMARY,
123 Data.DATA1,
124 Data.DATA2,
125 Data.DATA3,
126 Data.DATA4,
127 Data.DATA5,
128 Data.DATA6,
129 Data.DATA7,
130 Data.DATA8,
131 Data.DATA9,
132 Data.DATA10,
133 Data.DATA11,
134 Data.DATA12,
135 Data.DATA13,
136 Data.DATA14,
137 Data.DATA15
138 );
139
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800140 private static final int PERSIST_TRIES = 3;
141
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800142 public interface Listener {
143 public void onServiceCompleted(Intent callbackIntent);
144 }
145
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100146 private static final CopyOnWriteArrayList<Listener> sListeners =
147 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800148
149 private Handler mMainHandler;
150
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700151 public ContactSaveService() {
152 super(TAG);
153 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800154 mMainHandler = new Handler(Looper.getMainLooper());
155 }
156
157 public static void registerListener(Listener listener) {
158 if (!(listener instanceof Activity)) {
159 throw new ClassCastException("Only activities can be registered to"
160 + " receive callback from " + ContactSaveService.class.getName());
161 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100162 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800163 }
164
165 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100166 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700167 }
168
169 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800170 public Object getSystemService(String name) {
171 Object service = super.getSystemService(name);
172 if (service != null) {
173 return service;
174 }
175
176 return getApplicationContext().getSystemService(name);
177 }
178
179 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700180 protected void onHandleIntent(Intent intent) {
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700181 // Call an appropriate method. If we're sure it affects how incoming phone calls are
182 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700183 String action = intent.getAction();
184 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
185 createRawContact(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700186 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800187 } else if (ACTION_SAVE_CONTACT.equals(action)) {
188 saveContact(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700189 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800190 } else if (ACTION_CREATE_GROUP.equals(action)) {
191 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800192 } else if (ACTION_RENAME_GROUP.equals(action)) {
193 renameGroup(intent);
194 } else if (ACTION_DELETE_GROUP.equals(action)) {
195 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700196 } else if (ACTION_UPDATE_GROUP.equals(action)) {
197 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800198 } else if (ACTION_SET_STARRED.equals(action)) {
199 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800200 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
201 setSuperPrimary(intent);
202 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
203 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800204 } else if (ACTION_DELETE_CONTACT.equals(action)) {
205 deleteContact(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700206 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800207 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
208 joinContacts(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700209 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700210 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
211 setSendToVoicemail(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700212 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700213 } else if (ACTION_SET_RINGTONE.equals(action)) {
214 setRingtone(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700215 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700216 }
217 }
218
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800219 /**
220 * Creates an intent that can be sent to this service to create a new raw contact
221 * using data presented as a set of ContentValues.
222 */
223 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700224 ArrayList<ContentValues> values, AccountWithDataSet account,
225 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800226 Intent serviceIntent = new Intent(
227 context, ContactSaveService.class);
228 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
229 if (account != null) {
230 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
231 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700232 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800233 }
234 serviceIntent.putParcelableArrayListExtra(
235 ContactSaveService.EXTRA_CONTENT_VALUES, values);
236
237 // Callback intent will be invoked by the service once the new contact is
238 // created. The service will put the URI of the new contact as "data" on
239 // the callback intent.
240 Intent callbackIntent = new Intent(context, callbackActivity);
241 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800242 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
243 return serviceIntent;
244 }
245
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700246 private void createRawContact(Intent intent) {
247 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
248 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700249 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700250 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
251 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
252
253 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
254 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
255 .withValue(RawContacts.ACCOUNT_NAME, accountName)
256 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700257 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700258 .build());
259
260 int size = valueList.size();
261 for (int i = 0; i < size; i++) {
262 ContentValues values = valueList.get(i);
263 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
264 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
265 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
266 .withValues(values)
267 .build());
268 }
269
270 ContentResolver resolver = getContentResolver();
271 ContentProviderResult[] results;
272 try {
273 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
274 } catch (Exception e) {
275 throw new RuntimeException("Failed to store new contact", e);
276 }
277
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700278 Uri rawContactUri = results[0].uri;
279 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
280
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800281 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700282 }
283
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700284 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800285 * Creates an intent that can be sent to this service to create a new raw contact
286 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800287 * This variant is more convenient to use when there is only one photo that can
288 * possibly be updated, as in the Contact Details screen.
289 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
290 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800291 */
292 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700293 String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
Josh Garguse692e012012-01-18 14:53:11 -0800294 String callbackAction, long rawContactId, String updatedPhotoPath) {
295 Bundle bundle = new Bundle();
296 bundle.putString(String.valueOf(rawContactId), updatedPhotoPath);
297 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
298 callbackActivity, callbackAction, bundle);
299 }
300
301 /**
302 * Creates an intent that can be sent to this service to create a new raw contact
303 * using data presented as a set of ContentValues.
304 * This variant is used when multiple contacts' photos may be updated, as in the
305 * Contact Editor.
306 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
307 */
308 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
309 String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
310 String callbackAction, Bundle updatedPhotos) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800311 Intent serviceIntent = new Intent(
312 context, ContactSaveService.class);
313 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
314 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700315 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Josh Garguse692e012012-01-18 14:53:11 -0800316 if (updatedPhotos != null) {
317 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
318 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800319
320 // Callback intent will be invoked by the service once the contact is
321 // saved. The service will put the URI of the new contact as "data" on
322 // the callback intent.
323 Intent callbackIntent = new Intent(context, callbackActivity);
324 callbackIntent.putExtra(saveModeExtraKey, saveMode);
325 callbackIntent.setAction(callbackAction);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800326 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
327 return serviceIntent;
328 }
329
330 private void saveContact(Intent intent) {
331 EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
332 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700333 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800334 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800335
336 // Trim any empty fields, and RawContacts, before persisting
337 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
338 EntityModifier.trimEmpty(state, accountTypes);
339
340 Uri lookupUri = null;
341
342 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800343 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800344
Josh Gargusef15c8e2012-01-30 16:42:02 -0800345 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
346 long insertedRawContactId = -1;
347
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800348 // Attempt to persist changes
349 int tries = 0;
350 while (tries++ < PERSIST_TRIES) {
351 try {
352 // Build operations and try applying
353 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700354 if (DEBUG) {
355 Log.v(TAG, "Content Provider Operations:");
356 for (ContentProviderOperation operation : diff) {
357 Log.v(TAG, operation.toString());
358 }
359 }
360
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800361 ContentProviderResult[] results = null;
362 if (!diff.isEmpty()) {
363 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
364 }
365
366 final long rawContactId = getRawContactId(state, diff, results);
367 if (rawContactId == -1) {
368 throw new IllegalStateException("Could not determine RawContact ID after save");
369 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800370 // We don't have to check to see if the value is still -1. If we reach here,
371 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
372 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700373 if (isProfile) {
374 // Since the profile supports local raw contacts, which may have been completely
375 // removed if all information was removed, we need to do a special query to
376 // get the lookup URI for the profile contact (if it still exists).
377 Cursor c = resolver.query(Profile.CONTENT_URI,
378 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
379 null, null, null);
380 try {
Erik162b7e32011-09-20 15:23:55 -0700381 if (c.moveToFirst()) {
382 final long contactId = c.getLong(0);
383 final String lookupKey = c.getString(1);
384 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
385 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700386 } finally {
387 c.close();
388 }
389 } else {
390 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
391 rawContactId);
392 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
393 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800394 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
Josh Garguse692e012012-01-18 14:53:11 -0800395
396 // We can change this back to false later, if we fail to save the contact photo.
397 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800398 break;
399
400 } catch (RemoteException e) {
401 // Something went wrong, bail without success
402 Log.e(TAG, "Problem persisting user edits", e);
403 break;
404
405 } catch (OperationApplicationException e) {
406 // Version consistency failed, re-parent change and try again
407 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
408 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
409 boolean first = true;
410 final int count = state.size();
411 for (int i = 0; i < count; i++) {
412 Long rawContactId = state.getRawContactId(i);
413 if (rawContactId != null && rawContactId != -1) {
414 if (!first) {
415 sb.append(',');
416 }
417 sb.append(rawContactId);
418 first = false;
419 }
420 }
421 sb.append(")");
422
423 if (first) {
424 throw new IllegalStateException("Version consistency failed for a new contact");
425 }
426
Dave Santoroc90f95e2011-09-07 17:47:15 -0700427 final EntityDeltaList newState = EntityDeltaList.fromQuery(
428 isProfile
429 ? RawContactsEntity.PROFILE_CONTENT_URI
430 : RawContactsEntity.CONTENT_URI,
431 resolver, sb.toString(), null, null);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800432 state = EntityDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700433
434 // Update the new state to use profile URIs if appropriate.
435 if (isProfile) {
436 for (EntityDelta delta : state) {
437 delta.setProfileQueryUri();
438 }
439 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800440 }
441 }
442
Josh Garguse692e012012-01-18 14:53:11 -0800443 // Now save any updated photos. We do this at the end to ensure that
444 // the ContactProvider already knows about newly-created contacts.
445 if (updatedPhotos != null) {
446 for (String key : updatedPhotos.keySet()) {
447 String photoFilePath = updatedPhotos.getString(key);
448 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800449
450 // If the raw-contact ID is negative, we are saving a new raw-contact;
451 // replace the bogus ID with the new one that we actually saved the contact at.
452 if (rawContactId < 0) {
453 rawContactId = insertedRawContactId;
454 if (rawContactId == -1) {
455 throw new IllegalStateException(
456 "Could not determine RawContact ID for image insertion");
457 }
458 }
459
Josh Garguse692e012012-01-18 14:53:11 -0800460 File photoFile = new File(photoFilePath);
461 if (!saveUpdatedPhoto(rawContactId, photoFile)) succeeded = false;
462 }
463 }
464
465 if (succeeded) {
466 // Mark the intent to indicate that the save was successful (even if the lookup URI
467 // is now null). For local contacts or the local profile, it's possible that the
468 // save triggered removal of the contact, so no lookup URI would exist..
469 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
470 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800471 callbackIntent.setData(lookupUri);
472
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800473 deliverCallback(callbackIntent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800474 }
475
Josh Garguse692e012012-01-18 14:53:11 -0800476 /**
477 * Save updated photo for the specified raw-contact.
478 * @return true for success, false for failure
479 */
480 private boolean saveUpdatedPhoto(long rawContactId, File photoFile) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800481 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800482 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
483 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
484
Josh Garguse692e012012-01-18 14:53:11 -0800485 try {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800486 final FileOutputStream outputStream = getContentResolver()
487 .openAssetFileDescriptor(outputUri, "rw").createOutputStream();
Josh Garguse692e012012-01-18 14:53:11 -0800488 try {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800489 final FileInputStream inputStream = new FileInputStream(photoFile);
490 try {
491 final byte[] buffer = new byte[16 * 1024];
492 int length;
493 int totalLength = 0;
494 while ((length = inputStream.read(buffer)) > 0) {
495 outputStream.write(buffer, 0, length);
496 totalLength += length;
497 }
498 Log.v(TAG, "Wrote " + totalLength + " bytes for photo " + photoFile.toString());
499 } finally {
500 inputStream.close();
501 }
502 } finally {
Josh Garguse692e012012-01-18 14:53:11 -0800503 outputStream.close();
Josh Garguse692e012012-01-18 14:53:11 -0800504 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800505 } catch (IOException e) {
506 Log.e(TAG, "Failed to write photo: " + photoFile.toString() + " because: " + e);
507 return false;
Josh Garguse692e012012-01-18 14:53:11 -0800508 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800509 return true;
Josh Garguse692e012012-01-18 14:53:11 -0800510 }
511
Josh Gargusef15c8e2012-01-30 16:42:02 -0800512 /**
513 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
514 */
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800515 private long getRawContactId(EntityDeltaList state,
516 final ArrayList<ContentProviderOperation> diff,
517 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800518 long existingRawContactId = state.findRawContactId();
519 if (existingRawContactId != -1) {
520 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800521 }
522
Josh Gargusef15c8e2012-01-30 16:42:02 -0800523 return getInsertedRawContactId(diff, results);
524 }
525
526 /**
527 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
528 */
529 private long getInsertedRawContactId(
530 final ArrayList<ContentProviderOperation> diff,
531 final ContentProviderResult[] results) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800532 final int diffSize = diff.size();
533 for (int i = 0; i < diffSize; i++) {
534 ContentProviderOperation operation = diff.get(i);
535 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
536 && operation.getUri().getEncodedPath().contains(
537 RawContacts.CONTENT_URI.getEncodedPath())) {
538 return ContentUris.parseId(results[i].uri);
539 }
540 }
541 return -1;
542 }
543
544 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700545 * Creates an intent that can be sent to this service to create a new group as
546 * well as add new members at the same time.
547 *
548 * @param context of the application
549 * @param account in which the group should be created
550 * @param label is the name of the group (cannot be null)
551 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
552 * should be added to the group
553 * @param callbackActivity is the activity to send the callback intent to
554 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700555 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700556 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Katherine Kuan717e3432011-07-13 17:03:24 -0700557 String label, long[] rawContactsToAdd, Class<?> callbackActivity,
558 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800559 Intent serviceIntent = new Intent(context, ContactSaveService.class);
560 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
561 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
562 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700563 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800564 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700565 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700566
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800567 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700568 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800569 Intent callbackIntent = new Intent(context, callbackActivity);
570 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700571 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800572
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700573 return serviceIntent;
574 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800575
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800576 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700577 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
578 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
579 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
580 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700581 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800582
583 ContentValues values = new ContentValues();
584 values.put(Groups.ACCOUNT_TYPE, accountType);
585 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700586 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800587 values.put(Groups.TITLE, label);
588
Katherine Kuan717e3432011-07-13 17:03:24 -0700589 final ContentResolver resolver = getContentResolver();
590
591 // Create the new group
592 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
593
594 // If there's no URI, then the insertion failed. Abort early because group members can't be
595 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800596 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700597 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800598 return;
599 }
600
Katherine Kuan717e3432011-07-13 17:03:24 -0700601 // Add new group members
602 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
603
604 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
605 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800606 values.clear();
607 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
608 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
609
610 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700611 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700612 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800613 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800614 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800615 }
616
617 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800618 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800619 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700620 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
621 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800622 Intent serviceIntent = new Intent(context, ContactSaveService.class);
623 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
624 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
625 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700626
627 // Callback intent will be invoked by the service once the group is renamed.
628 Intent callbackIntent = new Intent(context, callbackActivity);
629 callbackIntent.setAction(callbackAction);
630 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
631
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800632 return serviceIntent;
633 }
634
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800635 private void renameGroup(Intent intent) {
636 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
637 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
638
639 if (groupId == -1) {
640 Log.e(TAG, "Invalid arguments for renameGroup request");
641 return;
642 }
643
644 ContentValues values = new ContentValues();
645 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700646 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
647 getContentResolver().update(groupUri, values, null, null);
648
649 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
650 callbackIntent.setData(groupUri);
651 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800652 }
653
654 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800655 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800656 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800657 public static Intent createGroupDeletionIntent(Context context, long groupId) {
658 Intent serviceIntent = new Intent(context, ContactSaveService.class);
659 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800660 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800661 return serviceIntent;
662 }
663
664 private void deleteGroup(Intent intent) {
665 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
666 if (groupId == -1) {
667 Log.e(TAG, "Invalid arguments for deleteGroup request");
668 return;
669 }
670
671 getContentResolver().delete(
672 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
673 }
674
675 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700676 * Creates an intent that can be sent to this service to rename a group as
677 * well as add and remove members from the group.
678 *
679 * @param context of the application
680 * @param groupId of the group that should be modified
681 * @param newLabel is the updated name of the group (can be null if the name
682 * should not be updated)
683 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
684 * should be added to the group
685 * @param rawContactsToRemove is an array of raw contact IDs for contacts
686 * that should be removed from the group
687 * @param callbackActivity is the activity to send the callback intent to
688 * @param callbackAction is the intent action for the callback intent
689 */
690 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
691 long[] rawContactsToAdd, long[] rawContactsToRemove,
692 Class<?> callbackActivity, String callbackAction) {
693 Intent serviceIntent = new Intent(context, ContactSaveService.class);
694 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
695 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
696 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
697 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
698 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
699 rawContactsToRemove);
700
701 // Callback intent will be invoked by the service once the group is updated
702 Intent callbackIntent = new Intent(context, callbackActivity);
703 callbackIntent.setAction(callbackAction);
704 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
705
706 return serviceIntent;
707 }
708
709 private void updateGroup(Intent intent) {
710 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
711 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
712 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
713 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
714
715 if (groupId == -1) {
716 Log.e(TAG, "Invalid arguments for updateGroup request");
717 return;
718 }
719
720 final ContentResolver resolver = getContentResolver();
721 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
722
723 // Update group name if necessary
724 if (label != null) {
725 ContentValues values = new ContentValues();
726 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700727 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700728 }
729
Katherine Kuan717e3432011-07-13 17:03:24 -0700730 // Add and remove members if necessary
731 addMembersToGroup(resolver, rawContactsToAdd, groupId);
732 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
733
734 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
735 callbackIntent.setData(groupUri);
736 deliverCallback(callbackIntent);
737 }
738
Daniel Lehmann18958a22012-02-28 17:45:25 -0800739 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700740 long groupId) {
741 if (rawContactsToAdd == null) {
742 return;
743 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700744 for (long rawContactId : rawContactsToAdd) {
745 try {
746 final ArrayList<ContentProviderOperation> rawContactOperations =
747 new ArrayList<ContentProviderOperation>();
748
749 // Build an assert operation to ensure the contact is not already in the group
750 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
751 .newAssertQuery(Data.CONTENT_URI);
752 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
753 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
754 new String[] { String.valueOf(rawContactId),
755 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
756 assertBuilder.withExpectedCount(0);
757 rawContactOperations.add(assertBuilder.build());
758
759 // Build an insert operation to add the contact to the group
760 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
761 .newInsert(Data.CONTENT_URI);
762 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
763 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
764 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
765 rawContactOperations.add(insertBuilder.build());
766
767 if (DEBUG) {
768 for (ContentProviderOperation operation : rawContactOperations) {
769 Log.v(TAG, operation.toString());
770 }
771 }
772
773 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700774 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800775 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700776 }
777 } catch (RemoteException e) {
778 // Something went wrong, bail without success
779 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
780 String.valueOf(rawContactId), e);
781 } catch (OperationApplicationException e) {
782 // The assert could have failed because the contact is already in the group,
783 // just continue to the next contact
784 Log.w(TAG, "Assert failed in adding raw contact ID " +
785 String.valueOf(rawContactId) + ". Already exists in group " +
786 String.valueOf(groupId), e);
787 }
788 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700789 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700790
Daniel Lehmann18958a22012-02-28 17:45:25 -0800791 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700792 long groupId) {
793 if (rawContactsToRemove == null) {
794 return;
795 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700796 for (long rawContactId : rawContactsToRemove) {
797 // Apply the delete operation on the data row for the given raw contact's
798 // membership in the given group. If no contact matches the provided selection, then
799 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800800 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700801 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
802 new String[] { String.valueOf(rawContactId),
803 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
804 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700805 }
806
807 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800808 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800809 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800810 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
811 Intent serviceIntent = new Intent(context, ContactSaveService.class);
812 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
813 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
814 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
815
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800816 return serviceIntent;
817 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800818
819 private void setStarred(Intent intent) {
820 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
821 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
822 if (contactUri == null) {
823 Log.e(TAG, "Invalid arguments for setStarred request");
824 return;
825 }
826
827 final ContentValues values = new ContentValues(1);
828 values.put(Contacts.STARRED, value);
829 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800830 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800831
832 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700833 * Creates an intent that can be sent to this service to set the redirect to voicemail.
834 */
835 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
836 boolean value) {
837 Intent serviceIntent = new Intent(context, ContactSaveService.class);
838 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
839 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
840 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
841
842 return serviceIntent;
843 }
844
845 private void setSendToVoicemail(Intent intent) {
846 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
847 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
848 if (contactUri == null) {
849 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
850 return;
851 }
852
853 final ContentValues values = new ContentValues(1);
854 values.put(Contacts.SEND_TO_VOICEMAIL, value);
855 getContentResolver().update(contactUri, values, null, null);
856 }
857
858 /**
859 * Creates an intent that can be sent to this service to save the contact's ringtone.
860 */
861 public static Intent createSetRingtone(Context context, Uri contactUri,
862 String value) {
863 Intent serviceIntent = new Intent(context, ContactSaveService.class);
864 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
865 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
866 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
867
868 return serviceIntent;
869 }
870
871 private void setRingtone(Intent intent) {
872 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
873 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
874 if (contactUri == null) {
875 Log.e(TAG, "Invalid arguments for setRingtone");
876 return;
877 }
878 ContentValues values = new ContentValues(1);
879 values.put(Contacts.CUSTOM_RINGTONE, value);
880 getContentResolver().update(contactUri, values, null, null);
881 }
882
883 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800884 * Creates an intent that sets the selected data item as super primary (default)
885 */
886 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
887 Intent serviceIntent = new Intent(context, ContactSaveService.class);
888 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
889 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
890 return serviceIntent;
891 }
892
893 private void setSuperPrimary(Intent intent) {
894 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
895 if (dataId == -1) {
896 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
897 return;
898 }
899
900 // Update the primary values in the data record.
901 ContentValues values = new ContentValues(1);
902 values.put(Data.IS_SUPER_PRIMARY, 1);
903 values.put(Data.IS_PRIMARY, 1);
904
905 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
906 values, null, null);
907 }
908
909 /**
910 * Creates an intent that clears the primary flag of all data items that belong to the same
911 * raw_contact as the given data item. Will only clear, if the data item was primary before
912 * this call
913 */
914 public static Intent createClearPrimaryIntent(Context context, long dataId) {
915 Intent serviceIntent = new Intent(context, ContactSaveService.class);
916 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
917 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
918 return serviceIntent;
919 }
920
921 private void clearPrimary(Intent intent) {
922 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
923 if (dataId == -1) {
924 Log.e(TAG, "Invalid arguments for clearPrimary request");
925 return;
926 }
927
928 // Update the primary values in the data record.
929 ContentValues values = new ContentValues(1);
930 values.put(Data.IS_SUPER_PRIMARY, 0);
931 values.put(Data.IS_PRIMARY, 0);
932
933 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
934 values, null, null);
935 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800936
937 /**
938 * Creates an intent that can be sent to this service to delete a contact.
939 */
940 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
941 Intent serviceIntent = new Intent(context, ContactSaveService.class);
942 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
943 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
944 return serviceIntent;
945 }
946
947 private void deleteContact(Intent intent) {
948 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
949 if (contactUri == null) {
950 Log.e(TAG, "Invalid arguments for deleteContact request");
951 return;
952 }
953
954 getContentResolver().delete(contactUri, null, null);
955 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800956
957 /**
958 * Creates an intent that can be sent to this service to join two contacts.
959 */
960 public static Intent createJoinContactsIntent(Context context, long contactId1,
961 long contactId2, boolean contactWritable,
962 Class<?> callbackActivity, String callbackAction) {
963 Intent serviceIntent = new Intent(context, ContactSaveService.class);
964 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
965 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
966 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
967 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
968
969 // Callback intent will be invoked by the service once the contacts are joined.
970 Intent callbackIntent = new Intent(context, callbackActivity);
971 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800972 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
973
974 return serviceIntent;
975 }
976
977
978 private interface JoinContactQuery {
979 String[] PROJECTION = {
980 RawContacts._ID,
981 RawContacts.CONTACT_ID,
982 RawContacts.NAME_VERIFIED,
983 RawContacts.DISPLAY_NAME_SOURCE,
984 };
985
986 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
987
988 int _ID = 0;
989 int CONTACT_ID = 1;
990 int NAME_VERIFIED = 2;
991 int DISPLAY_NAME_SOURCE = 3;
992 }
993
994 private void joinContacts(Intent intent) {
995 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
996 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
997 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
998 if (contactId1 == -1 || contactId2 == -1) {
999 Log.e(TAG, "Invalid arguments for joinContacts request");
1000 return;
1001 }
1002
1003 final ContentResolver resolver = getContentResolver();
1004
1005 // Load raw contact IDs for all raw contacts involved - currently edited and selected
1006 // in the join UIs
1007 Cursor c = resolver.query(RawContacts.CONTENT_URI,
1008 JoinContactQuery.PROJECTION,
1009 JoinContactQuery.SELECTION,
1010 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1011
1012 long rawContactIds[];
1013 long verifiedNameRawContactId = -1;
1014 try {
1015 int maxDisplayNameSource = -1;
1016 rawContactIds = new long[c.getCount()];
1017 for (int i = 0; i < rawContactIds.length; i++) {
1018 c.moveToPosition(i);
1019 long rawContactId = c.getLong(JoinContactQuery._ID);
1020 rawContactIds[i] = rawContactId;
1021 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1022 if (nameSource > maxDisplayNameSource) {
1023 maxDisplayNameSource = nameSource;
1024 }
1025 }
1026
1027 // Find an appropriate display name for the joined contact:
1028 // if should have a higher DisplayNameSource or be the name
1029 // of the original contact that we are joining with another.
1030 if (writable) {
1031 for (int i = 0; i < rawContactIds.length; i++) {
1032 c.moveToPosition(i);
1033 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1034 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1035 if (nameSource == maxDisplayNameSource
1036 && (verifiedNameRawContactId == -1
1037 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1038 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1039 }
1040 }
1041 }
1042 }
1043 } finally {
1044 c.close();
1045 }
1046
1047 // For each pair of raw contacts, insert an aggregation exception
1048 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1049 for (int i = 0; i < rawContactIds.length; i++) {
1050 for (int j = 0; j < rawContactIds.length; j++) {
1051 if (i != j) {
1052 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1053 }
1054 }
1055 }
1056
1057 // Mark the original contact as "name verified" to make sure that the contact
1058 // display name does not change as a result of the join
1059 if (verifiedNameRawContactId != -1) {
1060 Builder builder = ContentProviderOperation.newUpdate(
1061 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1062 builder.withValue(RawContacts.NAME_VERIFIED, 1);
1063 operations.add(builder.build());
1064 }
1065
1066 boolean success = false;
1067 // Apply all aggregation exceptions as one batch
1068 try {
1069 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001070 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001071 success = true;
1072 } catch (RemoteException e) {
1073 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001074 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001075 } catch (OperationApplicationException e) {
1076 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001077 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001078 }
1079
1080 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1081 if (success) {
1082 Uri uri = RawContacts.getContactLookupUri(resolver,
1083 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1084 callbackIntent.setData(uri);
1085 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001086 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001087 }
1088
1089 /**
1090 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1091 */
1092 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1093 long rawContactId1, long rawContactId2) {
1094 Builder builder =
1095 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1096 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1097 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1098 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1099 operations.add(builder.build());
1100 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001101
1102 /**
1103 * Shows a toast on the UI thread.
1104 */
1105 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001106 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001107
1108 @Override
1109 public void run() {
1110 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1111 }
1112 });
1113 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001114
1115 private void deliverCallback(final Intent callbackIntent) {
1116 mMainHandler.post(new Runnable() {
1117
1118 @Override
1119 public void run() {
1120 deliverCallbackOnUiThread(callbackIntent);
1121 }
1122 });
1123 }
1124
1125 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1126 // TODO: this assumes that if there are multiple instances of the same
1127 // activity registered, the last one registered is the one waiting for
1128 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001129 for (Listener listener : sListeners) {
1130 if (callbackIntent.getComponent().equals(
1131 ((Activity) listener).getIntent().getComponent())) {
1132 listener.onServiceCompleted(callbackIntent);
1133 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001134 }
1135 }
1136 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001137}