blob: 3bb330fa097111bad7db355d58e0741356214822 [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;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080021import com.android.contacts.model.EntityDeltaList;
22import com.android.contacts.model.EntityModifier;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080023import com.google.android.collect.Lists;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070024import com.google.android.collect.Sets;
25
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080026import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070027import android.app.IntentService;
28import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080029import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070030import android.content.ContentProviderResult;
31import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080032import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070033import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080034import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070035import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080036import android.content.OperationApplicationException;
37import android.database.Cursor;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070038import android.net.Uri;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080039import android.os.Handler;
40import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080041import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080042import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070043import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080044import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080045import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080046import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070047import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080048import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070049import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070050import android.provider.ContactsContract.RawContacts;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070051import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080052import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070053
54import java.util.ArrayList;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070055import java.util.HashSet;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080056import java.util.LinkedList;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070057import java.util.List;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070058
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080059/**
60 * A service responsible for saving changes to the content provider.
61 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070062public class ContactSaveService extends IntentService {
63 private static final String TAG = "ContactSaveService";
64
Katherine Kuana007e442011-07-07 09:25:34 -070065 /** Set to true in order to view logs on content provider operations */
66 private static final boolean DEBUG = false;
67
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070068 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
69
70 public static final String EXTRA_ACCOUNT_NAME = "accountName";
71 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070072 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070073 public static final String EXTRA_CONTENT_VALUES = "contentValues";
74 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
75
Dmitri Plotnikova0114142011-02-15 13:53:21 -080076 public static final String ACTION_SAVE_CONTACT = "saveContact";
77 public static final String EXTRA_CONTACT_STATE = "state";
78 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070079 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070080
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080081 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080082 public static final String ACTION_RENAME_GROUP = "renameGroup";
83 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070084 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080085 public static final String EXTRA_GROUP_ID = "groupId";
86 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070087 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
88 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080089
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080090 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -080091 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080092 public static final String EXTRA_CONTACT_URI = "contactUri";
93 public static final String EXTRA_STARRED_FLAG = "starred";
94
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -080095 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
96 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
97 public static final String EXTRA_DATA_ID = "dataId";
98
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080099 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
100 public static final String EXTRA_CONTACT_ID1 = "contactId1";
101 public static final String EXTRA_CONTACT_ID2 = "contactId2";
102 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
103
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700104 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
105 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
106
107 public static final String ACTION_SET_RINGTONE = "setRingtone";
108 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
109
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700110 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
111 Data.MIMETYPE,
112 Data.IS_PRIMARY,
113 Data.DATA1,
114 Data.DATA2,
115 Data.DATA3,
116 Data.DATA4,
117 Data.DATA5,
118 Data.DATA6,
119 Data.DATA7,
120 Data.DATA8,
121 Data.DATA9,
122 Data.DATA10,
123 Data.DATA11,
124 Data.DATA12,
125 Data.DATA13,
126 Data.DATA14,
127 Data.DATA15
128 );
129
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800130 private static final int PERSIST_TRIES = 3;
131
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800132 public interface Listener {
133 public void onServiceCompleted(Intent callbackIntent);
134 }
135
136 private static final LinkedList<Listener> sListeners = new LinkedList<Listener>();
137
138 private Handler mMainHandler;
139
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700140 public ContactSaveService() {
141 super(TAG);
142 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800143 mMainHandler = new Handler(Looper.getMainLooper());
144 }
145
146 public static void registerListener(Listener listener) {
147 if (!(listener instanceof Activity)) {
148 throw new ClassCastException("Only activities can be registered to"
149 + " receive callback from " + ContactSaveService.class.getName());
150 }
151 synchronized (sListeners) {
152 sListeners.addFirst(listener);
153 }
154 }
155
156 public static void unregisterListener(Listener listener) {
157 synchronized (sListeners) {
158 sListeners.remove(listener);
159 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700160 }
161
162 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800163 public Object getSystemService(String name) {
164 Object service = super.getSystemService(name);
165 if (service != null) {
166 return service;
167 }
168
169 return getApplicationContext().getSystemService(name);
170 }
171
172 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700173 protected void onHandleIntent(Intent intent) {
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700174 String action = intent.getAction();
175 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
176 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800177 } else if (ACTION_SAVE_CONTACT.equals(action)) {
178 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800179 } else if (ACTION_CREATE_GROUP.equals(action)) {
180 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800181 } else if (ACTION_RENAME_GROUP.equals(action)) {
182 renameGroup(intent);
183 } else if (ACTION_DELETE_GROUP.equals(action)) {
184 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700185 } else if (ACTION_UPDATE_GROUP.equals(action)) {
186 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800187 } else if (ACTION_SET_STARRED.equals(action)) {
188 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800189 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
190 setSuperPrimary(intent);
191 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
192 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800193 } else if (ACTION_DELETE_CONTACT.equals(action)) {
194 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800195 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
196 joinContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700197 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
198 setSendToVoicemail(intent);
199 } else if (ACTION_SET_RINGTONE.equals(action)) {
200 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700201 }
202 }
203
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800204 /**
205 * Creates an intent that can be sent to this service to create a new raw contact
206 * using data presented as a set of ContentValues.
207 */
208 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700209 ArrayList<ContentValues> values, AccountWithDataSet account,
210 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800211 Intent serviceIntent = new Intent(
212 context, ContactSaveService.class);
213 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
214 if (account != null) {
215 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
216 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700217 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800218 }
219 serviceIntent.putParcelableArrayListExtra(
220 ContactSaveService.EXTRA_CONTENT_VALUES, values);
221
222 // Callback intent will be invoked by the service once the new contact is
223 // created. The service will put the URI of the new contact as "data" on
224 // the callback intent.
225 Intent callbackIntent = new Intent(context, callbackActivity);
226 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800227 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
228 return serviceIntent;
229 }
230
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700231 private void createRawContact(Intent intent) {
232 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
233 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700234 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700235 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
236 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
237
238 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
239 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
240 .withValue(RawContacts.ACCOUNT_NAME, accountName)
241 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700242 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700243 .build());
244
245 int size = valueList.size();
246 for (int i = 0; i < size; i++) {
247 ContentValues values = valueList.get(i);
248 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
249 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
250 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
251 .withValues(values)
252 .build());
253 }
254
255 ContentResolver resolver = getContentResolver();
256 ContentProviderResult[] results;
257 try {
258 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
259 } catch (Exception e) {
260 throw new RuntimeException("Failed to store new contact", e);
261 }
262
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700263 Uri rawContactUri = results[0].uri;
264 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
265
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800266 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700267 }
268
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700269 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800270 * Creates an intent that can be sent to this service to create a new raw contact
271 * using data presented as a set of ContentValues.
272 */
273 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700274 String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800275 String callbackAction) {
276 Intent serviceIntent = new Intent(
277 context, ContactSaveService.class);
278 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
279 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700280 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800281
282 // Callback intent will be invoked by the service once the contact is
283 // saved. The service will put the URI of the new contact as "data" on
284 // the callback intent.
285 Intent callbackIntent = new Intent(context, callbackActivity);
286 callbackIntent.putExtra(saveModeExtraKey, saveMode);
287 callbackIntent.setAction(callbackAction);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800288 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
289 return serviceIntent;
290 }
291
292 private void saveContact(Intent intent) {
293 EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
294 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700295 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800296
297 // Trim any empty fields, and RawContacts, before persisting
298 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
299 EntityModifier.trimEmpty(state, accountTypes);
300
301 Uri lookupUri = null;
302
303 final ContentResolver resolver = getContentResolver();
304
305 // Attempt to persist changes
306 int tries = 0;
307 while (tries++ < PERSIST_TRIES) {
308 try {
309 // Build operations and try applying
310 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700311 if (DEBUG) {
312 Log.v(TAG, "Content Provider Operations:");
313 for (ContentProviderOperation operation : diff) {
314 Log.v(TAG, operation.toString());
315 }
316 }
317
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800318 ContentProviderResult[] results = null;
319 if (!diff.isEmpty()) {
320 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
321 }
322
323 final long rawContactId = getRawContactId(state, diff, results);
324 if (rawContactId == -1) {
325 throw new IllegalStateException("Could not determine RawContact ID after save");
326 }
327 final Uri rawContactUri = ContentUris.withAppendedId(
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700328 isProfile ? Profile.CONTENT_RAW_CONTACTS_URI : RawContacts.CONTENT_URI,
329 rawContactId);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800330 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
331 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
332 break;
333
334 } catch (RemoteException e) {
335 // Something went wrong, bail without success
336 Log.e(TAG, "Problem persisting user edits", e);
337 break;
338
339 } catch (OperationApplicationException e) {
340 // Version consistency failed, re-parent change and try again
341 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
342 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
343 boolean first = true;
344 final int count = state.size();
345 for (int i = 0; i < count; i++) {
346 Long rawContactId = state.getRawContactId(i);
347 if (rawContactId != null && rawContactId != -1) {
348 if (!first) {
349 sb.append(',');
350 }
351 sb.append(rawContactId);
352 first = false;
353 }
354 }
355 sb.append(")");
356
357 if (first) {
358 throw new IllegalStateException("Version consistency failed for a new contact");
359 }
360
361 final EntityDeltaList newState = EntityDeltaList.fromQuery(resolver,
362 sb.toString(), null, null);
363 state = EntityDeltaList.mergeAfter(newState, state);
364 }
365 }
366
367 callbackIntent.setData(lookupUri);
368
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800369 deliverCallback(callbackIntent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800370 }
371
372 private long getRawContactId(EntityDeltaList state,
373 final ArrayList<ContentProviderOperation> diff,
374 final ContentProviderResult[] results) {
375 long rawContactId = state.findRawContactId();
376 if (rawContactId != -1) {
377 return rawContactId;
378 }
379
380 final int diffSize = diff.size();
381 for (int i = 0; i < diffSize; i++) {
382 ContentProviderOperation operation = diff.get(i);
383 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
384 && operation.getUri().getEncodedPath().contains(
385 RawContacts.CONTENT_URI.getEncodedPath())) {
386 return ContentUris.parseId(results[i].uri);
387 }
388 }
389 return -1;
390 }
391
392 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700393 * Creates an intent that can be sent to this service to create a new group as
394 * well as add new members at the same time.
395 *
396 * @param context of the application
397 * @param account in which the group should be created
398 * @param label is the name of the group (cannot be null)
399 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
400 * should be added to the group
401 * @param callbackActivity is the activity to send the callback intent to
402 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700403 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700404 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Katherine Kuan717e3432011-07-13 17:03:24 -0700405 String label, long[] rawContactsToAdd, Class<?> callbackActivity,
406 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800407 Intent serviceIntent = new Intent(context, ContactSaveService.class);
408 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
409 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
410 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700411 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800412 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700413 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700414
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800415 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700416 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800417 Intent callbackIntent = new Intent(context, callbackActivity);
418 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700419 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800420
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700421 return serviceIntent;
422 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800423
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800424 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700425 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
426 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
427 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
428 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700429 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800430
431 ContentValues values = new ContentValues();
432 values.put(Groups.ACCOUNT_TYPE, accountType);
433 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700434 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800435 values.put(Groups.TITLE, label);
436
Katherine Kuan717e3432011-07-13 17:03:24 -0700437 final ContentResolver resolver = getContentResolver();
438
439 // Create the new group
440 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
441
442 // If there's no URI, then the insertion failed. Abort early because group members can't be
443 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800444 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700445 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800446 return;
447 }
448
Katherine Kuan717e3432011-07-13 17:03:24 -0700449 // Add new group members
450 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
451
452 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
453 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800454 values.clear();
455 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
456 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
457
458 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700459 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700460 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800461 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800462 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800463 }
464
465 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800466 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800467 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700468 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
469 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800470 Intent serviceIntent = new Intent(context, ContactSaveService.class);
471 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
472 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
473 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700474
475 // Callback intent will be invoked by the service once the group is renamed.
476 Intent callbackIntent = new Intent(context, callbackActivity);
477 callbackIntent.setAction(callbackAction);
478 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
479
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800480 return serviceIntent;
481 }
482
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800483 private void renameGroup(Intent intent) {
484 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
485 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
486
487 if (groupId == -1) {
488 Log.e(TAG, "Invalid arguments for renameGroup request");
489 return;
490 }
491
492 ContentValues values = new ContentValues();
493 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700494 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
495 getContentResolver().update(groupUri, values, null, null);
496
497 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
498 callbackIntent.setData(groupUri);
499 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800500 }
501
502 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800503 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800504 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800505 public static Intent createGroupDeletionIntent(Context context, long groupId) {
506 Intent serviceIntent = new Intent(context, ContactSaveService.class);
507 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800508 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800509 return serviceIntent;
510 }
511
512 private void deleteGroup(Intent intent) {
513 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
514 if (groupId == -1) {
515 Log.e(TAG, "Invalid arguments for deleteGroup request");
516 return;
517 }
518
519 getContentResolver().delete(
520 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
521 }
522
523 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700524 * Creates an intent that can be sent to this service to rename a group as
525 * well as add and remove members from the group.
526 *
527 * @param context of the application
528 * @param groupId of the group that should be modified
529 * @param newLabel is the updated name of the group (can be null if the name
530 * should not be updated)
531 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
532 * should be added to the group
533 * @param rawContactsToRemove is an array of raw contact IDs for contacts
534 * that should be removed from the group
535 * @param callbackActivity is the activity to send the callback intent to
536 * @param callbackAction is the intent action for the callback intent
537 */
538 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
539 long[] rawContactsToAdd, long[] rawContactsToRemove,
540 Class<?> callbackActivity, String callbackAction) {
541 Intent serviceIntent = new Intent(context, ContactSaveService.class);
542 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
543 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
544 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
545 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
546 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
547 rawContactsToRemove);
548
549 // Callback intent will be invoked by the service once the group is updated
550 Intent callbackIntent = new Intent(context, callbackActivity);
551 callbackIntent.setAction(callbackAction);
552 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
553
554 return serviceIntent;
555 }
556
557 private void updateGroup(Intent intent) {
558 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
559 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
560 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
561 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
562
563 if (groupId == -1) {
564 Log.e(TAG, "Invalid arguments for updateGroup request");
565 return;
566 }
567
568 final ContentResolver resolver = getContentResolver();
569 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
570
571 // Update group name if necessary
572 if (label != null) {
573 ContentValues values = new ContentValues();
574 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700575 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700576 }
577
Katherine Kuan717e3432011-07-13 17:03:24 -0700578 // Add and remove members if necessary
579 addMembersToGroup(resolver, rawContactsToAdd, groupId);
580 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
581
582 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
583 callbackIntent.setData(groupUri);
584 deliverCallback(callbackIntent);
585 }
586
587 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
588 long groupId) {
589 if (rawContactsToAdd == null) {
590 return;
591 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700592 for (long rawContactId : rawContactsToAdd) {
593 try {
594 final ArrayList<ContentProviderOperation> rawContactOperations =
595 new ArrayList<ContentProviderOperation>();
596
597 // Build an assert operation to ensure the contact is not already in the group
598 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
599 .newAssertQuery(Data.CONTENT_URI);
600 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
601 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
602 new String[] { String.valueOf(rawContactId),
603 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
604 assertBuilder.withExpectedCount(0);
605 rawContactOperations.add(assertBuilder.build());
606
607 // Build an insert operation to add the contact to the group
608 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
609 .newInsert(Data.CONTENT_URI);
610 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
611 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
612 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
613 rawContactOperations.add(insertBuilder.build());
614
615 if (DEBUG) {
616 for (ContentProviderOperation operation : rawContactOperations) {
617 Log.v(TAG, operation.toString());
618 }
619 }
620
621 // Apply batch
622 ContentProviderResult[] results = null;
623 if (!rawContactOperations.isEmpty()) {
624 results = resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
625 }
626 } catch (RemoteException e) {
627 // Something went wrong, bail without success
628 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
629 String.valueOf(rawContactId), e);
630 } catch (OperationApplicationException e) {
631 // The assert could have failed because the contact is already in the group,
632 // just continue to the next contact
633 Log.w(TAG, "Assert failed in adding raw contact ID " +
634 String.valueOf(rawContactId) + ". Already exists in group " +
635 String.valueOf(groupId), e);
636 }
637 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700638 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700639
Katherine Kuan717e3432011-07-13 17:03:24 -0700640 private void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
641 long groupId) {
642 if (rawContactsToRemove == null) {
643 return;
644 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700645 for (long rawContactId : rawContactsToRemove) {
646 // Apply the delete operation on the data row for the given raw contact's
647 // membership in the given group. If no contact matches the provided selection, then
648 // nothing will be done. Just continue to the next contact.
649 getContentResolver().delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
650 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
651 new String[] { String.valueOf(rawContactId),
652 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
653 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700654 }
655
656 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800657 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800658 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800659 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
660 Intent serviceIntent = new Intent(context, ContactSaveService.class);
661 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
662 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
663 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
664
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800665 return serviceIntent;
666 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800667
668 private void setStarred(Intent intent) {
669 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
670 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
671 if (contactUri == null) {
672 Log.e(TAG, "Invalid arguments for setStarred request");
673 return;
674 }
675
676 final ContentValues values = new ContentValues(1);
677 values.put(Contacts.STARRED, value);
678 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800679 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800680
681 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700682 * Creates an intent that can be sent to this service to set the redirect to voicemail.
683 */
684 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
685 boolean value) {
686 Intent serviceIntent = new Intent(context, ContactSaveService.class);
687 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
688 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
689 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
690
691 return serviceIntent;
692 }
693
694 private void setSendToVoicemail(Intent intent) {
695 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
696 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
697 if (contactUri == null) {
698 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
699 return;
700 }
701
702 final ContentValues values = new ContentValues(1);
703 values.put(Contacts.SEND_TO_VOICEMAIL, value);
704 getContentResolver().update(contactUri, values, null, null);
705 }
706
707 /**
708 * Creates an intent that can be sent to this service to save the contact's ringtone.
709 */
710 public static Intent createSetRingtone(Context context, Uri contactUri,
711 String value) {
712 Intent serviceIntent = new Intent(context, ContactSaveService.class);
713 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
714 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
715 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
716
717 return serviceIntent;
718 }
719
720 private void setRingtone(Intent intent) {
721 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
722 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
723 if (contactUri == null) {
724 Log.e(TAG, "Invalid arguments for setRingtone");
725 return;
726 }
727 ContentValues values = new ContentValues(1);
728 values.put(Contacts.CUSTOM_RINGTONE, value);
729 getContentResolver().update(contactUri, values, null, null);
730 }
731
732 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800733 * Creates an intent that sets the selected data item as super primary (default)
734 */
735 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
736 Intent serviceIntent = new Intent(context, ContactSaveService.class);
737 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
738 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
739 return serviceIntent;
740 }
741
742 private void setSuperPrimary(Intent intent) {
743 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
744 if (dataId == -1) {
745 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
746 return;
747 }
748
749 // Update the primary values in the data record.
750 ContentValues values = new ContentValues(1);
751 values.put(Data.IS_SUPER_PRIMARY, 1);
752 values.put(Data.IS_PRIMARY, 1);
753
754 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
755 values, null, null);
756 }
757
758 /**
759 * Creates an intent that clears the primary flag of all data items that belong to the same
760 * raw_contact as the given data item. Will only clear, if the data item was primary before
761 * this call
762 */
763 public static Intent createClearPrimaryIntent(Context context, long dataId) {
764 Intent serviceIntent = new Intent(context, ContactSaveService.class);
765 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
766 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
767 return serviceIntent;
768 }
769
770 private void clearPrimary(Intent intent) {
771 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
772 if (dataId == -1) {
773 Log.e(TAG, "Invalid arguments for clearPrimary request");
774 return;
775 }
776
777 // Update the primary values in the data record.
778 ContentValues values = new ContentValues(1);
779 values.put(Data.IS_SUPER_PRIMARY, 0);
780 values.put(Data.IS_PRIMARY, 0);
781
782 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
783 values, null, null);
784 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800785
786 /**
787 * Creates an intent that can be sent to this service to delete a contact.
788 */
789 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
790 Intent serviceIntent = new Intent(context, ContactSaveService.class);
791 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
792 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
793 return serviceIntent;
794 }
795
796 private void deleteContact(Intent intent) {
797 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
798 if (contactUri == null) {
799 Log.e(TAG, "Invalid arguments for deleteContact request");
800 return;
801 }
802
803 getContentResolver().delete(contactUri, null, null);
804 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800805
806 /**
807 * Creates an intent that can be sent to this service to join two contacts.
808 */
809 public static Intent createJoinContactsIntent(Context context, long contactId1,
810 long contactId2, boolean contactWritable,
811 Class<?> callbackActivity, String callbackAction) {
812 Intent serviceIntent = new Intent(context, ContactSaveService.class);
813 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
814 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
815 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
816 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
817
818 // Callback intent will be invoked by the service once the contacts are joined.
819 Intent callbackIntent = new Intent(context, callbackActivity);
820 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800821 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
822
823 return serviceIntent;
824 }
825
826
827 private interface JoinContactQuery {
828 String[] PROJECTION = {
829 RawContacts._ID,
830 RawContacts.CONTACT_ID,
831 RawContacts.NAME_VERIFIED,
832 RawContacts.DISPLAY_NAME_SOURCE,
833 };
834
835 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
836
837 int _ID = 0;
838 int CONTACT_ID = 1;
839 int NAME_VERIFIED = 2;
840 int DISPLAY_NAME_SOURCE = 3;
841 }
842
843 private void joinContacts(Intent intent) {
844 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
845 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
846 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
847 if (contactId1 == -1 || contactId2 == -1) {
848 Log.e(TAG, "Invalid arguments for joinContacts request");
849 return;
850 }
851
852 final ContentResolver resolver = getContentResolver();
853
854 // Load raw contact IDs for all raw contacts involved - currently edited and selected
855 // in the join UIs
856 Cursor c = resolver.query(RawContacts.CONTENT_URI,
857 JoinContactQuery.PROJECTION,
858 JoinContactQuery.SELECTION,
859 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
860
861 long rawContactIds[];
862 long verifiedNameRawContactId = -1;
863 try {
864 int maxDisplayNameSource = -1;
865 rawContactIds = new long[c.getCount()];
866 for (int i = 0; i < rawContactIds.length; i++) {
867 c.moveToPosition(i);
868 long rawContactId = c.getLong(JoinContactQuery._ID);
869 rawContactIds[i] = rawContactId;
870 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
871 if (nameSource > maxDisplayNameSource) {
872 maxDisplayNameSource = nameSource;
873 }
874 }
875
876 // Find an appropriate display name for the joined contact:
877 // if should have a higher DisplayNameSource or be the name
878 // of the original contact that we are joining with another.
879 if (writable) {
880 for (int i = 0; i < rawContactIds.length; i++) {
881 c.moveToPosition(i);
882 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
883 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
884 if (nameSource == maxDisplayNameSource
885 && (verifiedNameRawContactId == -1
886 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
887 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
888 }
889 }
890 }
891 }
892 } finally {
893 c.close();
894 }
895
896 // For each pair of raw contacts, insert an aggregation exception
897 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
898 for (int i = 0; i < rawContactIds.length; i++) {
899 for (int j = 0; j < rawContactIds.length; j++) {
900 if (i != j) {
901 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
902 }
903 }
904 }
905
906 // Mark the original contact as "name verified" to make sure that the contact
907 // display name does not change as a result of the join
908 if (verifiedNameRawContactId != -1) {
909 Builder builder = ContentProviderOperation.newUpdate(
910 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
911 builder.withValue(RawContacts.NAME_VERIFIED, 1);
912 operations.add(builder.build());
913 }
914
915 boolean success = false;
916 // Apply all aggregation exceptions as one batch
917 try {
918 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800919 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800920 success = true;
921 } catch (RemoteException e) {
922 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800923 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800924 } catch (OperationApplicationException e) {
925 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800926 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800927 }
928
929 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
930 if (success) {
931 Uri uri = RawContacts.getContactLookupUri(resolver,
932 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
933 callbackIntent.setData(uri);
934 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800935 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800936 }
937
938 /**
939 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
940 */
941 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
942 long rawContactId1, long rawContactId2) {
943 Builder builder =
944 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
945 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
946 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
947 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
948 operations.add(builder.build());
949 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800950
951 /**
952 * Shows a toast on the UI thread.
953 */
954 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800955 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800956
957 @Override
958 public void run() {
959 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
960 }
961 });
962 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800963
964 private void deliverCallback(final Intent callbackIntent) {
965 mMainHandler.post(new Runnable() {
966
967 @Override
968 public void run() {
969 deliverCallbackOnUiThread(callbackIntent);
970 }
971 });
972 }
973
974 void deliverCallbackOnUiThread(final Intent callbackIntent) {
975 // TODO: this assumes that if there are multiple instances of the same
976 // activity registered, the last one registered is the one waiting for
977 // the callback. Validity of this assumption needs to be verified.
978 synchronized (sListeners) {
979 for (Listener listener : sListeners) {
980 if (callbackIntent.getComponent().equals(
981 ((Activity) listener).getIntent().getComponent())) {
982 listener.onServiceCompleted(callbackIntent);
983 return;
984 }
985 }
986 }
987 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700988}