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