blob: 3acc34c83602dfaa651659b46e9eaeed09b2d973 [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 Chenge0b2f1e2012-06-12 13:07:56 -070049import com.android.contacts.model.AccountTypeManager;
50import com.android.contacts.model.AccountWithDataSet;
51import com.android.contacts.model.EntityDelta;
52import com.android.contacts.model.EntityDeltaList;
53import com.android.contacts.model.EntityModifier;
54import com.android.contacts.util.CallerInfoCacheUtils;
55import com.google.common.collect.Lists;
56import com.google.common.collect.Sets;
57
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,
Josh Garguse5d3f892012-04-11 11:56:15 -0700225 Class<? extends Activity> 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,
Josh Garguse5d3f892012-04-11 11:56:15 -0700293 String saveModeExtraKey, int saveMode, boolean isProfile,
294 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
295 String updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800296 Bundle bundle = new Bundle();
297 bundle.putString(String.valueOf(rawContactId), updatedPhotoPath);
298 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
299 callbackActivity, callbackAction, bundle);
300 }
301
302 /**
303 * Creates an intent that can be sent to this service to create a new raw contact
304 * using data presented as a set of ContentValues.
305 * This variant is used when multiple contacts' photos may be updated, as in the
306 * Contact Editor.
307 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
308 */
309 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700310 String saveModeExtraKey, int saveMode, boolean isProfile,
311 Class<? extends Activity> callbackActivity, String callbackAction,
312 Bundle updatedPhotos) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800313 Intent serviceIntent = new Intent(
314 context, ContactSaveService.class);
315 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
316 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700317 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Josh Garguse692e012012-01-18 14:53:11 -0800318 if (updatedPhotos != null) {
319 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
320 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800321
Josh Garguse5d3f892012-04-11 11:56:15 -0700322 if (callbackActivity != null) {
323 // Callback intent will be invoked by the service once the contact is
324 // saved. The service will put the URI of the new contact as "data" on
325 // the callback intent.
326 Intent callbackIntent = new Intent(context, callbackActivity);
327 callbackIntent.putExtra(saveModeExtraKey, saveMode);
328 callbackIntent.setAction(callbackAction);
329 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
330 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800331 return serviceIntent;
332 }
333
334 private void saveContact(Intent intent) {
335 EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700336 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800337 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800338
339 // Trim any empty fields, and RawContacts, before persisting
340 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
341 EntityModifier.trimEmpty(state, accountTypes);
342
343 Uri lookupUri = null;
344
345 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800346 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800347
Josh Gargusef15c8e2012-01-30 16:42:02 -0800348 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
349 long insertedRawContactId = -1;
350
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800351 // Attempt to persist changes
352 int tries = 0;
353 while (tries++ < PERSIST_TRIES) {
354 try {
355 // Build operations and try applying
356 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700357 if (DEBUG) {
358 Log.v(TAG, "Content Provider Operations:");
359 for (ContentProviderOperation operation : diff) {
360 Log.v(TAG, operation.toString());
361 }
362 }
363
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800364 ContentProviderResult[] results = null;
365 if (!diff.isEmpty()) {
366 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
367 }
368
369 final long rawContactId = getRawContactId(state, diff, results);
370 if (rawContactId == -1) {
371 throw new IllegalStateException("Could not determine RawContact ID after save");
372 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800373 // We don't have to check to see if the value is still -1. If we reach here,
374 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
375 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700376 if (isProfile) {
377 // Since the profile supports local raw contacts, which may have been completely
378 // removed if all information was removed, we need to do a special query to
379 // get the lookup URI for the profile contact (if it still exists).
380 Cursor c = resolver.query(Profile.CONTENT_URI,
381 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
382 null, null, null);
383 try {
Erik162b7e32011-09-20 15:23:55 -0700384 if (c.moveToFirst()) {
385 final long contactId = c.getLong(0);
386 final String lookupKey = c.getString(1);
387 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
388 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700389 } finally {
390 c.close();
391 }
392 } else {
393 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
394 rawContactId);
395 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
396 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800397 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
Josh Garguse692e012012-01-18 14:53:11 -0800398
399 // We can change this back to false later, if we fail to save the contact photo.
400 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800401 break;
402
403 } catch (RemoteException e) {
404 // Something went wrong, bail without success
405 Log.e(TAG, "Problem persisting user edits", e);
406 break;
407
408 } catch (OperationApplicationException e) {
409 // Version consistency failed, re-parent change and try again
410 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
411 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
412 boolean first = true;
413 final int count = state.size();
414 for (int i = 0; i < count; i++) {
415 Long rawContactId = state.getRawContactId(i);
416 if (rawContactId != null && rawContactId != -1) {
417 if (!first) {
418 sb.append(',');
419 }
420 sb.append(rawContactId);
421 first = false;
422 }
423 }
424 sb.append(")");
425
426 if (first) {
427 throw new IllegalStateException("Version consistency failed for a new contact");
428 }
429
Dave Santoroc90f95e2011-09-07 17:47:15 -0700430 final EntityDeltaList newState = EntityDeltaList.fromQuery(
431 isProfile
432 ? RawContactsEntity.PROFILE_CONTENT_URI
433 : RawContactsEntity.CONTENT_URI,
434 resolver, sb.toString(), null, null);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800435 state = EntityDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700436
437 // Update the new state to use profile URIs if appropriate.
438 if (isProfile) {
439 for (EntityDelta delta : state) {
440 delta.setProfileQueryUri();
441 }
442 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800443 }
444 }
445
Josh Garguse692e012012-01-18 14:53:11 -0800446 // Now save any updated photos. We do this at the end to ensure that
447 // the ContactProvider already knows about newly-created contacts.
448 if (updatedPhotos != null) {
449 for (String key : updatedPhotos.keySet()) {
450 String photoFilePath = updatedPhotos.getString(key);
451 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800452
453 // If the raw-contact ID is negative, we are saving a new raw-contact;
454 // replace the bogus ID with the new one that we actually saved the contact at.
455 if (rawContactId < 0) {
456 rawContactId = insertedRawContactId;
457 if (rawContactId == -1) {
458 throw new IllegalStateException(
459 "Could not determine RawContact ID for image insertion");
460 }
461 }
462
Josh Garguse692e012012-01-18 14:53:11 -0800463 File photoFile = new File(photoFilePath);
464 if (!saveUpdatedPhoto(rawContactId, photoFile)) succeeded = false;
465 }
466 }
467
Josh Garguse5d3f892012-04-11 11:56:15 -0700468 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
469 if (callbackIntent != null) {
470 if (succeeded) {
471 // Mark the intent to indicate that the save was successful (even if the lookup URI
472 // is now null). For local contacts or the local profile, it's possible that the
473 // save triggered removal of the contact, so no lookup URI would exist..
474 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
475 }
476 callbackIntent.setData(lookupUri);
477 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800478 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800479 }
480
Josh Garguse692e012012-01-18 14:53:11 -0800481 /**
482 * Save updated photo for the specified raw-contact.
483 * @return true for success, false for failure
484 */
485 private boolean saveUpdatedPhoto(long rawContactId, File photoFile) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800486 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800487 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
488 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
489
Josh Garguse692e012012-01-18 14:53:11 -0800490 try {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800491 final FileOutputStream outputStream = getContentResolver()
492 .openAssetFileDescriptor(outputUri, "rw").createOutputStream();
Josh Garguse692e012012-01-18 14:53:11 -0800493 try {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800494 final FileInputStream inputStream = new FileInputStream(photoFile);
495 try {
496 final byte[] buffer = new byte[16 * 1024];
497 int length;
498 int totalLength = 0;
499 while ((length = inputStream.read(buffer)) > 0) {
500 outputStream.write(buffer, 0, length);
501 totalLength += length;
502 }
503 Log.v(TAG, "Wrote " + totalLength + " bytes for photo " + photoFile.toString());
504 } finally {
505 inputStream.close();
506 }
507 } finally {
Josh Garguse692e012012-01-18 14:53:11 -0800508 outputStream.close();
Josh Gargusebc17922012-05-04 18:47:09 -0700509 photoFile.delete();
Josh Garguse692e012012-01-18 14:53:11 -0800510 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800511 } catch (IOException e) {
512 Log.e(TAG, "Failed to write photo: " + photoFile.toString() + " because: " + e);
513 return false;
Josh Garguse692e012012-01-18 14:53:11 -0800514 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800515 return true;
Josh Garguse692e012012-01-18 14:53:11 -0800516 }
517
Josh Gargusef15c8e2012-01-30 16:42:02 -0800518 /**
519 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
520 */
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800521 private long getRawContactId(EntityDeltaList state,
522 final ArrayList<ContentProviderOperation> diff,
523 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800524 long existingRawContactId = state.findRawContactId();
525 if (existingRawContactId != -1) {
526 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800527 }
528
Josh Gargusef15c8e2012-01-30 16:42:02 -0800529 return getInsertedRawContactId(diff, results);
530 }
531
532 /**
533 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
534 */
535 private long getInsertedRawContactId(
536 final ArrayList<ContentProviderOperation> diff,
537 final ContentProviderResult[] results) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800538 final int diffSize = diff.size();
539 for (int i = 0; i < diffSize; i++) {
540 ContentProviderOperation operation = diff.get(i);
541 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
542 && operation.getUri().getEncodedPath().contains(
543 RawContacts.CONTENT_URI.getEncodedPath())) {
544 return ContentUris.parseId(results[i].uri);
545 }
546 }
547 return -1;
548 }
549
550 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700551 * Creates an intent that can be sent to this service to create a new group as
552 * well as add new members at the same time.
553 *
554 * @param context of the application
555 * @param account in which the group should be created
556 * @param label is the name of the group (cannot be null)
557 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
558 * should be added to the group
559 * @param callbackActivity is the activity to send the callback intent to
560 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700561 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700562 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700563 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700564 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800565 Intent serviceIntent = new Intent(context, ContactSaveService.class);
566 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
567 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
568 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700569 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800570 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700571 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700572
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800573 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700574 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800575 Intent callbackIntent = new Intent(context, callbackActivity);
576 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700577 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800578
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700579 return serviceIntent;
580 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800581
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800582 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700583 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
584 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
585 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
586 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700587 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800588
589 ContentValues values = new ContentValues();
590 values.put(Groups.ACCOUNT_TYPE, accountType);
591 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700592 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800593 values.put(Groups.TITLE, label);
594
Katherine Kuan717e3432011-07-13 17:03:24 -0700595 final ContentResolver resolver = getContentResolver();
596
597 // Create the new group
598 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
599
600 // If there's no URI, then the insertion failed. Abort early because group members can't be
601 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800602 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700603 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800604 return;
605 }
606
Katherine Kuan717e3432011-07-13 17:03:24 -0700607 // Add new group members
608 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
609
610 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
611 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800612 values.clear();
613 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
614 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
615
616 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700617 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700618 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800619 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800620 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800621 }
622
623 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800624 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800625 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700626 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700627 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800628 Intent serviceIntent = new Intent(context, ContactSaveService.class);
629 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
630 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
631 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700632
633 // Callback intent will be invoked by the service once the group is renamed.
634 Intent callbackIntent = new Intent(context, callbackActivity);
635 callbackIntent.setAction(callbackAction);
636 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
637
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800638 return serviceIntent;
639 }
640
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800641 private void renameGroup(Intent intent) {
642 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
643 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
644
645 if (groupId == -1) {
646 Log.e(TAG, "Invalid arguments for renameGroup request");
647 return;
648 }
649
650 ContentValues values = new ContentValues();
651 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700652 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
653 getContentResolver().update(groupUri, values, null, null);
654
655 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
656 callbackIntent.setData(groupUri);
657 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800658 }
659
660 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800661 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800662 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800663 public static Intent createGroupDeletionIntent(Context context, long groupId) {
664 Intent serviceIntent = new Intent(context, ContactSaveService.class);
665 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800666 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800667 return serviceIntent;
668 }
669
670 private void deleteGroup(Intent intent) {
671 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
672 if (groupId == -1) {
673 Log.e(TAG, "Invalid arguments for deleteGroup request");
674 return;
675 }
676
677 getContentResolver().delete(
678 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
679 }
680
681 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700682 * Creates an intent that can be sent to this service to rename a group as
683 * well as add and remove members from the group.
684 *
685 * @param context of the application
686 * @param groupId of the group that should be modified
687 * @param newLabel is the updated name of the group (can be null if the name
688 * should not be updated)
689 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
690 * should be added to the group
691 * @param rawContactsToRemove is an array of raw contact IDs for contacts
692 * that should be removed from the group
693 * @param callbackActivity is the activity to send the callback intent to
694 * @param callbackAction is the intent action for the callback intent
695 */
696 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
697 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700698 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700699 Intent serviceIntent = new Intent(context, ContactSaveService.class);
700 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
701 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
702 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
703 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
704 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
705 rawContactsToRemove);
706
707 // Callback intent will be invoked by the service once the group is updated
708 Intent callbackIntent = new Intent(context, callbackActivity);
709 callbackIntent.setAction(callbackAction);
710 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
711
712 return serviceIntent;
713 }
714
715 private void updateGroup(Intent intent) {
716 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
717 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
718 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
719 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
720
721 if (groupId == -1) {
722 Log.e(TAG, "Invalid arguments for updateGroup request");
723 return;
724 }
725
726 final ContentResolver resolver = getContentResolver();
727 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
728
729 // Update group name if necessary
730 if (label != null) {
731 ContentValues values = new ContentValues();
732 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700733 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700734 }
735
Katherine Kuan717e3432011-07-13 17:03:24 -0700736 // Add and remove members if necessary
737 addMembersToGroup(resolver, rawContactsToAdd, groupId);
738 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
739
740 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
741 callbackIntent.setData(groupUri);
742 deliverCallback(callbackIntent);
743 }
744
Daniel Lehmann18958a22012-02-28 17:45:25 -0800745 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700746 long groupId) {
747 if (rawContactsToAdd == null) {
748 return;
749 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700750 for (long rawContactId : rawContactsToAdd) {
751 try {
752 final ArrayList<ContentProviderOperation> rawContactOperations =
753 new ArrayList<ContentProviderOperation>();
754
755 // Build an assert operation to ensure the contact is not already in the group
756 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
757 .newAssertQuery(Data.CONTENT_URI);
758 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
759 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
760 new String[] { String.valueOf(rawContactId),
761 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
762 assertBuilder.withExpectedCount(0);
763 rawContactOperations.add(assertBuilder.build());
764
765 // Build an insert operation to add the contact to the group
766 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
767 .newInsert(Data.CONTENT_URI);
768 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
769 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
770 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
771 rawContactOperations.add(insertBuilder.build());
772
773 if (DEBUG) {
774 for (ContentProviderOperation operation : rawContactOperations) {
775 Log.v(TAG, operation.toString());
776 }
777 }
778
779 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700780 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800781 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700782 }
783 } catch (RemoteException e) {
784 // Something went wrong, bail without success
785 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
786 String.valueOf(rawContactId), e);
787 } catch (OperationApplicationException e) {
788 // The assert could have failed because the contact is already in the group,
789 // just continue to the next contact
790 Log.w(TAG, "Assert failed in adding raw contact ID " +
791 String.valueOf(rawContactId) + ". Already exists in group " +
792 String.valueOf(groupId), e);
793 }
794 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700795 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700796
Daniel Lehmann18958a22012-02-28 17:45:25 -0800797 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700798 long groupId) {
799 if (rawContactsToRemove == null) {
800 return;
801 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700802 for (long rawContactId : rawContactsToRemove) {
803 // Apply the delete operation on the data row for the given raw contact's
804 // membership in the given group. If no contact matches the provided selection, then
805 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800806 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700807 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
808 new String[] { String.valueOf(rawContactId),
809 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
810 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700811 }
812
813 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800814 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800815 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800816 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
817 Intent serviceIntent = new Intent(context, ContactSaveService.class);
818 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
819 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
820 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
821
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800822 return serviceIntent;
823 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800824
825 private void setStarred(Intent intent) {
826 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
827 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
828 if (contactUri == null) {
829 Log.e(TAG, "Invalid arguments for setStarred request");
830 return;
831 }
832
833 final ContentValues values = new ContentValues(1);
834 values.put(Contacts.STARRED, value);
835 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800836 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800837
838 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700839 * Creates an intent that can be sent to this service to set the redirect to voicemail.
840 */
841 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
842 boolean value) {
843 Intent serviceIntent = new Intent(context, ContactSaveService.class);
844 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
845 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
846 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
847
848 return serviceIntent;
849 }
850
851 private void setSendToVoicemail(Intent intent) {
852 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
853 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
854 if (contactUri == null) {
855 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
856 return;
857 }
858
859 final ContentValues values = new ContentValues(1);
860 values.put(Contacts.SEND_TO_VOICEMAIL, value);
861 getContentResolver().update(contactUri, values, null, null);
862 }
863
864 /**
865 * Creates an intent that can be sent to this service to save the contact's ringtone.
866 */
867 public static Intent createSetRingtone(Context context, Uri contactUri,
868 String value) {
869 Intent serviceIntent = new Intent(context, ContactSaveService.class);
870 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
871 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
872 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
873
874 return serviceIntent;
875 }
876
877 private void setRingtone(Intent intent) {
878 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
879 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
880 if (contactUri == null) {
881 Log.e(TAG, "Invalid arguments for setRingtone");
882 return;
883 }
884 ContentValues values = new ContentValues(1);
885 values.put(Contacts.CUSTOM_RINGTONE, value);
886 getContentResolver().update(contactUri, values, null, null);
887 }
888
889 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800890 * Creates an intent that sets the selected data item as super primary (default)
891 */
892 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
893 Intent serviceIntent = new Intent(context, ContactSaveService.class);
894 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
895 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
896 return serviceIntent;
897 }
898
899 private void setSuperPrimary(Intent intent) {
900 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
901 if (dataId == -1) {
902 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
903 return;
904 }
905
906 // Update the primary values in the data record.
907 ContentValues values = new ContentValues(1);
908 values.put(Data.IS_SUPER_PRIMARY, 1);
909 values.put(Data.IS_PRIMARY, 1);
910
911 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
912 values, null, null);
913 }
914
915 /**
916 * Creates an intent that clears the primary flag of all data items that belong to the same
917 * raw_contact as the given data item. Will only clear, if the data item was primary before
918 * this call
919 */
920 public static Intent createClearPrimaryIntent(Context context, long dataId) {
921 Intent serviceIntent = new Intent(context, ContactSaveService.class);
922 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
923 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
924 return serviceIntent;
925 }
926
927 private void clearPrimary(Intent intent) {
928 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
929 if (dataId == -1) {
930 Log.e(TAG, "Invalid arguments for clearPrimary request");
931 return;
932 }
933
934 // Update the primary values in the data record.
935 ContentValues values = new ContentValues(1);
936 values.put(Data.IS_SUPER_PRIMARY, 0);
937 values.put(Data.IS_PRIMARY, 0);
938
939 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
940 values, null, null);
941 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800942
943 /**
944 * Creates an intent that can be sent to this service to delete a contact.
945 */
946 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
947 Intent serviceIntent = new Intent(context, ContactSaveService.class);
948 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
949 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
950 return serviceIntent;
951 }
952
953 private void deleteContact(Intent intent) {
954 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
955 if (contactUri == null) {
956 Log.e(TAG, "Invalid arguments for deleteContact request");
957 return;
958 }
959
960 getContentResolver().delete(contactUri, null, null);
961 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800962
963 /**
964 * Creates an intent that can be sent to this service to join two contacts.
965 */
966 public static Intent createJoinContactsIntent(Context context, long contactId1,
967 long contactId2, boolean contactWritable,
Josh Garguse5d3f892012-04-11 11:56:15 -0700968 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800969 Intent serviceIntent = new Intent(context, ContactSaveService.class);
970 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
971 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
972 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
973 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
974
975 // Callback intent will be invoked by the service once the contacts are joined.
976 Intent callbackIntent = new Intent(context, callbackActivity);
977 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800978 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
979
980 return serviceIntent;
981 }
982
983
984 private interface JoinContactQuery {
985 String[] PROJECTION = {
986 RawContacts._ID,
987 RawContacts.CONTACT_ID,
988 RawContacts.NAME_VERIFIED,
989 RawContacts.DISPLAY_NAME_SOURCE,
990 };
991
992 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
993
994 int _ID = 0;
995 int CONTACT_ID = 1;
996 int NAME_VERIFIED = 2;
997 int DISPLAY_NAME_SOURCE = 3;
998 }
999
1000 private void joinContacts(Intent intent) {
1001 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1002 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
1003 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
1004 if (contactId1 == -1 || contactId2 == -1) {
1005 Log.e(TAG, "Invalid arguments for joinContacts request");
1006 return;
1007 }
1008
1009 final ContentResolver resolver = getContentResolver();
1010
1011 // Load raw contact IDs for all raw contacts involved - currently edited and selected
1012 // in the join UIs
1013 Cursor c = resolver.query(RawContacts.CONTENT_URI,
1014 JoinContactQuery.PROJECTION,
1015 JoinContactQuery.SELECTION,
1016 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1017
1018 long rawContactIds[];
1019 long verifiedNameRawContactId = -1;
1020 try {
1021 int maxDisplayNameSource = -1;
1022 rawContactIds = new long[c.getCount()];
1023 for (int i = 0; i < rawContactIds.length; i++) {
1024 c.moveToPosition(i);
1025 long rawContactId = c.getLong(JoinContactQuery._ID);
1026 rawContactIds[i] = rawContactId;
1027 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1028 if (nameSource > maxDisplayNameSource) {
1029 maxDisplayNameSource = nameSource;
1030 }
1031 }
1032
1033 // Find an appropriate display name for the joined contact:
1034 // if should have a higher DisplayNameSource or be the name
1035 // of the original contact that we are joining with another.
1036 if (writable) {
1037 for (int i = 0; i < rawContactIds.length; i++) {
1038 c.moveToPosition(i);
1039 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1040 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1041 if (nameSource == maxDisplayNameSource
1042 && (verifiedNameRawContactId == -1
1043 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1044 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1045 }
1046 }
1047 }
1048 }
1049 } finally {
1050 c.close();
1051 }
1052
1053 // For each pair of raw contacts, insert an aggregation exception
1054 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1055 for (int i = 0; i < rawContactIds.length; i++) {
1056 for (int j = 0; j < rawContactIds.length; j++) {
1057 if (i != j) {
1058 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1059 }
1060 }
1061 }
1062
1063 // Mark the original contact as "name verified" to make sure that the contact
1064 // display name does not change as a result of the join
1065 if (verifiedNameRawContactId != -1) {
1066 Builder builder = ContentProviderOperation.newUpdate(
1067 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1068 builder.withValue(RawContacts.NAME_VERIFIED, 1);
1069 operations.add(builder.build());
1070 }
1071
1072 boolean success = false;
1073 // Apply all aggregation exceptions as one batch
1074 try {
1075 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001076 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001077 success = true;
1078 } catch (RemoteException e) {
1079 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001080 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001081 } catch (OperationApplicationException e) {
1082 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001083 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001084 }
1085
1086 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1087 if (success) {
1088 Uri uri = RawContacts.getContactLookupUri(resolver,
1089 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1090 callbackIntent.setData(uri);
1091 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001092 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001093 }
1094
1095 /**
1096 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1097 */
1098 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1099 long rawContactId1, long rawContactId2) {
1100 Builder builder =
1101 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1102 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1103 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1104 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1105 operations.add(builder.build());
1106 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001107
1108 /**
1109 * Shows a toast on the UI thread.
1110 */
1111 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001112 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001113
1114 @Override
1115 public void run() {
1116 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1117 }
1118 });
1119 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001120
1121 private void deliverCallback(final Intent callbackIntent) {
1122 mMainHandler.post(new Runnable() {
1123
1124 @Override
1125 public void run() {
1126 deliverCallbackOnUiThread(callbackIntent);
1127 }
1128 });
1129 }
1130
1131 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1132 // TODO: this assumes that if there are multiple instances of the same
1133 // activity registered, the last one registered is the one waiting for
1134 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001135 for (Listener listener : sListeners) {
1136 if (callbackIntent.getComponent().equals(
1137 ((Activity) listener).getIntent().getComponent())) {
1138 listener.onServiceCompleted(callbackIntent);
1139 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001140 }
1141 }
1142 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001143}