blob: 2df2e47acf672e6bb0d820ab907f356ade00dddf [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 }
326 final Uri rawContactUri = ContentUris.withAppendedId(
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700327 isProfile ? Profile.CONTENT_RAW_CONTACTS_URI : RawContacts.CONTENT_URI,
328 rawContactId);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800329 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
330 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
331 break;
332
333 } catch (RemoteException e) {
334 // Something went wrong, bail without success
335 Log.e(TAG, "Problem persisting user edits", e);
336 break;
337
338 } catch (OperationApplicationException e) {
339 // Version consistency failed, re-parent change and try again
340 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
341 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
342 boolean first = true;
343 final int count = state.size();
344 for (int i = 0; i < count; i++) {
345 Long rawContactId = state.getRawContactId(i);
346 if (rawContactId != null && rawContactId != -1) {
347 if (!first) {
348 sb.append(',');
349 }
350 sb.append(rawContactId);
351 first = false;
352 }
353 }
354 sb.append(")");
355
356 if (first) {
357 throw new IllegalStateException("Version consistency failed for a new contact");
358 }
359
Dave Santoroc90f95e2011-09-07 17:47:15 -0700360 final EntityDeltaList newState = EntityDeltaList.fromQuery(
361 isProfile
362 ? RawContactsEntity.PROFILE_CONTENT_URI
363 : RawContactsEntity.CONTENT_URI,
364 resolver, sb.toString(), null, null);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800365 state = EntityDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700366
367 // Update the new state to use profile URIs if appropriate.
368 if (isProfile) {
369 for (EntityDelta delta : state) {
370 delta.setProfileQueryUri();
371 }
372 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800373 }
374 }
375
376 callbackIntent.setData(lookupUri);
377
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800378 deliverCallback(callbackIntent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800379 }
380
381 private long getRawContactId(EntityDeltaList state,
382 final ArrayList<ContentProviderOperation> diff,
383 final ContentProviderResult[] results) {
384 long rawContactId = state.findRawContactId();
385 if (rawContactId != -1) {
386 return rawContactId;
387 }
388
389 final int diffSize = diff.size();
390 for (int i = 0; i < diffSize; i++) {
391 ContentProviderOperation operation = diff.get(i);
392 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
393 && operation.getUri().getEncodedPath().contains(
394 RawContacts.CONTENT_URI.getEncodedPath())) {
395 return ContentUris.parseId(results[i].uri);
396 }
397 }
398 return -1;
399 }
400
401 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700402 * Creates an intent that can be sent to this service to create a new group as
403 * well as add new members at the same time.
404 *
405 * @param context of the application
406 * @param account in which the group should be created
407 * @param label is the name of the group (cannot be null)
408 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
409 * should be added to the group
410 * @param callbackActivity is the activity to send the callback intent to
411 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700412 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700413 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Katherine Kuan717e3432011-07-13 17:03:24 -0700414 String label, long[] rawContactsToAdd, Class<?> callbackActivity,
415 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800416 Intent serviceIntent = new Intent(context, ContactSaveService.class);
417 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
418 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
419 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700420 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800421 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700422 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700423
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800424 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700425 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800426 Intent callbackIntent = new Intent(context, callbackActivity);
427 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700428 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800429
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700430 return serviceIntent;
431 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800432
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800433 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700434 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
435 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
436 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
437 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700438 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800439
440 ContentValues values = new ContentValues();
441 values.put(Groups.ACCOUNT_TYPE, accountType);
442 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700443 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800444 values.put(Groups.TITLE, label);
445
Katherine Kuan717e3432011-07-13 17:03:24 -0700446 final ContentResolver resolver = getContentResolver();
447
448 // Create the new group
449 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
450
451 // If there's no URI, then the insertion failed. Abort early because group members can't be
452 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800453 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700454 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800455 return;
456 }
457
Katherine Kuan717e3432011-07-13 17:03:24 -0700458 // Add new group members
459 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
460
461 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
462 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800463 values.clear();
464 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
465 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
466
467 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700468 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700469 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800470 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800471 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800472 }
473
474 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800475 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800476 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700477 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
478 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800479 Intent serviceIntent = new Intent(context, ContactSaveService.class);
480 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
481 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
482 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700483
484 // Callback intent will be invoked by the service once the group is renamed.
485 Intent callbackIntent = new Intent(context, callbackActivity);
486 callbackIntent.setAction(callbackAction);
487 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
488
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800489 return serviceIntent;
490 }
491
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800492 private void renameGroup(Intent intent) {
493 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
494 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
495
496 if (groupId == -1) {
497 Log.e(TAG, "Invalid arguments for renameGroup request");
498 return;
499 }
500
501 ContentValues values = new ContentValues();
502 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700503 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
504 getContentResolver().update(groupUri, values, null, null);
505
506 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
507 callbackIntent.setData(groupUri);
508 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800509 }
510
511 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800512 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800513 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800514 public static Intent createGroupDeletionIntent(Context context, long groupId) {
515 Intent serviceIntent = new Intent(context, ContactSaveService.class);
516 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800517 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800518 return serviceIntent;
519 }
520
521 private void deleteGroup(Intent intent) {
522 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
523 if (groupId == -1) {
524 Log.e(TAG, "Invalid arguments for deleteGroup request");
525 return;
526 }
527
528 getContentResolver().delete(
529 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
530 }
531
532 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700533 * Creates an intent that can be sent to this service to rename a group as
534 * well as add and remove members from the group.
535 *
536 * @param context of the application
537 * @param groupId of the group that should be modified
538 * @param newLabel is the updated name of the group (can be null if the name
539 * should not be updated)
540 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
541 * should be added to the group
542 * @param rawContactsToRemove is an array of raw contact IDs for contacts
543 * that should be removed from 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
546 */
547 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
548 long[] rawContactsToAdd, long[] rawContactsToRemove,
549 Class<?> callbackActivity, String callbackAction) {
550 Intent serviceIntent = new Intent(context, ContactSaveService.class);
551 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
552 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
553 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
554 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
555 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
556 rawContactsToRemove);
557
558 // Callback intent will be invoked by the service once the group is updated
559 Intent callbackIntent = new Intent(context, callbackActivity);
560 callbackIntent.setAction(callbackAction);
561 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
562
563 return serviceIntent;
564 }
565
566 private void updateGroup(Intent intent) {
567 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
568 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
569 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
570 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
571
572 if (groupId == -1) {
573 Log.e(TAG, "Invalid arguments for updateGroup request");
574 return;
575 }
576
577 final ContentResolver resolver = getContentResolver();
578 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
579
580 // Update group name if necessary
581 if (label != null) {
582 ContentValues values = new ContentValues();
583 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700584 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700585 }
586
Katherine Kuan717e3432011-07-13 17:03:24 -0700587 // Add and remove members if necessary
588 addMembersToGroup(resolver, rawContactsToAdd, groupId);
589 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
590
591 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
592 callbackIntent.setData(groupUri);
593 deliverCallback(callbackIntent);
594 }
595
596 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
597 long groupId) {
598 if (rawContactsToAdd == null) {
599 return;
600 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700601 for (long rawContactId : rawContactsToAdd) {
602 try {
603 final ArrayList<ContentProviderOperation> rawContactOperations =
604 new ArrayList<ContentProviderOperation>();
605
606 // Build an assert operation to ensure the contact is not already in the group
607 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
608 .newAssertQuery(Data.CONTENT_URI);
609 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
610 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
611 new String[] { String.valueOf(rawContactId),
612 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
613 assertBuilder.withExpectedCount(0);
614 rawContactOperations.add(assertBuilder.build());
615
616 // Build an insert operation to add the contact to the group
617 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
618 .newInsert(Data.CONTENT_URI);
619 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
620 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
621 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
622 rawContactOperations.add(insertBuilder.build());
623
624 if (DEBUG) {
625 for (ContentProviderOperation operation : rawContactOperations) {
626 Log.v(TAG, operation.toString());
627 }
628 }
629
630 // Apply batch
631 ContentProviderResult[] results = null;
632 if (!rawContactOperations.isEmpty()) {
633 results = resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
634 }
635 } catch (RemoteException e) {
636 // Something went wrong, bail without success
637 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
638 String.valueOf(rawContactId), e);
639 } catch (OperationApplicationException e) {
640 // The assert could have failed because the contact is already in the group,
641 // just continue to the next contact
642 Log.w(TAG, "Assert failed in adding raw contact ID " +
643 String.valueOf(rawContactId) + ". Already exists in group " +
644 String.valueOf(groupId), e);
645 }
646 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700647 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700648
Katherine Kuan717e3432011-07-13 17:03:24 -0700649 private void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
650 long groupId) {
651 if (rawContactsToRemove == null) {
652 return;
653 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700654 for (long rawContactId : rawContactsToRemove) {
655 // Apply the delete operation on the data row for the given raw contact's
656 // membership in the given group. If no contact matches the provided selection, then
657 // nothing will be done. Just continue to the next contact.
658 getContentResolver().delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
659 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
660 new String[] { String.valueOf(rawContactId),
661 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
662 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700663 }
664
665 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800666 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800667 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800668 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
669 Intent serviceIntent = new Intent(context, ContactSaveService.class);
670 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
671 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
672 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
673
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800674 return serviceIntent;
675 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800676
677 private void setStarred(Intent intent) {
678 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
679 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
680 if (contactUri == null) {
681 Log.e(TAG, "Invalid arguments for setStarred request");
682 return;
683 }
684
685 final ContentValues values = new ContentValues(1);
686 values.put(Contacts.STARRED, value);
687 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800688 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800689
690 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700691 * Creates an intent that can be sent to this service to set the redirect to voicemail.
692 */
693 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
694 boolean value) {
695 Intent serviceIntent = new Intent(context, ContactSaveService.class);
696 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
697 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
698 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
699
700 return serviceIntent;
701 }
702
703 private void setSendToVoicemail(Intent intent) {
704 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
705 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
706 if (contactUri == null) {
707 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
708 return;
709 }
710
711 final ContentValues values = new ContentValues(1);
712 values.put(Contacts.SEND_TO_VOICEMAIL, value);
713 getContentResolver().update(contactUri, values, null, null);
714 }
715
716 /**
717 * Creates an intent that can be sent to this service to save the contact's ringtone.
718 */
719 public static Intent createSetRingtone(Context context, Uri contactUri,
720 String value) {
721 Intent serviceIntent = new Intent(context, ContactSaveService.class);
722 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
723 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
724 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
725
726 return serviceIntent;
727 }
728
729 private void setRingtone(Intent intent) {
730 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
731 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
732 if (contactUri == null) {
733 Log.e(TAG, "Invalid arguments for setRingtone");
734 return;
735 }
736 ContentValues values = new ContentValues(1);
737 values.put(Contacts.CUSTOM_RINGTONE, value);
738 getContentResolver().update(contactUri, values, null, null);
739 }
740
741 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800742 * Creates an intent that sets the selected data item as super primary (default)
743 */
744 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
745 Intent serviceIntent = new Intent(context, ContactSaveService.class);
746 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
747 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
748 return serviceIntent;
749 }
750
751 private void setSuperPrimary(Intent intent) {
752 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
753 if (dataId == -1) {
754 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
755 return;
756 }
757
758 // Update the primary values in the data record.
759 ContentValues values = new ContentValues(1);
760 values.put(Data.IS_SUPER_PRIMARY, 1);
761 values.put(Data.IS_PRIMARY, 1);
762
763 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
764 values, null, null);
765 }
766
767 /**
768 * Creates an intent that clears the primary flag of all data items that belong to the same
769 * raw_contact as the given data item. Will only clear, if the data item was primary before
770 * this call
771 */
772 public static Intent createClearPrimaryIntent(Context context, long dataId) {
773 Intent serviceIntent = new Intent(context, ContactSaveService.class);
774 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
775 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
776 return serviceIntent;
777 }
778
779 private void clearPrimary(Intent intent) {
780 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
781 if (dataId == -1) {
782 Log.e(TAG, "Invalid arguments for clearPrimary request");
783 return;
784 }
785
786 // Update the primary values in the data record.
787 ContentValues values = new ContentValues(1);
788 values.put(Data.IS_SUPER_PRIMARY, 0);
789 values.put(Data.IS_PRIMARY, 0);
790
791 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
792 values, null, null);
793 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800794
795 /**
796 * Creates an intent that can be sent to this service to delete a contact.
797 */
798 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
799 Intent serviceIntent = new Intent(context, ContactSaveService.class);
800 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
801 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
802 return serviceIntent;
803 }
804
805 private void deleteContact(Intent intent) {
806 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
807 if (contactUri == null) {
808 Log.e(TAG, "Invalid arguments for deleteContact request");
809 return;
810 }
811
812 getContentResolver().delete(contactUri, null, null);
813 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800814
815 /**
816 * Creates an intent that can be sent to this service to join two contacts.
817 */
818 public static Intent createJoinContactsIntent(Context context, long contactId1,
819 long contactId2, boolean contactWritable,
820 Class<?> callbackActivity, String callbackAction) {
821 Intent serviceIntent = new Intent(context, ContactSaveService.class);
822 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
823 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
824 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
825 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
826
827 // Callback intent will be invoked by the service once the contacts are joined.
828 Intent callbackIntent = new Intent(context, callbackActivity);
829 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800830 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
831
832 return serviceIntent;
833 }
834
835
836 private interface JoinContactQuery {
837 String[] PROJECTION = {
838 RawContacts._ID,
839 RawContacts.CONTACT_ID,
840 RawContacts.NAME_VERIFIED,
841 RawContacts.DISPLAY_NAME_SOURCE,
842 };
843
844 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
845
846 int _ID = 0;
847 int CONTACT_ID = 1;
848 int NAME_VERIFIED = 2;
849 int DISPLAY_NAME_SOURCE = 3;
850 }
851
852 private void joinContacts(Intent intent) {
853 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
854 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
855 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
856 if (contactId1 == -1 || contactId2 == -1) {
857 Log.e(TAG, "Invalid arguments for joinContacts request");
858 return;
859 }
860
861 final ContentResolver resolver = getContentResolver();
862
863 // Load raw contact IDs for all raw contacts involved - currently edited and selected
864 // in the join UIs
865 Cursor c = resolver.query(RawContacts.CONTENT_URI,
866 JoinContactQuery.PROJECTION,
867 JoinContactQuery.SELECTION,
868 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
869
870 long rawContactIds[];
871 long verifiedNameRawContactId = -1;
872 try {
873 int maxDisplayNameSource = -1;
874 rawContactIds = new long[c.getCount()];
875 for (int i = 0; i < rawContactIds.length; i++) {
876 c.moveToPosition(i);
877 long rawContactId = c.getLong(JoinContactQuery._ID);
878 rawContactIds[i] = rawContactId;
879 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
880 if (nameSource > maxDisplayNameSource) {
881 maxDisplayNameSource = nameSource;
882 }
883 }
884
885 // Find an appropriate display name for the joined contact:
886 // if should have a higher DisplayNameSource or be the name
887 // of the original contact that we are joining with another.
888 if (writable) {
889 for (int i = 0; i < rawContactIds.length; i++) {
890 c.moveToPosition(i);
891 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
892 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
893 if (nameSource == maxDisplayNameSource
894 && (verifiedNameRawContactId == -1
895 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
896 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
897 }
898 }
899 }
900 }
901 } finally {
902 c.close();
903 }
904
905 // For each pair of raw contacts, insert an aggregation exception
906 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
907 for (int i = 0; i < rawContactIds.length; i++) {
908 for (int j = 0; j < rawContactIds.length; j++) {
909 if (i != j) {
910 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
911 }
912 }
913 }
914
915 // Mark the original contact as "name verified" to make sure that the contact
916 // display name does not change as a result of the join
917 if (verifiedNameRawContactId != -1) {
918 Builder builder = ContentProviderOperation.newUpdate(
919 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
920 builder.withValue(RawContacts.NAME_VERIFIED, 1);
921 operations.add(builder.build());
922 }
923
924 boolean success = false;
925 // Apply all aggregation exceptions as one batch
926 try {
927 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800928 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800929 success = true;
930 } catch (RemoteException e) {
931 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800932 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800933 } catch (OperationApplicationException e) {
934 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800935 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800936 }
937
938 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
939 if (success) {
940 Uri uri = RawContacts.getContactLookupUri(resolver,
941 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
942 callbackIntent.setData(uri);
943 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800944 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800945 }
946
947 /**
948 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
949 */
950 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
951 long rawContactId1, long rawContactId2) {
952 Builder builder =
953 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
954 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
955 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
956 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
957 operations.add(builder.build());
958 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800959
960 /**
961 * Shows a toast on the UI thread.
962 */
963 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800964 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800965
966 @Override
967 public void run() {
968 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
969 }
970 });
971 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800972
973 private void deliverCallback(final Intent callbackIntent) {
974 mMainHandler.post(new Runnable() {
975
976 @Override
977 public void run() {
978 deliverCallbackOnUiThread(callbackIntent);
979 }
980 });
981 }
982
983 void deliverCallbackOnUiThread(final Intent callbackIntent) {
984 // TODO: this assumes that if there are multiple instances of the same
985 // activity registered, the last one registered is the one waiting for
986 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100987 for (Listener listener : sListeners) {
988 if (callbackIntent.getComponent().equals(
989 ((Activity) listener).getIntent().getComponent())) {
990 listener.onServiceCompleted(callbackIntent);
991 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800992 }
993 }
994 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700995}