blob: 78c4b18721021817dddfe937fb594d016799c3eb [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;
56import java.util.List;
Hugo Hudsona831c0b2011-08-13 11:50:15 +010057import java.util.concurrent.CopyOnWriteArrayList;
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
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100136 private static final CopyOnWriteArrayList<Listener> sListeners =
137 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800138
139 private Handler mMainHandler;
140
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700141 public ContactSaveService() {
142 super(TAG);
143 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800144 mMainHandler = new Handler(Looper.getMainLooper());
145 }
146
147 public static void registerListener(Listener listener) {
148 if (!(listener instanceof Activity)) {
149 throw new ClassCastException("Only activities can be registered to"
150 + " receive callback from " + ContactSaveService.class.getName());
151 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100152 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800153 }
154
155 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100156 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700157 }
158
159 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800160 public Object getSystemService(String name) {
161 Object service = super.getSystemService(name);
162 if (service != null) {
163 return service;
164 }
165
166 return getApplicationContext().getSystemService(name);
167 }
168
169 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700170 protected void onHandleIntent(Intent intent) {
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700171 String action = intent.getAction();
172 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
173 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800174 } else if (ACTION_SAVE_CONTACT.equals(action)) {
175 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800176 } else if (ACTION_CREATE_GROUP.equals(action)) {
177 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800178 } else if (ACTION_RENAME_GROUP.equals(action)) {
179 renameGroup(intent);
180 } else if (ACTION_DELETE_GROUP.equals(action)) {
181 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700182 } else if (ACTION_UPDATE_GROUP.equals(action)) {
183 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800184 } else if (ACTION_SET_STARRED.equals(action)) {
185 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800186 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
187 setSuperPrimary(intent);
188 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
189 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800190 } else if (ACTION_DELETE_CONTACT.equals(action)) {
191 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800192 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
193 joinContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700194 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
195 setSendToVoicemail(intent);
196 } else if (ACTION_SET_RINGTONE.equals(action)) {
197 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700198 }
199 }
200
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800201 /**
202 * Creates an intent that can be sent to this service to create a new raw contact
203 * using data presented as a set of ContentValues.
204 */
205 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700206 ArrayList<ContentValues> values, AccountWithDataSet account,
207 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800208 Intent serviceIntent = new Intent(
209 context, ContactSaveService.class);
210 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
211 if (account != null) {
212 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
213 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700214 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800215 }
216 serviceIntent.putParcelableArrayListExtra(
217 ContactSaveService.EXTRA_CONTENT_VALUES, values);
218
219 // Callback intent will be invoked by the service once the new contact is
220 // created. The service will put the URI of the new contact as "data" on
221 // the callback intent.
222 Intent callbackIntent = new Intent(context, callbackActivity);
223 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800224 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
225 return serviceIntent;
226 }
227
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700228 private void createRawContact(Intent intent) {
229 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
230 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700231 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700232 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
233 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
234
235 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
236 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
237 .withValue(RawContacts.ACCOUNT_NAME, accountName)
238 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700239 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700240 .build());
241
242 int size = valueList.size();
243 for (int i = 0; i < size; i++) {
244 ContentValues values = valueList.get(i);
245 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
246 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
247 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
248 .withValues(values)
249 .build());
250 }
251
252 ContentResolver resolver = getContentResolver();
253 ContentProviderResult[] results;
254 try {
255 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
256 } catch (Exception e) {
257 throw new RuntimeException("Failed to store new contact", e);
258 }
259
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700260 Uri rawContactUri = results[0].uri;
261 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
262
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800263 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700264 }
265
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700266 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800267 * Creates an intent that can be sent to this service to create a new raw contact
268 * using data presented as a set of ContentValues.
269 */
270 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700271 String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800272 String callbackAction) {
273 Intent serviceIntent = new Intent(
274 context, ContactSaveService.class);
275 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
276 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700277 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800278
279 // Callback intent will be invoked by the service once the contact is
280 // saved. The service will put the URI of the new contact as "data" on
281 // the callback intent.
282 Intent callbackIntent = new Intent(context, callbackActivity);
283 callbackIntent.putExtra(saveModeExtraKey, saveMode);
284 callbackIntent.setAction(callbackAction);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800285 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
286 return serviceIntent;
287 }
288
289 private void saveContact(Intent intent) {
290 EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
291 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700292 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800293
294 // Trim any empty fields, and RawContacts, before persisting
295 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
296 EntityModifier.trimEmpty(state, accountTypes);
297
298 Uri lookupUri = null;
299
300 final ContentResolver resolver = getContentResolver();
301
302 // Attempt to persist changes
303 int tries = 0;
304 while (tries++ < PERSIST_TRIES) {
305 try {
306 // Build operations and try applying
307 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700308 if (DEBUG) {
309 Log.v(TAG, "Content Provider Operations:");
310 for (ContentProviderOperation operation : diff) {
311 Log.v(TAG, operation.toString());
312 }
313 }
314
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800315 ContentProviderResult[] results = null;
316 if (!diff.isEmpty()) {
317 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
318 }
319
320 final long rawContactId = getRawContactId(state, diff, results);
321 if (rawContactId == -1) {
322 throw new IllegalStateException("Could not determine RawContact ID after save");
323 }
324 final Uri rawContactUri = ContentUris.withAppendedId(
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700325 isProfile ? Profile.CONTENT_RAW_CONTACTS_URI : RawContacts.CONTENT_URI,
326 rawContactId);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800327 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
328 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
329 break;
330
331 } catch (RemoteException e) {
332 // Something went wrong, bail without success
333 Log.e(TAG, "Problem persisting user edits", e);
334 break;
335
336 } catch (OperationApplicationException e) {
337 // Version consistency failed, re-parent change and try again
338 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
339 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
340 boolean first = true;
341 final int count = state.size();
342 for (int i = 0; i < count; i++) {
343 Long rawContactId = state.getRawContactId(i);
344 if (rawContactId != null && rawContactId != -1) {
345 if (!first) {
346 sb.append(',');
347 }
348 sb.append(rawContactId);
349 first = false;
350 }
351 }
352 sb.append(")");
353
354 if (first) {
355 throw new IllegalStateException("Version consistency failed for a new contact");
356 }
357
358 final EntityDeltaList newState = EntityDeltaList.fromQuery(resolver,
359 sb.toString(), null, null);
360 state = EntityDeltaList.mergeAfter(newState, state);
361 }
362 }
363
364 callbackIntent.setData(lookupUri);
365
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800366 deliverCallback(callbackIntent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800367 }
368
369 private long getRawContactId(EntityDeltaList state,
370 final ArrayList<ContentProviderOperation> diff,
371 final ContentProviderResult[] results) {
372 long rawContactId = state.findRawContactId();
373 if (rawContactId != -1) {
374 return rawContactId;
375 }
376
377 final int diffSize = diff.size();
378 for (int i = 0; i < diffSize; i++) {
379 ContentProviderOperation operation = diff.get(i);
380 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
381 && operation.getUri().getEncodedPath().contains(
382 RawContacts.CONTENT_URI.getEncodedPath())) {
383 return ContentUris.parseId(results[i].uri);
384 }
385 }
386 return -1;
387 }
388
389 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700390 * Creates an intent that can be sent to this service to create a new group as
391 * well as add new members at the same time.
392 *
393 * @param context of the application
394 * @param account in which the group should be created
395 * @param label is the name of the group (cannot be null)
396 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
397 * should be added to the group
398 * @param callbackActivity is the activity to send the callback intent to
399 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700400 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700401 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Katherine Kuan717e3432011-07-13 17:03:24 -0700402 String label, long[] rawContactsToAdd, Class<?> callbackActivity,
403 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800404 Intent serviceIntent = new Intent(context, ContactSaveService.class);
405 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
406 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
407 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700408 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800409 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700410 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700411
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800412 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700413 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800414 Intent callbackIntent = new Intent(context, callbackActivity);
415 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700416 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800417
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700418 return serviceIntent;
419 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800420
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800421 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700422 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
423 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
424 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
425 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700426 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800427
428 ContentValues values = new ContentValues();
429 values.put(Groups.ACCOUNT_TYPE, accountType);
430 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700431 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800432 values.put(Groups.TITLE, label);
433
Katherine Kuan717e3432011-07-13 17:03:24 -0700434 final ContentResolver resolver = getContentResolver();
435
436 // Create the new group
437 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
438
439 // If there's no URI, then the insertion failed. Abort early because group members can't be
440 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800441 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700442 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800443 return;
444 }
445
Katherine Kuan717e3432011-07-13 17:03:24 -0700446 // Add new group members
447 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
448
449 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
450 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800451 values.clear();
452 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
453 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
454
455 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700456 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700457 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800458 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800459 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800460 }
461
462 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800463 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800464 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700465 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
466 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800467 Intent serviceIntent = new Intent(context, ContactSaveService.class);
468 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
469 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
470 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700471
472 // Callback intent will be invoked by the service once the group is renamed.
473 Intent callbackIntent = new Intent(context, callbackActivity);
474 callbackIntent.setAction(callbackAction);
475 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
476
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800477 return serviceIntent;
478 }
479
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800480 private void renameGroup(Intent intent) {
481 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
482 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
483
484 if (groupId == -1) {
485 Log.e(TAG, "Invalid arguments for renameGroup request");
486 return;
487 }
488
489 ContentValues values = new ContentValues();
490 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700491 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
492 getContentResolver().update(groupUri, values, null, null);
493
494 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
495 callbackIntent.setData(groupUri);
496 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800497 }
498
499 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800500 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800501 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800502 public static Intent createGroupDeletionIntent(Context context, long groupId) {
503 Intent serviceIntent = new Intent(context, ContactSaveService.class);
504 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800505 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800506 return serviceIntent;
507 }
508
509 private void deleteGroup(Intent intent) {
510 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
511 if (groupId == -1) {
512 Log.e(TAG, "Invalid arguments for deleteGroup request");
513 return;
514 }
515
516 getContentResolver().delete(
517 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
518 }
519
520 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700521 * Creates an intent that can be sent to this service to rename a group as
522 * well as add and remove members from the group.
523 *
524 * @param context of the application
525 * @param groupId of the group that should be modified
526 * @param newLabel is the updated name of the group (can be null if the name
527 * should not be updated)
528 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
529 * should be added to the group
530 * @param rawContactsToRemove is an array of raw contact IDs for contacts
531 * that should be removed from the group
532 * @param callbackActivity is the activity to send the callback intent to
533 * @param callbackAction is the intent action for the callback intent
534 */
535 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
536 long[] rawContactsToAdd, long[] rawContactsToRemove,
537 Class<?> callbackActivity, String callbackAction) {
538 Intent serviceIntent = new Intent(context, ContactSaveService.class);
539 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
540 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
541 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
542 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
543 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
544 rawContactsToRemove);
545
546 // Callback intent will be invoked by the service once the group is updated
547 Intent callbackIntent = new Intent(context, callbackActivity);
548 callbackIntent.setAction(callbackAction);
549 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
550
551 return serviceIntent;
552 }
553
554 private void updateGroup(Intent intent) {
555 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
556 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
557 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
558 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
559
560 if (groupId == -1) {
561 Log.e(TAG, "Invalid arguments for updateGroup request");
562 return;
563 }
564
565 final ContentResolver resolver = getContentResolver();
566 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
567
568 // Update group name if necessary
569 if (label != null) {
570 ContentValues values = new ContentValues();
571 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700572 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700573 }
574
Katherine Kuan717e3432011-07-13 17:03:24 -0700575 // Add and remove members if necessary
576 addMembersToGroup(resolver, rawContactsToAdd, groupId);
577 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
578
579 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
580 callbackIntent.setData(groupUri);
581 deliverCallback(callbackIntent);
582 }
583
584 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
585 long groupId) {
586 if (rawContactsToAdd == null) {
587 return;
588 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700589 for (long rawContactId : rawContactsToAdd) {
590 try {
591 final ArrayList<ContentProviderOperation> rawContactOperations =
592 new ArrayList<ContentProviderOperation>();
593
594 // Build an assert operation to ensure the contact is not already in the group
595 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
596 .newAssertQuery(Data.CONTENT_URI);
597 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
598 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
599 new String[] { String.valueOf(rawContactId),
600 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
601 assertBuilder.withExpectedCount(0);
602 rawContactOperations.add(assertBuilder.build());
603
604 // Build an insert operation to add the contact to the group
605 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
606 .newInsert(Data.CONTENT_URI);
607 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
608 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
609 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
610 rawContactOperations.add(insertBuilder.build());
611
612 if (DEBUG) {
613 for (ContentProviderOperation operation : rawContactOperations) {
614 Log.v(TAG, operation.toString());
615 }
616 }
617
618 // Apply batch
619 ContentProviderResult[] results = null;
620 if (!rawContactOperations.isEmpty()) {
621 results = resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
622 }
623 } catch (RemoteException e) {
624 // Something went wrong, bail without success
625 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
626 String.valueOf(rawContactId), e);
627 } catch (OperationApplicationException e) {
628 // The assert could have failed because the contact is already in the group,
629 // just continue to the next contact
630 Log.w(TAG, "Assert failed in adding raw contact ID " +
631 String.valueOf(rawContactId) + ". Already exists in group " +
632 String.valueOf(groupId), e);
633 }
634 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700635 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700636
Katherine Kuan717e3432011-07-13 17:03:24 -0700637 private void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
638 long groupId) {
639 if (rawContactsToRemove == null) {
640 return;
641 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700642 for (long rawContactId : rawContactsToRemove) {
643 // Apply the delete operation on the data row for the given raw contact's
644 // membership in the given group. If no contact matches the provided selection, then
645 // nothing will be done. Just continue to the next contact.
646 getContentResolver().delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
647 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
648 new String[] { String.valueOf(rawContactId),
649 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
650 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700651 }
652
653 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800654 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800655 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800656 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
657 Intent serviceIntent = new Intent(context, ContactSaveService.class);
658 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
659 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
660 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
661
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800662 return serviceIntent;
663 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800664
665 private void setStarred(Intent intent) {
666 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
667 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
668 if (contactUri == null) {
669 Log.e(TAG, "Invalid arguments for setStarred request");
670 return;
671 }
672
673 final ContentValues values = new ContentValues(1);
674 values.put(Contacts.STARRED, value);
675 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800676 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800677
678 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700679 * Creates an intent that can be sent to this service to set the redirect to voicemail.
680 */
681 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
682 boolean value) {
683 Intent serviceIntent = new Intent(context, ContactSaveService.class);
684 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
685 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
686 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
687
688 return serviceIntent;
689 }
690
691 private void setSendToVoicemail(Intent intent) {
692 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
693 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
694 if (contactUri == null) {
695 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
696 return;
697 }
698
699 final ContentValues values = new ContentValues(1);
700 values.put(Contacts.SEND_TO_VOICEMAIL, value);
701 getContentResolver().update(contactUri, values, null, null);
702 }
703
704 /**
705 * Creates an intent that can be sent to this service to save the contact's ringtone.
706 */
707 public static Intent createSetRingtone(Context context, Uri contactUri,
708 String value) {
709 Intent serviceIntent = new Intent(context, ContactSaveService.class);
710 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
711 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
712 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
713
714 return serviceIntent;
715 }
716
717 private void setRingtone(Intent intent) {
718 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
719 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
720 if (contactUri == null) {
721 Log.e(TAG, "Invalid arguments for setRingtone");
722 return;
723 }
724 ContentValues values = new ContentValues(1);
725 values.put(Contacts.CUSTOM_RINGTONE, value);
726 getContentResolver().update(contactUri, values, null, null);
727 }
728
729 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800730 * Creates an intent that sets the selected data item as super primary (default)
731 */
732 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
733 Intent serviceIntent = new Intent(context, ContactSaveService.class);
734 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
735 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
736 return serviceIntent;
737 }
738
739 private void setSuperPrimary(Intent intent) {
740 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
741 if (dataId == -1) {
742 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
743 return;
744 }
745
746 // Update the primary values in the data record.
747 ContentValues values = new ContentValues(1);
748 values.put(Data.IS_SUPER_PRIMARY, 1);
749 values.put(Data.IS_PRIMARY, 1);
750
751 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
752 values, null, null);
753 }
754
755 /**
756 * Creates an intent that clears the primary flag of all data items that belong to the same
757 * raw_contact as the given data item. Will only clear, if the data item was primary before
758 * this call
759 */
760 public static Intent createClearPrimaryIntent(Context context, long dataId) {
761 Intent serviceIntent = new Intent(context, ContactSaveService.class);
762 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
763 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
764 return serviceIntent;
765 }
766
767 private void clearPrimary(Intent intent) {
768 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
769 if (dataId == -1) {
770 Log.e(TAG, "Invalid arguments for clearPrimary request");
771 return;
772 }
773
774 // Update the primary values in the data record.
775 ContentValues values = new ContentValues(1);
776 values.put(Data.IS_SUPER_PRIMARY, 0);
777 values.put(Data.IS_PRIMARY, 0);
778
779 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
780 values, null, null);
781 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800782
783 /**
784 * Creates an intent that can be sent to this service to delete a contact.
785 */
786 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
787 Intent serviceIntent = new Intent(context, ContactSaveService.class);
788 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
789 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
790 return serviceIntent;
791 }
792
793 private void deleteContact(Intent intent) {
794 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
795 if (contactUri == null) {
796 Log.e(TAG, "Invalid arguments for deleteContact request");
797 return;
798 }
799
800 getContentResolver().delete(contactUri, null, null);
801 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800802
803 /**
804 * Creates an intent that can be sent to this service to join two contacts.
805 */
806 public static Intent createJoinContactsIntent(Context context, long contactId1,
807 long contactId2, boolean contactWritable,
808 Class<?> callbackActivity, String callbackAction) {
809 Intent serviceIntent = new Intent(context, ContactSaveService.class);
810 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
811 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
812 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
813 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
814
815 // Callback intent will be invoked by the service once the contacts are joined.
816 Intent callbackIntent = new Intent(context, callbackActivity);
817 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800818 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
819
820 return serviceIntent;
821 }
822
823
824 private interface JoinContactQuery {
825 String[] PROJECTION = {
826 RawContacts._ID,
827 RawContacts.CONTACT_ID,
828 RawContacts.NAME_VERIFIED,
829 RawContacts.DISPLAY_NAME_SOURCE,
830 };
831
832 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
833
834 int _ID = 0;
835 int CONTACT_ID = 1;
836 int NAME_VERIFIED = 2;
837 int DISPLAY_NAME_SOURCE = 3;
838 }
839
840 private void joinContacts(Intent intent) {
841 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
842 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
843 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
844 if (contactId1 == -1 || contactId2 == -1) {
845 Log.e(TAG, "Invalid arguments for joinContacts request");
846 return;
847 }
848
849 final ContentResolver resolver = getContentResolver();
850
851 // Load raw contact IDs for all raw contacts involved - currently edited and selected
852 // in the join UIs
853 Cursor c = resolver.query(RawContacts.CONTENT_URI,
854 JoinContactQuery.PROJECTION,
855 JoinContactQuery.SELECTION,
856 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
857
858 long rawContactIds[];
859 long verifiedNameRawContactId = -1;
860 try {
861 int maxDisplayNameSource = -1;
862 rawContactIds = new long[c.getCount()];
863 for (int i = 0; i < rawContactIds.length; i++) {
864 c.moveToPosition(i);
865 long rawContactId = c.getLong(JoinContactQuery._ID);
866 rawContactIds[i] = rawContactId;
867 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
868 if (nameSource > maxDisplayNameSource) {
869 maxDisplayNameSource = nameSource;
870 }
871 }
872
873 // Find an appropriate display name for the joined contact:
874 // if should have a higher DisplayNameSource or be the name
875 // of the original contact that we are joining with another.
876 if (writable) {
877 for (int i = 0; i < rawContactIds.length; i++) {
878 c.moveToPosition(i);
879 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
880 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
881 if (nameSource == maxDisplayNameSource
882 && (verifiedNameRawContactId == -1
883 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
884 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
885 }
886 }
887 }
888 }
889 } finally {
890 c.close();
891 }
892
893 // For each pair of raw contacts, insert an aggregation exception
894 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
895 for (int i = 0; i < rawContactIds.length; i++) {
896 for (int j = 0; j < rawContactIds.length; j++) {
897 if (i != j) {
898 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
899 }
900 }
901 }
902
903 // Mark the original contact as "name verified" to make sure that the contact
904 // display name does not change as a result of the join
905 if (verifiedNameRawContactId != -1) {
906 Builder builder = ContentProviderOperation.newUpdate(
907 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
908 builder.withValue(RawContacts.NAME_VERIFIED, 1);
909 operations.add(builder.build());
910 }
911
912 boolean success = false;
913 // Apply all aggregation exceptions as one batch
914 try {
915 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800916 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800917 success = true;
918 } catch (RemoteException e) {
919 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800920 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800921 } catch (OperationApplicationException 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 }
925
926 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
927 if (success) {
928 Uri uri = RawContacts.getContactLookupUri(resolver,
929 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
930 callbackIntent.setData(uri);
931 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800932 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800933 }
934
935 /**
936 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
937 */
938 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
939 long rawContactId1, long rawContactId2) {
940 Builder builder =
941 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
942 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
943 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
944 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
945 operations.add(builder.build());
946 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800947
948 /**
949 * Shows a toast on the UI thread.
950 */
951 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800952 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800953
954 @Override
955 public void run() {
956 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
957 }
958 });
959 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800960
961 private void deliverCallback(final Intent callbackIntent) {
962 mMainHandler.post(new Runnable() {
963
964 @Override
965 public void run() {
966 deliverCallbackOnUiThread(callbackIntent);
967 }
968 });
969 }
970
971 void deliverCallbackOnUiThread(final Intent callbackIntent) {
972 // TODO: this assumes that if there are multiple instances of the same
973 // activity registered, the last one registered is the one waiting for
974 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100975 for (Listener listener : sListeners) {
976 if (callbackIntent.getComponent().equals(
977 ((Activity) listener).getIntent().getComponent())) {
978 listener.onServiceCompleted(callbackIntent);
979 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800980 }
981 }
982 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700983}