blob: f47560937d7dcfb1cfab5f2e15d9216d1d0e3c75 [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;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080024import com.google.android.collect.Lists;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070025import com.google.android.collect.Sets;
26
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080027import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070028import android.app.IntentService;
29import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080030import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070031import android.content.ContentProviderResult;
32import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080033import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070034import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080035import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070036import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080037import android.content.OperationApplicationException;
38import android.database.Cursor;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070039import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080040import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080041import android.os.Handler;
42import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080043import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080044import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070045import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080046import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080047import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080048import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070049import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080050import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070051import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070052import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070053import android.provider.ContactsContract.RawContactsEntity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070054import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080055import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070056
Josh Garguse692e012012-01-18 14:53:11 -080057import java.io.File;
58import java.io.FileInputStream;
59import java.io.FileOutputStream;
60import java.io.IOException;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080061import java.util.ArrayList;
62import java.util.HashSet;
63import java.util.List;
64import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070065
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080066/**
67 * A service responsible for saving changes to the content provider.
68 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070069public class ContactSaveService extends IntentService {
70 private static final String TAG = "ContactSaveService";
71
Katherine Kuana007e442011-07-07 09:25:34 -070072 /** Set to true in order to view logs on content provider operations */
73 private static final boolean DEBUG = false;
74
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070075 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
76
77 public static final String EXTRA_ACCOUNT_NAME = "accountName";
78 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070079 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070080 public static final String EXTRA_CONTENT_VALUES = "contentValues";
81 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
82
Dmitri Plotnikova0114142011-02-15 13:53:21 -080083 public static final String ACTION_SAVE_CONTACT = "saveContact";
84 public static final String EXTRA_CONTACT_STATE = "state";
85 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070086 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070087 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080088 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070089
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080090 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080091 public static final String ACTION_RENAME_GROUP = "renameGroup";
92 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070093 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080094 public static final String EXTRA_GROUP_ID = "groupId";
95 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070096 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
97 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080098
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080099 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800100 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800101 public static final String EXTRA_CONTACT_URI = "contactUri";
102 public static final String EXTRA_STARRED_FLAG = "starred";
103
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800104 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
105 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
106 public static final String EXTRA_DATA_ID = "dataId";
107
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800108 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
109 public static final String EXTRA_CONTACT_ID1 = "contactId1";
110 public static final String EXTRA_CONTACT_ID2 = "contactId2";
111 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
112
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700113 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
114 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
115
116 public static final String ACTION_SET_RINGTONE = "setRingtone";
117 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
118
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700119 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
120 Data.MIMETYPE,
121 Data.IS_PRIMARY,
122 Data.DATA1,
123 Data.DATA2,
124 Data.DATA3,
125 Data.DATA4,
126 Data.DATA5,
127 Data.DATA6,
128 Data.DATA7,
129 Data.DATA8,
130 Data.DATA9,
131 Data.DATA10,
132 Data.DATA11,
133 Data.DATA12,
134 Data.DATA13,
135 Data.DATA14,
136 Data.DATA15
137 );
138
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800139 private static final int PERSIST_TRIES = 3;
140
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800141 public interface Listener {
142 public void onServiceCompleted(Intent callbackIntent);
143 }
144
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100145 private static final CopyOnWriteArrayList<Listener> sListeners =
146 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800147
148 private Handler mMainHandler;
149
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700150 public ContactSaveService() {
151 super(TAG);
152 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800153 mMainHandler = new Handler(Looper.getMainLooper());
154 }
155
156 public static void registerListener(Listener listener) {
157 if (!(listener instanceof Activity)) {
158 throw new ClassCastException("Only activities can be registered to"
159 + " receive callback from " + ContactSaveService.class.getName());
160 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100161 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800162 }
163
164 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100165 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700166 }
167
168 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800169 public Object getSystemService(String name) {
170 Object service = super.getSystemService(name);
171 if (service != null) {
172 return service;
173 }
174
175 return getApplicationContext().getSystemService(name);
176 }
177
178 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700179 protected void onHandleIntent(Intent intent) {
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700180 String action = intent.getAction();
181 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
182 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800183 } else if (ACTION_SAVE_CONTACT.equals(action)) {
184 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800185 } else if (ACTION_CREATE_GROUP.equals(action)) {
186 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800187 } else if (ACTION_RENAME_GROUP.equals(action)) {
188 renameGroup(intent);
189 } else if (ACTION_DELETE_GROUP.equals(action)) {
190 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700191 } else if (ACTION_UPDATE_GROUP.equals(action)) {
192 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800193 } else if (ACTION_SET_STARRED.equals(action)) {
194 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800195 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
196 setSuperPrimary(intent);
197 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
198 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800199 } else if (ACTION_DELETE_CONTACT.equals(action)) {
200 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800201 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
202 joinContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700203 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
204 setSendToVoicemail(intent);
205 } else if (ACTION_SET_RINGTONE.equals(action)) {
206 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700207 }
208 }
209
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800210 /**
211 * Creates an intent that can be sent to this service to create a new raw contact
212 * using data presented as a set of ContentValues.
213 */
214 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700215 ArrayList<ContentValues> values, AccountWithDataSet account,
216 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800217 Intent serviceIntent = new Intent(
218 context, ContactSaveService.class);
219 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
220 if (account != null) {
221 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
222 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700223 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800224 }
225 serviceIntent.putParcelableArrayListExtra(
226 ContactSaveService.EXTRA_CONTENT_VALUES, values);
227
228 // Callback intent will be invoked by the service once the new contact is
229 // created. The service will put the URI of the new contact as "data" on
230 // the callback intent.
231 Intent callbackIntent = new Intent(context, callbackActivity);
232 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800233 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
234 return serviceIntent;
235 }
236
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700237 private void createRawContact(Intent intent) {
238 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
239 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700240 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700241 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
242 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
243
244 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
245 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
246 .withValue(RawContacts.ACCOUNT_NAME, accountName)
247 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700248 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700249 .build());
250
251 int size = valueList.size();
252 for (int i = 0; i < size; i++) {
253 ContentValues values = valueList.get(i);
254 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
255 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
256 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
257 .withValues(values)
258 .build());
259 }
260
261 ContentResolver resolver = getContentResolver();
262 ContentProviderResult[] results;
263 try {
264 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
265 } catch (Exception e) {
266 throw new RuntimeException("Failed to store new contact", e);
267 }
268
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700269 Uri rawContactUri = results[0].uri;
270 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
271
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800272 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700273 }
274
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700275 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800276 * Creates an intent that can be sent to this service to create a new raw contact
277 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800278 * This variant is more convenient to use when there is only one photo that can
279 * possibly be updated, as in the Contact Details screen.
280 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
281 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800282 */
283 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700284 String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
Josh Garguse692e012012-01-18 14:53:11 -0800285 String callbackAction, long rawContactId, String updatedPhotoPath) {
286 Bundle bundle = new Bundle();
287 bundle.putString(String.valueOf(rawContactId), updatedPhotoPath);
288 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
289 callbackActivity, callbackAction, bundle);
290 }
291
292 /**
293 * Creates an intent that can be sent to this service to create a new raw contact
294 * using data presented as a set of ContentValues.
295 * This variant is used when multiple contacts' photos may be updated, as in the
296 * Contact Editor.
297 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
298 */
299 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
300 String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
301 String callbackAction, Bundle updatedPhotos) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800302 Intent serviceIntent = new Intent(
303 context, ContactSaveService.class);
304 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
305 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700306 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Josh Garguse692e012012-01-18 14:53:11 -0800307 if (updatedPhotos != null) {
308 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
309 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800310
311 // Callback intent will be invoked by the service once the contact is
312 // saved. The service will put the URI of the new contact as "data" on
313 // the callback intent.
314 Intent callbackIntent = new Intent(context, callbackActivity);
315 callbackIntent.putExtra(saveModeExtraKey, saveMode);
316 callbackIntent.setAction(callbackAction);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800317 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
318 return serviceIntent;
319 }
320
321 private void saveContact(Intent intent) {
322 EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
323 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700324 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800325 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800326
327 // Trim any empty fields, and RawContacts, before persisting
328 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
329 EntityModifier.trimEmpty(state, accountTypes);
330
331 Uri lookupUri = null;
332
333 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800334 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800335
Josh Gargusef15c8e2012-01-30 16:42:02 -0800336 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
337 long insertedRawContactId = -1;
338
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800339 // Attempt to persist changes
340 int tries = 0;
341 while (tries++ < PERSIST_TRIES) {
342 try {
343 // Build operations and try applying
344 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700345 if (DEBUG) {
346 Log.v(TAG, "Content Provider Operations:");
347 for (ContentProviderOperation operation : diff) {
348 Log.v(TAG, operation.toString());
349 }
350 }
351
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800352 ContentProviderResult[] results = null;
353 if (!diff.isEmpty()) {
354 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
355 }
356
357 final long rawContactId = getRawContactId(state, diff, results);
358 if (rawContactId == -1) {
359 throw new IllegalStateException("Could not determine RawContact ID after save");
360 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800361 // We don't have to check to see if the value is still -1. If we reach here,
362 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
363 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700364 if (isProfile) {
365 // Since the profile supports local raw contacts, which may have been completely
366 // removed if all information was removed, we need to do a special query to
367 // get the lookup URI for the profile contact (if it still exists).
368 Cursor c = resolver.query(Profile.CONTENT_URI,
369 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
370 null, null, null);
371 try {
Erik162b7e32011-09-20 15:23:55 -0700372 if (c.moveToFirst()) {
373 final long contactId = c.getLong(0);
374 final String lookupKey = c.getString(1);
375 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
376 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700377 } finally {
378 c.close();
379 }
380 } else {
381 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
382 rawContactId);
383 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
384 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800385 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
Josh Garguse692e012012-01-18 14:53:11 -0800386
387 // We can change this back to false later, if we fail to save the contact photo.
388 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800389 break;
390
391 } catch (RemoteException e) {
392 // Something went wrong, bail without success
393 Log.e(TAG, "Problem persisting user edits", e);
394 break;
395
396 } catch (OperationApplicationException e) {
397 // Version consistency failed, re-parent change and try again
398 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
399 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
400 boolean first = true;
401 final int count = state.size();
402 for (int i = 0; i < count; i++) {
403 Long rawContactId = state.getRawContactId(i);
404 if (rawContactId != null && rawContactId != -1) {
405 if (!first) {
406 sb.append(',');
407 }
408 sb.append(rawContactId);
409 first = false;
410 }
411 }
412 sb.append(")");
413
414 if (first) {
415 throw new IllegalStateException("Version consistency failed for a new contact");
416 }
417
Dave Santoroc90f95e2011-09-07 17:47:15 -0700418 final EntityDeltaList newState = EntityDeltaList.fromQuery(
419 isProfile
420 ? RawContactsEntity.PROFILE_CONTENT_URI
421 : RawContactsEntity.CONTENT_URI,
422 resolver, sb.toString(), null, null);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800423 state = EntityDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700424
425 // Update the new state to use profile URIs if appropriate.
426 if (isProfile) {
427 for (EntityDelta delta : state) {
428 delta.setProfileQueryUri();
429 }
430 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800431 }
432 }
433
Josh Garguse692e012012-01-18 14:53:11 -0800434 // Now save any updated photos. We do this at the end to ensure that
435 // the ContactProvider already knows about newly-created contacts.
436 if (updatedPhotos != null) {
437 for (String key : updatedPhotos.keySet()) {
438 String photoFilePath = updatedPhotos.getString(key);
439 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800440
441 // If the raw-contact ID is negative, we are saving a new raw-contact;
442 // replace the bogus ID with the new one that we actually saved the contact at.
443 if (rawContactId < 0) {
444 rawContactId = insertedRawContactId;
445 if (rawContactId == -1) {
446 throw new IllegalStateException(
447 "Could not determine RawContact ID for image insertion");
448 }
449 }
450
Josh Garguse692e012012-01-18 14:53:11 -0800451 File photoFile = new File(photoFilePath);
452 if (!saveUpdatedPhoto(rawContactId, photoFile)) succeeded = false;
453 }
454 }
455
456 if (succeeded) {
457 // Mark the intent to indicate that the save was successful (even if the lookup URI
458 // is now null). For local contacts or the local profile, it's possible that the
459 // save triggered removal of the contact, so no lookup URI would exist..
460 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
461 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800462 callbackIntent.setData(lookupUri);
463
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800464 deliverCallback(callbackIntent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800465 }
466
Josh Garguse692e012012-01-18 14:53:11 -0800467 /**
468 * Save updated photo for the specified raw-contact.
469 * @return true for success, false for failure
470 */
471 private boolean saveUpdatedPhoto(long rawContactId, File photoFile) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800472 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800473 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
474 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
475
Josh Garguse692e012012-01-18 14:53:11 -0800476 try {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800477 final FileOutputStream outputStream = getContentResolver()
478 .openAssetFileDescriptor(outputUri, "rw").createOutputStream();
Josh Garguse692e012012-01-18 14:53:11 -0800479 try {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800480 final FileInputStream inputStream = new FileInputStream(photoFile);
481 try {
482 final byte[] buffer = new byte[16 * 1024];
483 int length;
484 int totalLength = 0;
485 while ((length = inputStream.read(buffer)) > 0) {
486 outputStream.write(buffer, 0, length);
487 totalLength += length;
488 }
489 Log.v(TAG, "Wrote " + totalLength + " bytes for photo " + photoFile.toString());
490 } finally {
491 inputStream.close();
492 }
493 } finally {
Josh Garguse692e012012-01-18 14:53:11 -0800494 outputStream.close();
Josh Garguse692e012012-01-18 14:53:11 -0800495 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800496 } catch (IOException e) {
497 Log.e(TAG, "Failed to write photo: " + photoFile.toString() + " because: " + e);
498 return false;
Josh Garguse692e012012-01-18 14:53:11 -0800499 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800500 return true;
Josh Garguse692e012012-01-18 14:53:11 -0800501 }
502
Josh Gargusef15c8e2012-01-30 16:42:02 -0800503 /**
504 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
505 */
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800506 private long getRawContactId(EntityDeltaList state,
507 final ArrayList<ContentProviderOperation> diff,
508 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800509 long existingRawContactId = state.findRawContactId();
510 if (existingRawContactId != -1) {
511 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800512 }
513
Josh Gargusef15c8e2012-01-30 16:42:02 -0800514 return getInsertedRawContactId(diff, results);
515 }
516
517 /**
518 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
519 */
520 private long getInsertedRawContactId(
521 final ArrayList<ContentProviderOperation> diff,
522 final ContentProviderResult[] results) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800523 final int diffSize = diff.size();
524 for (int i = 0; i < diffSize; i++) {
525 ContentProviderOperation operation = diff.get(i);
526 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
527 && operation.getUri().getEncodedPath().contains(
528 RawContacts.CONTENT_URI.getEncodedPath())) {
529 return ContentUris.parseId(results[i].uri);
530 }
531 }
532 return -1;
533 }
534
535 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700536 * Creates an intent that can be sent to this service to create a new group as
537 * well as add new members at the same time.
538 *
539 * @param context of the application
540 * @param account in which the group should be created
541 * @param label is the name of the group (cannot be null)
542 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
543 * should be added to the group
544 * @param callbackActivity is the activity to send the callback intent to
545 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700546 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700547 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Katherine Kuan717e3432011-07-13 17:03:24 -0700548 String label, long[] rawContactsToAdd, Class<?> callbackActivity,
549 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800550 Intent serviceIntent = new Intent(context, ContactSaveService.class);
551 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
552 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
553 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700554 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800555 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700556 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700557
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800558 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700559 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800560 Intent callbackIntent = new Intent(context, callbackActivity);
561 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700562 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800563
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700564 return serviceIntent;
565 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800566
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800567 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700568 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
569 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
570 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
571 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700572 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800573
574 ContentValues values = new ContentValues();
575 values.put(Groups.ACCOUNT_TYPE, accountType);
576 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700577 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800578 values.put(Groups.TITLE, label);
579
Katherine Kuan717e3432011-07-13 17:03:24 -0700580 final ContentResolver resolver = getContentResolver();
581
582 // Create the new group
583 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
584
585 // If there's no URI, then the insertion failed. Abort early because group members can't be
586 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800587 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700588 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800589 return;
590 }
591
Katherine Kuan717e3432011-07-13 17:03:24 -0700592 // Add new group members
593 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
594
595 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
596 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800597 values.clear();
598 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
599 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
600
601 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700602 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700603 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800604 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800605 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800606 }
607
608 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800609 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800610 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700611 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
612 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800613 Intent serviceIntent = new Intent(context, ContactSaveService.class);
614 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
615 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
616 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700617
618 // Callback intent will be invoked by the service once the group is renamed.
619 Intent callbackIntent = new Intent(context, callbackActivity);
620 callbackIntent.setAction(callbackAction);
621 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
622
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800623 return serviceIntent;
624 }
625
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800626 private void renameGroup(Intent intent) {
627 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
628 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
629
630 if (groupId == -1) {
631 Log.e(TAG, "Invalid arguments for renameGroup request");
632 return;
633 }
634
635 ContentValues values = new ContentValues();
636 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700637 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
638 getContentResolver().update(groupUri, values, null, null);
639
640 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
641 callbackIntent.setData(groupUri);
642 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800643 }
644
645 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800646 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800647 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800648 public static Intent createGroupDeletionIntent(Context context, long groupId) {
649 Intent serviceIntent = new Intent(context, ContactSaveService.class);
650 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800651 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800652 return serviceIntent;
653 }
654
655 private void deleteGroup(Intent intent) {
656 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
657 if (groupId == -1) {
658 Log.e(TAG, "Invalid arguments for deleteGroup request");
659 return;
660 }
661
662 getContentResolver().delete(
663 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
664 }
665
666 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700667 * Creates an intent that can be sent to this service to rename a group as
668 * well as add and remove members from the group.
669 *
670 * @param context of the application
671 * @param groupId of the group that should be modified
672 * @param newLabel is the updated name of the group (can be null if the name
673 * should not be updated)
674 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
675 * should be added to the group
676 * @param rawContactsToRemove is an array of raw contact IDs for contacts
677 * that should be removed from the group
678 * @param callbackActivity is the activity to send the callback intent to
679 * @param callbackAction is the intent action for the callback intent
680 */
681 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
682 long[] rawContactsToAdd, long[] rawContactsToRemove,
683 Class<?> callbackActivity, String callbackAction) {
684 Intent serviceIntent = new Intent(context, ContactSaveService.class);
685 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
686 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
687 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
688 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
689 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
690 rawContactsToRemove);
691
692 // Callback intent will be invoked by the service once the group is updated
693 Intent callbackIntent = new Intent(context, callbackActivity);
694 callbackIntent.setAction(callbackAction);
695 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
696
697 return serviceIntent;
698 }
699
700 private void updateGroup(Intent intent) {
701 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
702 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
703 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
704 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
705
706 if (groupId == -1) {
707 Log.e(TAG, "Invalid arguments for updateGroup request");
708 return;
709 }
710
711 final ContentResolver resolver = getContentResolver();
712 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
713
714 // Update group name if necessary
715 if (label != null) {
716 ContentValues values = new ContentValues();
717 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700718 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700719 }
720
Katherine Kuan717e3432011-07-13 17:03:24 -0700721 // Add and remove members if necessary
722 addMembersToGroup(resolver, rawContactsToAdd, groupId);
723 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
724
725 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
726 callbackIntent.setData(groupUri);
727 deliverCallback(callbackIntent);
728 }
729
730 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
731 long groupId) {
732 if (rawContactsToAdd == null) {
733 return;
734 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700735 for (long rawContactId : rawContactsToAdd) {
736 try {
737 final ArrayList<ContentProviderOperation> rawContactOperations =
738 new ArrayList<ContentProviderOperation>();
739
740 // Build an assert operation to ensure the contact is not already in the group
741 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
742 .newAssertQuery(Data.CONTENT_URI);
743 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
744 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
745 new String[] { String.valueOf(rawContactId),
746 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
747 assertBuilder.withExpectedCount(0);
748 rawContactOperations.add(assertBuilder.build());
749
750 // Build an insert operation to add the contact to the group
751 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
752 .newInsert(Data.CONTENT_URI);
753 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
754 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
755 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
756 rawContactOperations.add(insertBuilder.build());
757
758 if (DEBUG) {
759 for (ContentProviderOperation operation : rawContactOperations) {
760 Log.v(TAG, operation.toString());
761 }
762 }
763
764 // Apply batch
765 ContentProviderResult[] results = null;
766 if (!rawContactOperations.isEmpty()) {
767 results = resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
768 }
769 } catch (RemoteException e) {
770 // Something went wrong, bail without success
771 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
772 String.valueOf(rawContactId), e);
773 } catch (OperationApplicationException e) {
774 // The assert could have failed because the contact is already in the group,
775 // just continue to the next contact
776 Log.w(TAG, "Assert failed in adding raw contact ID " +
777 String.valueOf(rawContactId) + ". Already exists in group " +
778 String.valueOf(groupId), e);
779 }
780 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700781 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700782
Katherine Kuan717e3432011-07-13 17:03:24 -0700783 private void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
784 long groupId) {
785 if (rawContactsToRemove == null) {
786 return;
787 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700788 for (long rawContactId : rawContactsToRemove) {
789 // Apply the delete operation on the data row for the given raw contact's
790 // membership in the given group. If no contact matches the provided selection, then
791 // nothing will be done. Just continue to the next contact.
792 getContentResolver().delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
793 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
794 new String[] { String.valueOf(rawContactId),
795 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
796 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700797 }
798
799 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800800 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800801 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800802 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
803 Intent serviceIntent = new Intent(context, ContactSaveService.class);
804 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
805 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
806 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
807
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800808 return serviceIntent;
809 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800810
811 private void setStarred(Intent intent) {
812 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
813 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
814 if (contactUri == null) {
815 Log.e(TAG, "Invalid arguments for setStarred request");
816 return;
817 }
818
819 final ContentValues values = new ContentValues(1);
820 values.put(Contacts.STARRED, value);
821 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800822 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800823
824 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700825 * Creates an intent that can be sent to this service to set the redirect to voicemail.
826 */
827 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
828 boolean value) {
829 Intent serviceIntent = new Intent(context, ContactSaveService.class);
830 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
831 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
832 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
833
834 return serviceIntent;
835 }
836
837 private void setSendToVoicemail(Intent intent) {
838 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
839 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
840 if (contactUri == null) {
841 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
842 return;
843 }
844
845 final ContentValues values = new ContentValues(1);
846 values.put(Contacts.SEND_TO_VOICEMAIL, value);
847 getContentResolver().update(contactUri, values, null, null);
848 }
849
850 /**
851 * Creates an intent that can be sent to this service to save the contact's ringtone.
852 */
853 public static Intent createSetRingtone(Context context, Uri contactUri,
854 String value) {
855 Intent serviceIntent = new Intent(context, ContactSaveService.class);
856 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
857 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
858 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
859
860 return serviceIntent;
861 }
862
863 private void setRingtone(Intent intent) {
864 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
865 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
866 if (contactUri == null) {
867 Log.e(TAG, "Invalid arguments for setRingtone");
868 return;
869 }
870 ContentValues values = new ContentValues(1);
871 values.put(Contacts.CUSTOM_RINGTONE, value);
872 getContentResolver().update(contactUri, values, null, null);
873 }
874
875 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800876 * Creates an intent that sets the selected data item as super primary (default)
877 */
878 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
879 Intent serviceIntent = new Intent(context, ContactSaveService.class);
880 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
881 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
882 return serviceIntent;
883 }
884
885 private void setSuperPrimary(Intent intent) {
886 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
887 if (dataId == -1) {
888 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
889 return;
890 }
891
892 // Update the primary values in the data record.
893 ContentValues values = new ContentValues(1);
894 values.put(Data.IS_SUPER_PRIMARY, 1);
895 values.put(Data.IS_PRIMARY, 1);
896
897 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
898 values, null, null);
899 }
900
901 /**
902 * Creates an intent that clears the primary flag of all data items that belong to the same
903 * raw_contact as the given data item. Will only clear, if the data item was primary before
904 * this call
905 */
906 public static Intent createClearPrimaryIntent(Context context, long dataId) {
907 Intent serviceIntent = new Intent(context, ContactSaveService.class);
908 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
909 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
910 return serviceIntent;
911 }
912
913 private void clearPrimary(Intent intent) {
914 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
915 if (dataId == -1) {
916 Log.e(TAG, "Invalid arguments for clearPrimary request");
917 return;
918 }
919
920 // Update the primary values in the data record.
921 ContentValues values = new ContentValues(1);
922 values.put(Data.IS_SUPER_PRIMARY, 0);
923 values.put(Data.IS_PRIMARY, 0);
924
925 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
926 values, null, null);
927 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800928
929 /**
930 * Creates an intent that can be sent to this service to delete a contact.
931 */
932 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
933 Intent serviceIntent = new Intent(context, ContactSaveService.class);
934 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
935 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
936 return serviceIntent;
937 }
938
939 private void deleteContact(Intent intent) {
940 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
941 if (contactUri == null) {
942 Log.e(TAG, "Invalid arguments for deleteContact request");
943 return;
944 }
945
946 getContentResolver().delete(contactUri, null, null);
947 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800948
949 /**
950 * Creates an intent that can be sent to this service to join two contacts.
951 */
952 public static Intent createJoinContactsIntent(Context context, long contactId1,
953 long contactId2, boolean contactWritable,
954 Class<?> callbackActivity, String callbackAction) {
955 Intent serviceIntent = new Intent(context, ContactSaveService.class);
956 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
957 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
958 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
959 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
960
961 // Callback intent will be invoked by the service once the contacts are joined.
962 Intent callbackIntent = new Intent(context, callbackActivity);
963 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800964 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
965
966 return serviceIntent;
967 }
968
969
970 private interface JoinContactQuery {
971 String[] PROJECTION = {
972 RawContacts._ID,
973 RawContacts.CONTACT_ID,
974 RawContacts.NAME_VERIFIED,
975 RawContacts.DISPLAY_NAME_SOURCE,
976 };
977
978 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
979
980 int _ID = 0;
981 int CONTACT_ID = 1;
982 int NAME_VERIFIED = 2;
983 int DISPLAY_NAME_SOURCE = 3;
984 }
985
986 private void joinContacts(Intent intent) {
987 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
988 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
989 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
990 if (contactId1 == -1 || contactId2 == -1) {
991 Log.e(TAG, "Invalid arguments for joinContacts request");
992 return;
993 }
994
995 final ContentResolver resolver = getContentResolver();
996
997 // Load raw contact IDs for all raw contacts involved - currently edited and selected
998 // in the join UIs
999 Cursor c = resolver.query(RawContacts.CONTENT_URI,
1000 JoinContactQuery.PROJECTION,
1001 JoinContactQuery.SELECTION,
1002 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1003
1004 long rawContactIds[];
1005 long verifiedNameRawContactId = -1;
1006 try {
1007 int maxDisplayNameSource = -1;
1008 rawContactIds = new long[c.getCount()];
1009 for (int i = 0; i < rawContactIds.length; i++) {
1010 c.moveToPosition(i);
1011 long rawContactId = c.getLong(JoinContactQuery._ID);
1012 rawContactIds[i] = rawContactId;
1013 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1014 if (nameSource > maxDisplayNameSource) {
1015 maxDisplayNameSource = nameSource;
1016 }
1017 }
1018
1019 // Find an appropriate display name for the joined contact:
1020 // if should have a higher DisplayNameSource or be the name
1021 // of the original contact that we are joining with another.
1022 if (writable) {
1023 for (int i = 0; i < rawContactIds.length; i++) {
1024 c.moveToPosition(i);
1025 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1026 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1027 if (nameSource == maxDisplayNameSource
1028 && (verifiedNameRawContactId == -1
1029 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1030 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1031 }
1032 }
1033 }
1034 }
1035 } finally {
1036 c.close();
1037 }
1038
1039 // For each pair of raw contacts, insert an aggregation exception
1040 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1041 for (int i = 0; i < rawContactIds.length; i++) {
1042 for (int j = 0; j < rawContactIds.length; j++) {
1043 if (i != j) {
1044 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1045 }
1046 }
1047 }
1048
1049 // Mark the original contact as "name verified" to make sure that the contact
1050 // display name does not change as a result of the join
1051 if (verifiedNameRawContactId != -1) {
1052 Builder builder = ContentProviderOperation.newUpdate(
1053 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1054 builder.withValue(RawContacts.NAME_VERIFIED, 1);
1055 operations.add(builder.build());
1056 }
1057
1058 boolean success = false;
1059 // Apply all aggregation exceptions as one batch
1060 try {
1061 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001062 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001063 success = true;
1064 } catch (RemoteException e) {
1065 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001066 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001067 } catch (OperationApplicationException e) {
1068 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001069 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001070 }
1071
1072 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1073 if (success) {
1074 Uri uri = RawContacts.getContactLookupUri(resolver,
1075 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1076 callbackIntent.setData(uri);
1077 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001078 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001079 }
1080
1081 /**
1082 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1083 */
1084 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1085 long rawContactId1, long rawContactId2) {
1086 Builder builder =
1087 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1088 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1089 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1090 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1091 operations.add(builder.build());
1092 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001093
1094 /**
1095 * Shows a toast on the UI thread.
1096 */
1097 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001098 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001099
1100 @Override
1101 public void run() {
1102 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1103 }
1104 });
1105 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001106
1107 private void deliverCallback(final Intent callbackIntent) {
1108 mMainHandler.post(new Runnable() {
1109
1110 @Override
1111 public void run() {
1112 deliverCallbackOnUiThread(callbackIntent);
1113 }
1114 });
1115 }
1116
1117 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1118 // TODO: this assumes that if there are multiple instances of the same
1119 // activity registered, the last one registered is the one waiting for
1120 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001121 for (Listener listener : sListeners) {
1122 if (callbackIntent.getComponent().equals(
1123 ((Activity) listener).getIntent().getComponent())) {
1124 listener.onServiceCompleted(callbackIntent);
1125 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001126 }
1127 }
1128 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001129}