blob: 6de361d77e0610a48d5236808fa0f30668379112 [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
Daniel Lehmann18958a22012-02-28 17:45:25 -0800730 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700731 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
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700765 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800766 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700767 }
768 } catch (RemoteException e) {
769 // Something went wrong, bail without success
770 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
771 String.valueOf(rawContactId), e);
772 } catch (OperationApplicationException e) {
773 // The assert could have failed because the contact is already in the group,
774 // just continue to the next contact
775 Log.w(TAG, "Assert failed in adding raw contact ID " +
776 String.valueOf(rawContactId) + ". Already exists in group " +
777 String.valueOf(groupId), e);
778 }
779 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700780 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700781
Daniel Lehmann18958a22012-02-28 17:45:25 -0800782 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700783 long groupId) {
784 if (rawContactsToRemove == null) {
785 return;
786 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700787 for (long rawContactId : rawContactsToRemove) {
788 // Apply the delete operation on the data row for the given raw contact's
789 // membership in the given group. If no contact matches the provided selection, then
790 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800791 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700792 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
793 new String[] { String.valueOf(rawContactId),
794 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
795 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700796 }
797
798 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800799 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800800 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800801 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
802 Intent serviceIntent = new Intent(context, ContactSaveService.class);
803 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
804 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
805 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
806
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800807 return serviceIntent;
808 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800809
810 private void setStarred(Intent intent) {
811 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
812 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
813 if (contactUri == null) {
814 Log.e(TAG, "Invalid arguments for setStarred request");
815 return;
816 }
817
818 final ContentValues values = new ContentValues(1);
819 values.put(Contacts.STARRED, value);
820 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800821 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800822
823 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700824 * Creates an intent that can be sent to this service to set the redirect to voicemail.
825 */
826 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
827 boolean value) {
828 Intent serviceIntent = new Intent(context, ContactSaveService.class);
829 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
830 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
831 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
832
833 return serviceIntent;
834 }
835
836 private void setSendToVoicemail(Intent intent) {
837 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
838 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
839 if (contactUri == null) {
840 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
841 return;
842 }
843
844 final ContentValues values = new ContentValues(1);
845 values.put(Contacts.SEND_TO_VOICEMAIL, value);
846 getContentResolver().update(contactUri, values, null, null);
847 }
848
849 /**
850 * Creates an intent that can be sent to this service to save the contact's ringtone.
851 */
852 public static Intent createSetRingtone(Context context, Uri contactUri,
853 String value) {
854 Intent serviceIntent = new Intent(context, ContactSaveService.class);
855 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
856 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
857 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
858
859 return serviceIntent;
860 }
861
862 private void setRingtone(Intent intent) {
863 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
864 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
865 if (contactUri == null) {
866 Log.e(TAG, "Invalid arguments for setRingtone");
867 return;
868 }
869 ContentValues values = new ContentValues(1);
870 values.put(Contacts.CUSTOM_RINGTONE, value);
871 getContentResolver().update(contactUri, values, null, null);
872 }
873
874 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800875 * Creates an intent that sets the selected data item as super primary (default)
876 */
877 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
878 Intent serviceIntent = new Intent(context, ContactSaveService.class);
879 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
880 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
881 return serviceIntent;
882 }
883
884 private void setSuperPrimary(Intent intent) {
885 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
886 if (dataId == -1) {
887 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
888 return;
889 }
890
891 // Update the primary values in the data record.
892 ContentValues values = new ContentValues(1);
893 values.put(Data.IS_SUPER_PRIMARY, 1);
894 values.put(Data.IS_PRIMARY, 1);
895
896 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
897 values, null, null);
898 }
899
900 /**
901 * Creates an intent that clears the primary flag of all data items that belong to the same
902 * raw_contact as the given data item. Will only clear, if the data item was primary before
903 * this call
904 */
905 public static Intent createClearPrimaryIntent(Context context, long dataId) {
906 Intent serviceIntent = new Intent(context, ContactSaveService.class);
907 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
908 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
909 return serviceIntent;
910 }
911
912 private void clearPrimary(Intent intent) {
913 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
914 if (dataId == -1) {
915 Log.e(TAG, "Invalid arguments for clearPrimary request");
916 return;
917 }
918
919 // Update the primary values in the data record.
920 ContentValues values = new ContentValues(1);
921 values.put(Data.IS_SUPER_PRIMARY, 0);
922 values.put(Data.IS_PRIMARY, 0);
923
924 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
925 values, null, null);
926 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800927
928 /**
929 * Creates an intent that can be sent to this service to delete a contact.
930 */
931 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
932 Intent serviceIntent = new Intent(context, ContactSaveService.class);
933 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
934 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
935 return serviceIntent;
936 }
937
938 private void deleteContact(Intent intent) {
939 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
940 if (contactUri == null) {
941 Log.e(TAG, "Invalid arguments for deleteContact request");
942 return;
943 }
944
945 getContentResolver().delete(contactUri, null, null);
946 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800947
948 /**
949 * Creates an intent that can be sent to this service to join two contacts.
950 */
951 public static Intent createJoinContactsIntent(Context context, long contactId1,
952 long contactId2, boolean contactWritable,
953 Class<?> callbackActivity, String callbackAction) {
954 Intent serviceIntent = new Intent(context, ContactSaveService.class);
955 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
956 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
957 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
958 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
959
960 // Callback intent will be invoked by the service once the contacts are joined.
961 Intent callbackIntent = new Intent(context, callbackActivity);
962 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800963 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
964
965 return serviceIntent;
966 }
967
968
969 private interface JoinContactQuery {
970 String[] PROJECTION = {
971 RawContacts._ID,
972 RawContacts.CONTACT_ID,
973 RawContacts.NAME_VERIFIED,
974 RawContacts.DISPLAY_NAME_SOURCE,
975 };
976
977 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
978
979 int _ID = 0;
980 int CONTACT_ID = 1;
981 int NAME_VERIFIED = 2;
982 int DISPLAY_NAME_SOURCE = 3;
983 }
984
985 private void joinContacts(Intent intent) {
986 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
987 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
988 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
989 if (contactId1 == -1 || contactId2 == -1) {
990 Log.e(TAG, "Invalid arguments for joinContacts request");
991 return;
992 }
993
994 final ContentResolver resolver = getContentResolver();
995
996 // Load raw contact IDs for all raw contacts involved - currently edited and selected
997 // in the join UIs
998 Cursor c = resolver.query(RawContacts.CONTENT_URI,
999 JoinContactQuery.PROJECTION,
1000 JoinContactQuery.SELECTION,
1001 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1002
1003 long rawContactIds[];
1004 long verifiedNameRawContactId = -1;
1005 try {
1006 int maxDisplayNameSource = -1;
1007 rawContactIds = new long[c.getCount()];
1008 for (int i = 0; i < rawContactIds.length; i++) {
1009 c.moveToPosition(i);
1010 long rawContactId = c.getLong(JoinContactQuery._ID);
1011 rawContactIds[i] = rawContactId;
1012 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1013 if (nameSource > maxDisplayNameSource) {
1014 maxDisplayNameSource = nameSource;
1015 }
1016 }
1017
1018 // Find an appropriate display name for the joined contact:
1019 // if should have a higher DisplayNameSource or be the name
1020 // of the original contact that we are joining with another.
1021 if (writable) {
1022 for (int i = 0; i < rawContactIds.length; i++) {
1023 c.moveToPosition(i);
1024 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1025 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1026 if (nameSource == maxDisplayNameSource
1027 && (verifiedNameRawContactId == -1
1028 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1029 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1030 }
1031 }
1032 }
1033 }
1034 } finally {
1035 c.close();
1036 }
1037
1038 // For each pair of raw contacts, insert an aggregation exception
1039 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1040 for (int i = 0; i < rawContactIds.length; i++) {
1041 for (int j = 0; j < rawContactIds.length; j++) {
1042 if (i != j) {
1043 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1044 }
1045 }
1046 }
1047
1048 // Mark the original contact as "name verified" to make sure that the contact
1049 // display name does not change as a result of the join
1050 if (verifiedNameRawContactId != -1) {
1051 Builder builder = ContentProviderOperation.newUpdate(
1052 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1053 builder.withValue(RawContacts.NAME_VERIFIED, 1);
1054 operations.add(builder.build());
1055 }
1056
1057 boolean success = false;
1058 // Apply all aggregation exceptions as one batch
1059 try {
1060 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001061 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001062 success = true;
1063 } catch (RemoteException e) {
1064 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001065 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001066 } catch (OperationApplicationException e) {
1067 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001068 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001069 }
1070
1071 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1072 if (success) {
1073 Uri uri = RawContacts.getContactLookupUri(resolver,
1074 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1075 callbackIntent.setData(uri);
1076 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001077 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001078 }
1079
1080 /**
1081 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1082 */
1083 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1084 long rawContactId1, long rawContactId2) {
1085 Builder builder =
1086 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1087 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1088 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1089 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1090 operations.add(builder.build());
1091 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001092
1093 /**
1094 * Shows a toast on the UI thread.
1095 */
1096 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001097 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001098
1099 @Override
1100 public void run() {
1101 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1102 }
1103 });
1104 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001105
1106 private void deliverCallback(final Intent callbackIntent) {
1107 mMainHandler.post(new Runnable() {
1108
1109 @Override
1110 public void run() {
1111 deliverCallbackOnUiThread(callbackIntent);
1112 }
1113 });
1114 }
1115
1116 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1117 // TODO: this assumes that if there are multiple instances of the same
1118 // activity registered, the last one registered is the one waiting for
1119 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001120 for (Listener listener : sListeners) {
1121 if (callbackIntent.getComponent().equals(
1122 ((Activity) listener).getIntent().getComponent())) {
1123 listener.onServiceCompleted(callbackIntent);
1124 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001125 }
1126 }
1127 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001128}