blob: fdfd0f7bde202f8a3eed56d19ac9ebba8bae9404 [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,
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 Garguse692e012012-01-18 14:53:11 -0800509 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800510 } catch (IOException e) {
511 Log.e(TAG, "Failed to write photo: " + photoFile.toString() + " because: " + e);
512 return false;
Josh Garguse692e012012-01-18 14:53:11 -0800513 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800514 return true;
Josh Garguse692e012012-01-18 14:53:11 -0800515 }
516
Josh Gargusef15c8e2012-01-30 16:42:02 -0800517 /**
518 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
519 */
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800520 private long getRawContactId(EntityDeltaList state,
521 final ArrayList<ContentProviderOperation> diff,
522 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800523 long existingRawContactId = state.findRawContactId();
524 if (existingRawContactId != -1) {
525 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800526 }
527
Josh Gargusef15c8e2012-01-30 16:42:02 -0800528 return getInsertedRawContactId(diff, results);
529 }
530
531 /**
532 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
533 */
534 private long getInsertedRawContactId(
535 final ArrayList<ContentProviderOperation> diff,
536 final ContentProviderResult[] results) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800537 final int diffSize = diff.size();
538 for (int i = 0; i < diffSize; i++) {
539 ContentProviderOperation operation = diff.get(i);
540 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
541 && operation.getUri().getEncodedPath().contains(
542 RawContacts.CONTENT_URI.getEncodedPath())) {
543 return ContentUris.parseId(results[i].uri);
544 }
545 }
546 return -1;
547 }
548
549 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700550 * Creates an intent that can be sent to this service to create a new group as
551 * well as add new members at the same time.
552 *
553 * @param context of the application
554 * @param account in which the group should be created
555 * @param label is the name of the group (cannot be null)
556 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
557 * should be added to the group
558 * @param callbackActivity is the activity to send the callback intent to
559 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700560 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700561 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700562 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700563 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800564 Intent serviceIntent = new Intent(context, ContactSaveService.class);
565 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
566 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
567 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700568 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800569 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700570 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700571
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800572 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700573 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800574 Intent callbackIntent = new Intent(context, callbackActivity);
575 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700576 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800577
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700578 return serviceIntent;
579 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800580
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800581 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700582 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
583 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
584 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
585 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700586 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800587
588 ContentValues values = new ContentValues();
589 values.put(Groups.ACCOUNT_TYPE, accountType);
590 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700591 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800592 values.put(Groups.TITLE, label);
593
Katherine Kuan717e3432011-07-13 17:03:24 -0700594 final ContentResolver resolver = getContentResolver();
595
596 // Create the new group
597 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
598
599 // If there's no URI, then the insertion failed. Abort early because group members can't be
600 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800601 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700602 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800603 return;
604 }
605
Katherine Kuan717e3432011-07-13 17:03:24 -0700606 // Add new group members
607 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
608
609 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
610 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800611 values.clear();
612 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
613 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
614
615 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700616 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700617 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800618 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800619 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800620 }
621
622 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800623 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800624 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700625 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700626 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800627 Intent serviceIntent = new Intent(context, ContactSaveService.class);
628 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
629 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
630 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700631
632 // Callback intent will be invoked by the service once the group is renamed.
633 Intent callbackIntent = new Intent(context, callbackActivity);
634 callbackIntent.setAction(callbackAction);
635 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
636
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800637 return serviceIntent;
638 }
639
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800640 private void renameGroup(Intent intent) {
641 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
642 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
643
644 if (groupId == -1) {
645 Log.e(TAG, "Invalid arguments for renameGroup request");
646 return;
647 }
648
649 ContentValues values = new ContentValues();
650 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700651 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
652 getContentResolver().update(groupUri, values, null, null);
653
654 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
655 callbackIntent.setData(groupUri);
656 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800657 }
658
659 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800660 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800661 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800662 public static Intent createGroupDeletionIntent(Context context, long groupId) {
663 Intent serviceIntent = new Intent(context, ContactSaveService.class);
664 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800665 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800666 return serviceIntent;
667 }
668
669 private void deleteGroup(Intent intent) {
670 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
671 if (groupId == -1) {
672 Log.e(TAG, "Invalid arguments for deleteGroup request");
673 return;
674 }
675
676 getContentResolver().delete(
677 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
678 }
679
680 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700681 * Creates an intent that can be sent to this service to rename a group as
682 * well as add and remove members from the group.
683 *
684 * @param context of the application
685 * @param groupId of the group that should be modified
686 * @param newLabel is the updated name of the group (can be null if the name
687 * should not be updated)
688 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
689 * should be added to the group
690 * @param rawContactsToRemove is an array of raw contact IDs for contacts
691 * that should be removed from the group
692 * @param callbackActivity is the activity to send the callback intent to
693 * @param callbackAction is the intent action for the callback intent
694 */
695 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
696 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700697 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700698 Intent serviceIntent = new Intent(context, ContactSaveService.class);
699 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
700 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
701 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
702 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
703 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
704 rawContactsToRemove);
705
706 // Callback intent will be invoked by the service once the group is updated
707 Intent callbackIntent = new Intent(context, callbackActivity);
708 callbackIntent.setAction(callbackAction);
709 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
710
711 return serviceIntent;
712 }
713
714 private void updateGroup(Intent intent) {
715 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
716 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
717 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
718 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
719
720 if (groupId == -1) {
721 Log.e(TAG, "Invalid arguments for updateGroup request");
722 return;
723 }
724
725 final ContentResolver resolver = getContentResolver();
726 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
727
728 // Update group name if necessary
729 if (label != null) {
730 ContentValues values = new ContentValues();
731 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700732 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700733 }
734
Katherine Kuan717e3432011-07-13 17:03:24 -0700735 // Add and remove members if necessary
736 addMembersToGroup(resolver, rawContactsToAdd, groupId);
737 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
738
739 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
740 callbackIntent.setData(groupUri);
741 deliverCallback(callbackIntent);
742 }
743
Daniel Lehmann18958a22012-02-28 17:45:25 -0800744 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700745 long groupId) {
746 if (rawContactsToAdd == null) {
747 return;
748 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700749 for (long rawContactId : rawContactsToAdd) {
750 try {
751 final ArrayList<ContentProviderOperation> rawContactOperations =
752 new ArrayList<ContentProviderOperation>();
753
754 // Build an assert operation to ensure the contact is not already in the group
755 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
756 .newAssertQuery(Data.CONTENT_URI);
757 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
758 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
759 new String[] { String.valueOf(rawContactId),
760 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
761 assertBuilder.withExpectedCount(0);
762 rawContactOperations.add(assertBuilder.build());
763
764 // Build an insert operation to add the contact to the group
765 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
766 .newInsert(Data.CONTENT_URI);
767 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
768 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
769 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
770 rawContactOperations.add(insertBuilder.build());
771
772 if (DEBUG) {
773 for (ContentProviderOperation operation : rawContactOperations) {
774 Log.v(TAG, operation.toString());
775 }
776 }
777
778 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700779 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800780 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700781 }
782 } catch (RemoteException e) {
783 // Something went wrong, bail without success
784 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
785 String.valueOf(rawContactId), e);
786 } catch (OperationApplicationException e) {
787 // The assert could have failed because the contact is already in the group,
788 // just continue to the next contact
789 Log.w(TAG, "Assert failed in adding raw contact ID " +
790 String.valueOf(rawContactId) + ". Already exists in group " +
791 String.valueOf(groupId), e);
792 }
793 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700794 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700795
Daniel Lehmann18958a22012-02-28 17:45:25 -0800796 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700797 long groupId) {
798 if (rawContactsToRemove == null) {
799 return;
800 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700801 for (long rawContactId : rawContactsToRemove) {
802 // Apply the delete operation on the data row for the given raw contact's
803 // membership in the given group. If no contact matches the provided selection, then
804 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800805 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700806 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
807 new String[] { String.valueOf(rawContactId),
808 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
809 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700810 }
811
812 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800813 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800814 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800815 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
816 Intent serviceIntent = new Intent(context, ContactSaveService.class);
817 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
818 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
819 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
820
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800821 return serviceIntent;
822 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800823
824 private void setStarred(Intent intent) {
825 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
826 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
827 if (contactUri == null) {
828 Log.e(TAG, "Invalid arguments for setStarred request");
829 return;
830 }
831
832 final ContentValues values = new ContentValues(1);
833 values.put(Contacts.STARRED, value);
834 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800835 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800836
837 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700838 * Creates an intent that can be sent to this service to set the redirect to voicemail.
839 */
840 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
841 boolean value) {
842 Intent serviceIntent = new Intent(context, ContactSaveService.class);
843 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
844 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
845 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
846
847 return serviceIntent;
848 }
849
850 private void setSendToVoicemail(Intent intent) {
851 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
852 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
853 if (contactUri == null) {
854 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
855 return;
856 }
857
858 final ContentValues values = new ContentValues(1);
859 values.put(Contacts.SEND_TO_VOICEMAIL, value);
860 getContentResolver().update(contactUri, values, null, null);
861 }
862
863 /**
864 * Creates an intent that can be sent to this service to save the contact's ringtone.
865 */
866 public static Intent createSetRingtone(Context context, Uri contactUri,
867 String value) {
868 Intent serviceIntent = new Intent(context, ContactSaveService.class);
869 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
870 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
871 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
872
873 return serviceIntent;
874 }
875
876 private void setRingtone(Intent intent) {
877 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
878 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
879 if (contactUri == null) {
880 Log.e(TAG, "Invalid arguments for setRingtone");
881 return;
882 }
883 ContentValues values = new ContentValues(1);
884 values.put(Contacts.CUSTOM_RINGTONE, value);
885 getContentResolver().update(contactUri, values, null, null);
886 }
887
888 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800889 * Creates an intent that sets the selected data item as super primary (default)
890 */
891 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
892 Intent serviceIntent = new Intent(context, ContactSaveService.class);
893 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
894 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
895 return serviceIntent;
896 }
897
898 private void setSuperPrimary(Intent intent) {
899 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
900 if (dataId == -1) {
901 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
902 return;
903 }
904
905 // Update the primary values in the data record.
906 ContentValues values = new ContentValues(1);
907 values.put(Data.IS_SUPER_PRIMARY, 1);
908 values.put(Data.IS_PRIMARY, 1);
909
910 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
911 values, null, null);
912 }
913
914 /**
915 * Creates an intent that clears the primary flag of all data items that belong to the same
916 * raw_contact as the given data item. Will only clear, if the data item was primary before
917 * this call
918 */
919 public static Intent createClearPrimaryIntent(Context context, long dataId) {
920 Intent serviceIntent = new Intent(context, ContactSaveService.class);
921 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
922 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
923 return serviceIntent;
924 }
925
926 private void clearPrimary(Intent intent) {
927 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
928 if (dataId == -1) {
929 Log.e(TAG, "Invalid arguments for clearPrimary request");
930 return;
931 }
932
933 // Update the primary values in the data record.
934 ContentValues values = new ContentValues(1);
935 values.put(Data.IS_SUPER_PRIMARY, 0);
936 values.put(Data.IS_PRIMARY, 0);
937
938 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
939 values, null, null);
940 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800941
942 /**
943 * Creates an intent that can be sent to this service to delete a contact.
944 */
945 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
946 Intent serviceIntent = new Intent(context, ContactSaveService.class);
947 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
948 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
949 return serviceIntent;
950 }
951
952 private void deleteContact(Intent intent) {
953 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
954 if (contactUri == null) {
955 Log.e(TAG, "Invalid arguments for deleteContact request");
956 return;
957 }
958
959 getContentResolver().delete(contactUri, null, null);
960 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800961
962 /**
963 * Creates an intent that can be sent to this service to join two contacts.
964 */
965 public static Intent createJoinContactsIntent(Context context, long contactId1,
966 long contactId2, boolean contactWritable,
Josh Garguse5d3f892012-04-11 11:56:15 -0700967 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800968 Intent serviceIntent = new Intent(context, ContactSaveService.class);
969 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
970 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
971 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
972 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
973
974 // Callback intent will be invoked by the service once the contacts are joined.
975 Intent callbackIntent = new Intent(context, callbackActivity);
976 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800977 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
978
979 return serviceIntent;
980 }
981
982
983 private interface JoinContactQuery {
984 String[] PROJECTION = {
985 RawContacts._ID,
986 RawContacts.CONTACT_ID,
987 RawContacts.NAME_VERIFIED,
988 RawContacts.DISPLAY_NAME_SOURCE,
989 };
990
991 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
992
993 int _ID = 0;
994 int CONTACT_ID = 1;
995 int NAME_VERIFIED = 2;
996 int DISPLAY_NAME_SOURCE = 3;
997 }
998
999 private void joinContacts(Intent intent) {
1000 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1001 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
1002 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
1003 if (contactId1 == -1 || contactId2 == -1) {
1004 Log.e(TAG, "Invalid arguments for joinContacts request");
1005 return;
1006 }
1007
1008 final ContentResolver resolver = getContentResolver();
1009
1010 // Load raw contact IDs for all raw contacts involved - currently edited and selected
1011 // in the join UIs
1012 Cursor c = resolver.query(RawContacts.CONTENT_URI,
1013 JoinContactQuery.PROJECTION,
1014 JoinContactQuery.SELECTION,
1015 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1016
1017 long rawContactIds[];
1018 long verifiedNameRawContactId = -1;
1019 try {
1020 int maxDisplayNameSource = -1;
1021 rawContactIds = new long[c.getCount()];
1022 for (int i = 0; i < rawContactIds.length; i++) {
1023 c.moveToPosition(i);
1024 long rawContactId = c.getLong(JoinContactQuery._ID);
1025 rawContactIds[i] = rawContactId;
1026 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1027 if (nameSource > maxDisplayNameSource) {
1028 maxDisplayNameSource = nameSource;
1029 }
1030 }
1031
1032 // Find an appropriate display name for the joined contact:
1033 // if should have a higher DisplayNameSource or be the name
1034 // of the original contact that we are joining with another.
1035 if (writable) {
1036 for (int i = 0; i < rawContactIds.length; i++) {
1037 c.moveToPosition(i);
1038 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1039 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1040 if (nameSource == maxDisplayNameSource
1041 && (verifiedNameRawContactId == -1
1042 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1043 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1044 }
1045 }
1046 }
1047 }
1048 } finally {
1049 c.close();
1050 }
1051
1052 // For each pair of raw contacts, insert an aggregation exception
1053 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1054 for (int i = 0; i < rawContactIds.length; i++) {
1055 for (int j = 0; j < rawContactIds.length; j++) {
1056 if (i != j) {
1057 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1058 }
1059 }
1060 }
1061
1062 // Mark the original contact as "name verified" to make sure that the contact
1063 // display name does not change as a result of the join
1064 if (verifiedNameRawContactId != -1) {
1065 Builder builder = ContentProviderOperation.newUpdate(
1066 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1067 builder.withValue(RawContacts.NAME_VERIFIED, 1);
1068 operations.add(builder.build());
1069 }
1070
1071 boolean success = false;
1072 // Apply all aggregation exceptions as one batch
1073 try {
1074 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001075 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001076 success = true;
1077 } catch (RemoteException e) {
1078 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001079 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001080 } catch (OperationApplicationException e) {
1081 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001082 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001083 }
1084
1085 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1086 if (success) {
1087 Uri uri = RawContacts.getContactLookupUri(resolver,
1088 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1089 callbackIntent.setData(uri);
1090 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001091 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001092 }
1093
1094 /**
1095 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1096 */
1097 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1098 long rawContactId1, long rawContactId2) {
1099 Builder builder =
1100 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1101 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1102 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1103 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1104 operations.add(builder.build());
1105 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001106
1107 /**
1108 * Shows a toast on the UI thread.
1109 */
1110 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001111 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001112
1113 @Override
1114 public void run() {
1115 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1116 }
1117 });
1118 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001119
1120 private void deliverCallback(final Intent callbackIntent) {
1121 mMainHandler.post(new Runnable() {
1122
1123 @Override
1124 public void run() {
1125 deliverCallbackOnUiThread(callbackIntent);
1126 }
1127 });
1128 }
1129
1130 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1131 // TODO: this assumes that if there are multiple instances of the same
1132 // activity registered, the last one registered is the one waiting for
1133 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001134 for (Listener listener : sListeners) {
1135 if (callbackIntent.getComponent().equals(
1136 ((Activity) listener).getIntent().getComponent())) {
1137 listener.onServiceCompleted(callbackIntent);
1138 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001139 }
1140 }
1141 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001142}