blob: 2697589c913cf230f6bda789a55090866181b402 [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;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080040import android.os.Handler;
41import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080042import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080043import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070044import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080045import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080046import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080047import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070048import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080049import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070050import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070051import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070052import android.provider.ContactsContract.RawContactsEntity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070053import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080054import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070055
56import java.util.ArrayList;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070057import java.util.HashSet;
58import java.util.List;
Hugo Hudsona831c0b2011-08-13 11:50:15 +010059import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070060
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080061/**
62 * A service responsible for saving changes to the content provider.
63 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070064public class ContactSaveService extends IntentService {
65 private static final String TAG = "ContactSaveService";
66
Katherine Kuana007e442011-07-07 09:25:34 -070067 /** Set to true in order to view logs on content provider operations */
68 private static final boolean DEBUG = false;
69
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070070 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
71
72 public static final String EXTRA_ACCOUNT_NAME = "accountName";
73 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070074 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070075 public static final String EXTRA_CONTENT_VALUES = "contentValues";
76 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
77
Dmitri Plotnikova0114142011-02-15 13:53:21 -080078 public static final String ACTION_SAVE_CONTACT = "saveContact";
79 public static final String EXTRA_CONTACT_STATE = "state";
80 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070081 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070082
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080083 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080084 public static final String ACTION_RENAME_GROUP = "renameGroup";
85 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070086 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080087 public static final String EXTRA_GROUP_ID = "groupId";
88 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070089 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
90 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080091
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080092 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -080093 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080094 public static final String EXTRA_CONTACT_URI = "contactUri";
95 public static final String EXTRA_STARRED_FLAG = "starred";
96
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -080097 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
98 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
99 public static final String EXTRA_DATA_ID = "dataId";
100
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800101 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
102 public static final String EXTRA_CONTACT_ID1 = "contactId1";
103 public static final String EXTRA_CONTACT_ID2 = "contactId2";
104 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
105
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700106 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
107 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
108
109 public static final String ACTION_SET_RINGTONE = "setRingtone";
110 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
111
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700112 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
113 Data.MIMETYPE,
114 Data.IS_PRIMARY,
115 Data.DATA1,
116 Data.DATA2,
117 Data.DATA3,
118 Data.DATA4,
119 Data.DATA5,
120 Data.DATA6,
121 Data.DATA7,
122 Data.DATA8,
123 Data.DATA9,
124 Data.DATA10,
125 Data.DATA11,
126 Data.DATA12,
127 Data.DATA13,
128 Data.DATA14,
129 Data.DATA15
130 );
131
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800132 private static final int PERSIST_TRIES = 3;
133
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800134 public interface Listener {
135 public void onServiceCompleted(Intent callbackIntent);
136 }
137
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100138 private static final CopyOnWriteArrayList<Listener> sListeners =
139 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800140
141 private Handler mMainHandler;
142
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700143 public ContactSaveService() {
144 super(TAG);
145 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800146 mMainHandler = new Handler(Looper.getMainLooper());
147 }
148
149 public static void registerListener(Listener listener) {
150 if (!(listener instanceof Activity)) {
151 throw new ClassCastException("Only activities can be registered to"
152 + " receive callback from " + ContactSaveService.class.getName());
153 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100154 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800155 }
156
157 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100158 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700159 }
160
161 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800162 public Object getSystemService(String name) {
163 Object service = super.getSystemService(name);
164 if (service != null) {
165 return service;
166 }
167
168 return getApplicationContext().getSystemService(name);
169 }
170
171 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700172 protected void onHandleIntent(Intent intent) {
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700173 String action = intent.getAction();
174 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
175 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800176 } else if (ACTION_SAVE_CONTACT.equals(action)) {
177 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800178 } else if (ACTION_CREATE_GROUP.equals(action)) {
179 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800180 } else if (ACTION_RENAME_GROUP.equals(action)) {
181 renameGroup(intent);
182 } else if (ACTION_DELETE_GROUP.equals(action)) {
183 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700184 } else if (ACTION_UPDATE_GROUP.equals(action)) {
185 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800186 } else if (ACTION_SET_STARRED.equals(action)) {
187 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800188 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
189 setSuperPrimary(intent);
190 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
191 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800192 } else if (ACTION_DELETE_CONTACT.equals(action)) {
193 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800194 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
195 joinContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700196 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
197 setSendToVoicemail(intent);
198 } else if (ACTION_SET_RINGTONE.equals(action)) {
199 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700200 }
201 }
202
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800203 /**
204 * Creates an intent that can be sent to this service to create a new raw contact
205 * using data presented as a set of ContentValues.
206 */
207 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700208 ArrayList<ContentValues> values, AccountWithDataSet account,
209 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800210 Intent serviceIntent = new Intent(
211 context, ContactSaveService.class);
212 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
213 if (account != null) {
214 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
215 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700216 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800217 }
218 serviceIntent.putParcelableArrayListExtra(
219 ContactSaveService.EXTRA_CONTENT_VALUES, values);
220
221 // Callback intent will be invoked by the service once the new contact is
222 // created. The service will put the URI of the new contact as "data" on
223 // the callback intent.
224 Intent callbackIntent = new Intent(context, callbackActivity);
225 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800226 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
227 return serviceIntent;
228 }
229
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700230 private void createRawContact(Intent intent) {
231 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
232 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700233 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700234 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
235 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
236
237 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
238 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
239 .withValue(RawContacts.ACCOUNT_NAME, accountName)
240 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700241 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700242 .build());
243
244 int size = valueList.size();
245 for (int i = 0; i < size; i++) {
246 ContentValues values = valueList.get(i);
247 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
248 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
249 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
250 .withValues(values)
251 .build());
252 }
253
254 ContentResolver resolver = getContentResolver();
255 ContentProviderResult[] results;
256 try {
257 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
258 } catch (Exception e) {
259 throw new RuntimeException("Failed to store new contact", e);
260 }
261
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700262 Uri rawContactUri = results[0].uri;
263 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
264
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800265 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700266 }
267
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700268 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800269 * Creates an intent that can be sent to this service to create a new raw contact
270 * using data presented as a set of ContentValues.
271 */
272 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700273 String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800274 String callbackAction) {
275 Intent serviceIntent = new Intent(
276 context, ContactSaveService.class);
277 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
278 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700279 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800280
281 // Callback intent will be invoked by the service once the contact is
282 // saved. The service will put the URI of the new contact as "data" on
283 // the callback intent.
284 Intent callbackIntent = new Intent(context, callbackActivity);
285 callbackIntent.putExtra(saveModeExtraKey, saveMode);
286 callbackIntent.setAction(callbackAction);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800287 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
288 return serviceIntent;
289 }
290
291 private void saveContact(Intent intent) {
292 EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
293 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700294 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800295
296 // Trim any empty fields, and RawContacts, before persisting
297 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
298 EntityModifier.trimEmpty(state, accountTypes);
299
300 Uri lookupUri = null;
301
302 final ContentResolver resolver = getContentResolver();
303
304 // Attempt to persist changes
305 int tries = 0;
306 while (tries++ < PERSIST_TRIES) {
307 try {
308 // Build operations and try applying
309 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700310 if (DEBUG) {
311 Log.v(TAG, "Content Provider Operations:");
312 for (ContentProviderOperation operation : diff) {
313 Log.v(TAG, operation.toString());
314 }
315 }
316
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800317 ContentProviderResult[] results = null;
318 if (!diff.isEmpty()) {
319 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
320 }
321
322 final long rawContactId = getRawContactId(state, diff, results);
323 if (rawContactId == -1) {
324 throw new IllegalStateException("Could not determine RawContact ID after save");
325 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700326 if (isProfile) {
327 // Since the profile supports local raw contacts, which may have been completely
328 // removed if all information was removed, we need to do a special query to
329 // get the lookup URI for the profile contact (if it still exists).
330 Cursor c = resolver.query(Profile.CONTENT_URI,
331 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
332 null, null, null);
333 try {
Erik162b7e32011-09-20 15:23:55 -0700334 if (c.moveToFirst()) {
335 final long contactId = c.getLong(0);
336 final String lookupKey = c.getString(1);
337 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
338 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700339 } finally {
340 c.close();
341 }
342 } else {
343 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
344 rawContactId);
345 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
346 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800347 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
348 break;
349
350 } catch (RemoteException e) {
351 // Something went wrong, bail without success
352 Log.e(TAG, "Problem persisting user edits", e);
353 break;
354
355 } catch (OperationApplicationException e) {
356 // Version consistency failed, re-parent change and try again
357 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
358 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
359 boolean first = true;
360 final int count = state.size();
361 for (int i = 0; i < count; i++) {
362 Long rawContactId = state.getRawContactId(i);
363 if (rawContactId != null && rawContactId != -1) {
364 if (!first) {
365 sb.append(',');
366 }
367 sb.append(rawContactId);
368 first = false;
369 }
370 }
371 sb.append(")");
372
373 if (first) {
374 throw new IllegalStateException("Version consistency failed for a new contact");
375 }
376
Dave Santoroc90f95e2011-09-07 17:47:15 -0700377 final EntityDeltaList newState = EntityDeltaList.fromQuery(
378 isProfile
379 ? RawContactsEntity.PROFILE_CONTENT_URI
380 : RawContactsEntity.CONTENT_URI,
381 resolver, sb.toString(), null, null);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800382 state = EntityDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700383
384 // Update the new state to use profile URIs if appropriate.
385 if (isProfile) {
386 for (EntityDelta delta : state) {
387 delta.setProfileQueryUri();
388 }
389 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800390 }
391 }
392
393 callbackIntent.setData(lookupUri);
394
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800395 deliverCallback(callbackIntent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800396 }
397
398 private long getRawContactId(EntityDeltaList state,
399 final ArrayList<ContentProviderOperation> diff,
400 final ContentProviderResult[] results) {
401 long rawContactId = state.findRawContactId();
402 if (rawContactId != -1) {
403 return rawContactId;
404 }
405
406 final int diffSize = diff.size();
407 for (int i = 0; i < diffSize; i++) {
408 ContentProviderOperation operation = diff.get(i);
409 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
410 && operation.getUri().getEncodedPath().contains(
411 RawContacts.CONTENT_URI.getEncodedPath())) {
412 return ContentUris.parseId(results[i].uri);
413 }
414 }
415 return -1;
416 }
417
418 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700419 * Creates an intent that can be sent to this service to create a new group as
420 * well as add new members at the same time.
421 *
422 * @param context of the application
423 * @param account in which the group should be created
424 * @param label is the name of the group (cannot be null)
425 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
426 * should be added to the group
427 * @param callbackActivity is the activity to send the callback intent to
428 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700429 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700430 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Katherine Kuan717e3432011-07-13 17:03:24 -0700431 String label, long[] rawContactsToAdd, Class<?> callbackActivity,
432 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800433 Intent serviceIntent = new Intent(context, ContactSaveService.class);
434 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
435 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
436 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700437 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800438 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700439 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700440
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800441 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700442 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800443 Intent callbackIntent = new Intent(context, callbackActivity);
444 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700445 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800446
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700447 return serviceIntent;
448 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800449
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800450 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700451 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
452 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
453 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
454 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700455 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800456
457 ContentValues values = new ContentValues();
458 values.put(Groups.ACCOUNT_TYPE, accountType);
459 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700460 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800461 values.put(Groups.TITLE, label);
462
Katherine Kuan717e3432011-07-13 17:03:24 -0700463 final ContentResolver resolver = getContentResolver();
464
465 // Create the new group
466 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
467
468 // If there's no URI, then the insertion failed. Abort early because group members can't be
469 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800470 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700471 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800472 return;
473 }
474
Katherine Kuan717e3432011-07-13 17:03:24 -0700475 // Add new group members
476 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
477
478 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
479 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800480 values.clear();
481 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
482 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
483
484 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700485 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700486 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800487 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800488 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800489 }
490
491 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800492 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800493 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700494 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
495 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800496 Intent serviceIntent = new Intent(context, ContactSaveService.class);
497 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
498 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
499 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700500
501 // Callback intent will be invoked by the service once the group is renamed.
502 Intent callbackIntent = new Intent(context, callbackActivity);
503 callbackIntent.setAction(callbackAction);
504 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
505
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800506 return serviceIntent;
507 }
508
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800509 private void renameGroup(Intent intent) {
510 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
511 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
512
513 if (groupId == -1) {
514 Log.e(TAG, "Invalid arguments for renameGroup request");
515 return;
516 }
517
518 ContentValues values = new ContentValues();
519 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700520 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
521 getContentResolver().update(groupUri, values, null, null);
522
523 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
524 callbackIntent.setData(groupUri);
525 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800526 }
527
528 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800529 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800530 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800531 public static Intent createGroupDeletionIntent(Context context, long groupId) {
532 Intent serviceIntent = new Intent(context, ContactSaveService.class);
533 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800534 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800535 return serviceIntent;
536 }
537
538 private void deleteGroup(Intent intent) {
539 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
540 if (groupId == -1) {
541 Log.e(TAG, "Invalid arguments for deleteGroup request");
542 return;
543 }
544
545 getContentResolver().delete(
546 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
547 }
548
549 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700550 * Creates an intent that can be sent to this service to rename a group as
551 * well as add and remove members from the group.
552 *
553 * @param context of the application
554 * @param groupId of the group that should be modified
555 * @param newLabel is the updated name of the group (can be null if the name
556 * should not be updated)
557 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
558 * should be added to the group
559 * @param rawContactsToRemove is an array of raw contact IDs for contacts
560 * that should be removed from the group
561 * @param callbackActivity is the activity to send the callback intent to
562 * @param callbackAction is the intent action for the callback intent
563 */
564 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
565 long[] rawContactsToAdd, long[] rawContactsToRemove,
566 Class<?> callbackActivity, String callbackAction) {
567 Intent serviceIntent = new Intent(context, ContactSaveService.class);
568 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
569 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
570 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
571 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
572 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
573 rawContactsToRemove);
574
575 // Callback intent will be invoked by the service once the group is updated
576 Intent callbackIntent = new Intent(context, callbackActivity);
577 callbackIntent.setAction(callbackAction);
578 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
579
580 return serviceIntent;
581 }
582
583 private void updateGroup(Intent intent) {
584 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
585 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
586 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
587 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
588
589 if (groupId == -1) {
590 Log.e(TAG, "Invalid arguments for updateGroup request");
591 return;
592 }
593
594 final ContentResolver resolver = getContentResolver();
595 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
596
597 // Update group name if necessary
598 if (label != null) {
599 ContentValues values = new ContentValues();
600 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700601 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700602 }
603
Katherine Kuan717e3432011-07-13 17:03:24 -0700604 // Add and remove members if necessary
605 addMembersToGroup(resolver, rawContactsToAdd, groupId);
606 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
607
608 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
609 callbackIntent.setData(groupUri);
610 deliverCallback(callbackIntent);
611 }
612
613 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
614 long groupId) {
615 if (rawContactsToAdd == null) {
616 return;
617 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700618 for (long rawContactId : rawContactsToAdd) {
619 try {
620 final ArrayList<ContentProviderOperation> rawContactOperations =
621 new ArrayList<ContentProviderOperation>();
622
623 // Build an assert operation to ensure the contact is not already in the group
624 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
625 .newAssertQuery(Data.CONTENT_URI);
626 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
627 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
628 new String[] { String.valueOf(rawContactId),
629 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
630 assertBuilder.withExpectedCount(0);
631 rawContactOperations.add(assertBuilder.build());
632
633 // Build an insert operation to add the contact to the group
634 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
635 .newInsert(Data.CONTENT_URI);
636 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
637 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
638 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
639 rawContactOperations.add(insertBuilder.build());
640
641 if (DEBUG) {
642 for (ContentProviderOperation operation : rawContactOperations) {
643 Log.v(TAG, operation.toString());
644 }
645 }
646
647 // Apply batch
648 ContentProviderResult[] results = null;
649 if (!rawContactOperations.isEmpty()) {
650 results = resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
651 }
652 } catch (RemoteException e) {
653 // Something went wrong, bail without success
654 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
655 String.valueOf(rawContactId), e);
656 } catch (OperationApplicationException e) {
657 // The assert could have failed because the contact is already in the group,
658 // just continue to the next contact
659 Log.w(TAG, "Assert failed in adding raw contact ID " +
660 String.valueOf(rawContactId) + ". Already exists in group " +
661 String.valueOf(groupId), e);
662 }
663 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700664 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700665
Katherine Kuan717e3432011-07-13 17:03:24 -0700666 private void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
667 long groupId) {
668 if (rawContactsToRemove == null) {
669 return;
670 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700671 for (long rawContactId : rawContactsToRemove) {
672 // Apply the delete operation on the data row for the given raw contact's
673 // membership in the given group. If no contact matches the provided selection, then
674 // nothing will be done. Just continue to the next contact.
675 getContentResolver().delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
676 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
677 new String[] { String.valueOf(rawContactId),
678 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
679 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700680 }
681
682 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800683 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800684 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800685 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
686 Intent serviceIntent = new Intent(context, ContactSaveService.class);
687 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
688 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
689 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
690
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800691 return serviceIntent;
692 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800693
694 private void setStarred(Intent intent) {
695 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
696 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
697 if (contactUri == null) {
698 Log.e(TAG, "Invalid arguments for setStarred request");
699 return;
700 }
701
702 final ContentValues values = new ContentValues(1);
703 values.put(Contacts.STARRED, value);
704 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800705 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800706
707 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700708 * Creates an intent that can be sent to this service to set the redirect to voicemail.
709 */
710 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
711 boolean value) {
712 Intent serviceIntent = new Intent(context, ContactSaveService.class);
713 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
714 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
715 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
716
717 return serviceIntent;
718 }
719
720 private void setSendToVoicemail(Intent intent) {
721 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
722 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
723 if (contactUri == null) {
724 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
725 return;
726 }
727
728 final ContentValues values = new ContentValues(1);
729 values.put(Contacts.SEND_TO_VOICEMAIL, value);
730 getContentResolver().update(contactUri, values, null, null);
731 }
732
733 /**
734 * Creates an intent that can be sent to this service to save the contact's ringtone.
735 */
736 public static Intent createSetRingtone(Context context, Uri contactUri,
737 String value) {
738 Intent serviceIntent = new Intent(context, ContactSaveService.class);
739 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
740 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
741 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
742
743 return serviceIntent;
744 }
745
746 private void setRingtone(Intent intent) {
747 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
748 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
749 if (contactUri == null) {
750 Log.e(TAG, "Invalid arguments for setRingtone");
751 return;
752 }
753 ContentValues values = new ContentValues(1);
754 values.put(Contacts.CUSTOM_RINGTONE, value);
755 getContentResolver().update(contactUri, values, null, null);
756 }
757
758 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800759 * Creates an intent that sets the selected data item as super primary (default)
760 */
761 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
762 Intent serviceIntent = new Intent(context, ContactSaveService.class);
763 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
764 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
765 return serviceIntent;
766 }
767
768 private void setSuperPrimary(Intent intent) {
769 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
770 if (dataId == -1) {
771 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
772 return;
773 }
774
775 // Update the primary values in the data record.
776 ContentValues values = new ContentValues(1);
777 values.put(Data.IS_SUPER_PRIMARY, 1);
778 values.put(Data.IS_PRIMARY, 1);
779
780 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
781 values, null, null);
782 }
783
784 /**
785 * Creates an intent that clears the primary flag of all data items that belong to the same
786 * raw_contact as the given data item. Will only clear, if the data item was primary before
787 * this call
788 */
789 public static Intent createClearPrimaryIntent(Context context, long dataId) {
790 Intent serviceIntent = new Intent(context, ContactSaveService.class);
791 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
792 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
793 return serviceIntent;
794 }
795
796 private void clearPrimary(Intent intent) {
797 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
798 if (dataId == -1) {
799 Log.e(TAG, "Invalid arguments for clearPrimary request");
800 return;
801 }
802
803 // Update the primary values in the data record.
804 ContentValues values = new ContentValues(1);
805 values.put(Data.IS_SUPER_PRIMARY, 0);
806 values.put(Data.IS_PRIMARY, 0);
807
808 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
809 values, null, null);
810 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800811
812 /**
813 * Creates an intent that can be sent to this service to delete a contact.
814 */
815 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
816 Intent serviceIntent = new Intent(context, ContactSaveService.class);
817 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
818 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
819 return serviceIntent;
820 }
821
822 private void deleteContact(Intent intent) {
823 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
824 if (contactUri == null) {
825 Log.e(TAG, "Invalid arguments for deleteContact request");
826 return;
827 }
828
829 getContentResolver().delete(contactUri, null, null);
830 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800831
832 /**
833 * Creates an intent that can be sent to this service to join two contacts.
834 */
835 public static Intent createJoinContactsIntent(Context context, long contactId1,
836 long contactId2, boolean contactWritable,
837 Class<?> callbackActivity, String callbackAction) {
838 Intent serviceIntent = new Intent(context, ContactSaveService.class);
839 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
840 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
841 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
842 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
843
844 // Callback intent will be invoked by the service once the contacts are joined.
845 Intent callbackIntent = new Intent(context, callbackActivity);
846 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800847 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
848
849 return serviceIntent;
850 }
851
852
853 private interface JoinContactQuery {
854 String[] PROJECTION = {
855 RawContacts._ID,
856 RawContacts.CONTACT_ID,
857 RawContacts.NAME_VERIFIED,
858 RawContacts.DISPLAY_NAME_SOURCE,
859 };
860
861 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
862
863 int _ID = 0;
864 int CONTACT_ID = 1;
865 int NAME_VERIFIED = 2;
866 int DISPLAY_NAME_SOURCE = 3;
867 }
868
869 private void joinContacts(Intent intent) {
870 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
871 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
872 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
873 if (contactId1 == -1 || contactId2 == -1) {
874 Log.e(TAG, "Invalid arguments for joinContacts request");
875 return;
876 }
877
878 final ContentResolver resolver = getContentResolver();
879
880 // Load raw contact IDs for all raw contacts involved - currently edited and selected
881 // in the join UIs
882 Cursor c = resolver.query(RawContacts.CONTENT_URI,
883 JoinContactQuery.PROJECTION,
884 JoinContactQuery.SELECTION,
885 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
886
887 long rawContactIds[];
888 long verifiedNameRawContactId = -1;
889 try {
890 int maxDisplayNameSource = -1;
891 rawContactIds = new long[c.getCount()];
892 for (int i = 0; i < rawContactIds.length; i++) {
893 c.moveToPosition(i);
894 long rawContactId = c.getLong(JoinContactQuery._ID);
895 rawContactIds[i] = rawContactId;
896 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
897 if (nameSource > maxDisplayNameSource) {
898 maxDisplayNameSource = nameSource;
899 }
900 }
901
902 // Find an appropriate display name for the joined contact:
903 // if should have a higher DisplayNameSource or be the name
904 // of the original contact that we are joining with another.
905 if (writable) {
906 for (int i = 0; i < rawContactIds.length; i++) {
907 c.moveToPosition(i);
908 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
909 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
910 if (nameSource == maxDisplayNameSource
911 && (verifiedNameRawContactId == -1
912 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
913 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
914 }
915 }
916 }
917 }
918 } finally {
919 c.close();
920 }
921
922 // For each pair of raw contacts, insert an aggregation exception
923 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
924 for (int i = 0; i < rawContactIds.length; i++) {
925 for (int j = 0; j < rawContactIds.length; j++) {
926 if (i != j) {
927 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
928 }
929 }
930 }
931
932 // Mark the original contact as "name verified" to make sure that the contact
933 // display name does not change as a result of the join
934 if (verifiedNameRawContactId != -1) {
935 Builder builder = ContentProviderOperation.newUpdate(
936 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
937 builder.withValue(RawContacts.NAME_VERIFIED, 1);
938 operations.add(builder.build());
939 }
940
941 boolean success = false;
942 // Apply all aggregation exceptions as one batch
943 try {
944 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800945 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800946 success = true;
947 } catch (RemoteException e) {
948 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800949 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800950 } catch (OperationApplicationException e) {
951 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800952 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800953 }
954
955 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
956 if (success) {
957 Uri uri = RawContacts.getContactLookupUri(resolver,
958 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
959 callbackIntent.setData(uri);
960 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800961 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800962 }
963
964 /**
965 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
966 */
967 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
968 long rawContactId1, long rawContactId2) {
969 Builder builder =
970 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
971 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
972 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
973 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
974 operations.add(builder.build());
975 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800976
977 /**
978 * Shows a toast on the UI thread.
979 */
980 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800981 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800982
983 @Override
984 public void run() {
985 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
986 }
987 });
988 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800989
990 private void deliverCallback(final Intent callbackIntent) {
991 mMainHandler.post(new Runnable() {
992
993 @Override
994 public void run() {
995 deliverCallbackOnUiThread(callbackIntent);
996 }
997 });
998 }
999
1000 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1001 // TODO: this assumes that if there are multiple instances of the same
1002 // activity registered, the last one registered is the one waiting for
1003 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001004 for (Listener listener : sListeners) {
1005 if (callbackIntent.getComponent().equals(
1006 ((Activity) listener).getIntent().getComponent())) {
1007 listener.onServiceCompleted(callbackIntent);
1008 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001009 }
1010 }
1011 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001012}