blob: be84cc4a085a8dc679caaff192d0ba3535ab738a [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";
Dave Santoro36d24d72011-09-25 17:08:10 -070082 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070083
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080084 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080085 public static final String ACTION_RENAME_GROUP = "renameGroup";
86 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070087 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080088 public static final String EXTRA_GROUP_ID = "groupId";
89 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070090 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
91 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080092
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080093 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -080094 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080095 public static final String EXTRA_CONTACT_URI = "contactUri";
96 public static final String EXTRA_STARRED_FLAG = "starred";
97
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -080098 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
99 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
100 public static final String EXTRA_DATA_ID = "dataId";
101
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800102 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
103 public static final String EXTRA_CONTACT_ID1 = "contactId1";
104 public static final String EXTRA_CONTACT_ID2 = "contactId2";
105 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
106
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700107 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
108 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
109
110 public static final String ACTION_SET_RINGTONE = "setRingtone";
111 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
112
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700113 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
114 Data.MIMETYPE,
115 Data.IS_PRIMARY,
116 Data.DATA1,
117 Data.DATA2,
118 Data.DATA3,
119 Data.DATA4,
120 Data.DATA5,
121 Data.DATA6,
122 Data.DATA7,
123 Data.DATA8,
124 Data.DATA9,
125 Data.DATA10,
126 Data.DATA11,
127 Data.DATA12,
128 Data.DATA13,
129 Data.DATA14,
130 Data.DATA15
131 );
132
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800133 private static final int PERSIST_TRIES = 3;
134
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800135 public interface Listener {
136 public void onServiceCompleted(Intent callbackIntent);
137 }
138
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100139 private static final CopyOnWriteArrayList<Listener> sListeners =
140 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800141
142 private Handler mMainHandler;
143
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700144 public ContactSaveService() {
145 super(TAG);
146 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800147 mMainHandler = new Handler(Looper.getMainLooper());
148 }
149
150 public static void registerListener(Listener listener) {
151 if (!(listener instanceof Activity)) {
152 throw new ClassCastException("Only activities can be registered to"
153 + " receive callback from " + ContactSaveService.class.getName());
154 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100155 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800156 }
157
158 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100159 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700160 }
161
162 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800163 public Object getSystemService(String name) {
164 Object service = super.getSystemService(name);
165 if (service != null) {
166 return service;
167 }
168
169 return getApplicationContext().getSystemService(name);
170 }
171
172 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700173 protected void onHandleIntent(Intent intent) {
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700174 String action = intent.getAction();
175 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
176 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800177 } else if (ACTION_SAVE_CONTACT.equals(action)) {
178 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800179 } else if (ACTION_CREATE_GROUP.equals(action)) {
180 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800181 } else if (ACTION_RENAME_GROUP.equals(action)) {
182 renameGroup(intent);
183 } else if (ACTION_DELETE_GROUP.equals(action)) {
184 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700185 } else if (ACTION_UPDATE_GROUP.equals(action)) {
186 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800187 } else if (ACTION_SET_STARRED.equals(action)) {
188 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800189 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
190 setSuperPrimary(intent);
191 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
192 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800193 } else if (ACTION_DELETE_CONTACT.equals(action)) {
194 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800195 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
196 joinContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700197 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
198 setSendToVoicemail(intent);
199 } else if (ACTION_SET_RINGTONE.equals(action)) {
200 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700201 }
202 }
203
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800204 /**
205 * Creates an intent that can be sent to this service to create a new raw contact
206 * using data presented as a set of ContentValues.
207 */
208 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700209 ArrayList<ContentValues> values, AccountWithDataSet account,
210 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800211 Intent serviceIntent = new Intent(
212 context, ContactSaveService.class);
213 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
214 if (account != null) {
215 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
216 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700217 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800218 }
219 serviceIntent.putParcelableArrayListExtra(
220 ContactSaveService.EXTRA_CONTENT_VALUES, values);
221
222 // Callback intent will be invoked by the service once the new contact is
223 // created. The service will put the URI of the new contact as "data" on
224 // the callback intent.
225 Intent callbackIntent = new Intent(context, callbackActivity);
226 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800227 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
228 return serviceIntent;
229 }
230
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700231 private void createRawContact(Intent intent) {
232 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
233 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700234 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700235 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
236 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
237
238 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
239 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
240 .withValue(RawContacts.ACCOUNT_NAME, accountName)
241 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700242 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700243 .build());
244
245 int size = valueList.size();
246 for (int i = 0; i < size; i++) {
247 ContentValues values = valueList.get(i);
248 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
249 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
250 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
251 .withValues(values)
252 .build());
253 }
254
255 ContentResolver resolver = getContentResolver();
256 ContentProviderResult[] results;
257 try {
258 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
259 } catch (Exception e) {
260 throw new RuntimeException("Failed to store new contact", e);
261 }
262
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700263 Uri rawContactUri = results[0].uri;
264 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
265
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800266 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700267 }
268
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700269 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800270 * Creates an intent that can be sent to this service to create a new raw contact
271 * using data presented as a set of ContentValues.
272 */
273 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700274 String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800275 String callbackAction) {
276 Intent serviceIntent = new Intent(
277 context, ContactSaveService.class);
278 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
279 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700280 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800281
282 // Callback intent will be invoked by the service once the contact is
283 // saved. The service will put the URI of the new contact as "data" on
284 // the callback intent.
285 Intent callbackIntent = new Intent(context, callbackActivity);
286 callbackIntent.putExtra(saveModeExtraKey, saveMode);
287 callbackIntent.setAction(callbackAction);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800288 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
289 return serviceIntent;
290 }
291
292 private void saveContact(Intent intent) {
293 EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
294 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700295 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800296
297 // Trim any empty fields, and RawContacts, before persisting
298 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
299 EntityModifier.trimEmpty(state, accountTypes);
300
301 Uri lookupUri = null;
302
303 final ContentResolver resolver = getContentResolver();
304
305 // Attempt to persist changes
306 int tries = 0;
307 while (tries++ < PERSIST_TRIES) {
308 try {
309 // Build operations and try applying
310 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700311 if (DEBUG) {
312 Log.v(TAG, "Content Provider Operations:");
313 for (ContentProviderOperation operation : diff) {
314 Log.v(TAG, operation.toString());
315 }
316 }
317
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800318 ContentProviderResult[] results = null;
319 if (!diff.isEmpty()) {
320 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
321 }
322
323 final long rawContactId = getRawContactId(state, diff, results);
324 if (rawContactId == -1) {
325 throw new IllegalStateException("Could not determine RawContact ID after save");
326 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700327 if (isProfile) {
328 // Since the profile supports local raw contacts, which may have been completely
329 // removed if all information was removed, we need to do a special query to
330 // get the lookup URI for the profile contact (if it still exists).
331 Cursor c = resolver.query(Profile.CONTENT_URI,
332 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
333 null, null, null);
334 try {
Erik162b7e32011-09-20 15:23:55 -0700335 if (c.moveToFirst()) {
336 final long contactId = c.getLong(0);
337 final String lookupKey = c.getString(1);
338 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
339 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700340 } finally {
341 c.close();
342 }
343 } else {
344 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
345 rawContactId);
346 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
347 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800348 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
Dave Santoro36d24d72011-09-25 17:08:10 -0700349 // Mark the intent to indicate that the save was successful (even if the lookup URI
350 // is now null). For local contacts or the local profile, it's possible that the
351 // save triggered removal of the contact, so no lookup URI would exist..
352 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800353 break;
354
355 } catch (RemoteException e) {
356 // Something went wrong, bail without success
357 Log.e(TAG, "Problem persisting user edits", e);
358 break;
359
360 } catch (OperationApplicationException e) {
361 // Version consistency failed, re-parent change and try again
362 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
363 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
364 boolean first = true;
365 final int count = state.size();
366 for (int i = 0; i < count; i++) {
367 Long rawContactId = state.getRawContactId(i);
368 if (rawContactId != null && rawContactId != -1) {
369 if (!first) {
370 sb.append(',');
371 }
372 sb.append(rawContactId);
373 first = false;
374 }
375 }
376 sb.append(")");
377
378 if (first) {
379 throw new IllegalStateException("Version consistency failed for a new contact");
380 }
381
Dave Santoroc90f95e2011-09-07 17:47:15 -0700382 final EntityDeltaList newState = EntityDeltaList.fromQuery(
383 isProfile
384 ? RawContactsEntity.PROFILE_CONTENT_URI
385 : RawContactsEntity.CONTENT_URI,
386 resolver, sb.toString(), null, null);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800387 state = EntityDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700388
389 // Update the new state to use profile URIs if appropriate.
390 if (isProfile) {
391 for (EntityDelta delta : state) {
392 delta.setProfileQueryUri();
393 }
394 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800395 }
396 }
397
398 callbackIntent.setData(lookupUri);
399
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800400 deliverCallback(callbackIntent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800401 }
402
403 private long getRawContactId(EntityDeltaList state,
404 final ArrayList<ContentProviderOperation> diff,
405 final ContentProviderResult[] results) {
406 long rawContactId = state.findRawContactId();
407 if (rawContactId != -1) {
408 return rawContactId;
409 }
410
411 final int diffSize = diff.size();
412 for (int i = 0; i < diffSize; i++) {
413 ContentProviderOperation operation = diff.get(i);
414 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
415 && operation.getUri().getEncodedPath().contains(
416 RawContacts.CONTENT_URI.getEncodedPath())) {
417 return ContentUris.parseId(results[i].uri);
418 }
419 }
420 return -1;
421 }
422
423 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700424 * Creates an intent that can be sent to this service to create a new group as
425 * well as add new members at the same time.
426 *
427 * @param context of the application
428 * @param account in which the group should be created
429 * @param label is the name of the group (cannot be null)
430 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
431 * should be added to the group
432 * @param callbackActivity is the activity to send the callback intent to
433 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700434 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700435 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Katherine Kuan717e3432011-07-13 17:03:24 -0700436 String label, long[] rawContactsToAdd, Class<?> callbackActivity,
437 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800438 Intent serviceIntent = new Intent(context, ContactSaveService.class);
439 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
440 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
441 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700442 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800443 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700444 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700445
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800446 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700447 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800448 Intent callbackIntent = new Intent(context, callbackActivity);
449 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700450 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800451
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700452 return serviceIntent;
453 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800454
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800455 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700456 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
457 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
458 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
459 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700460 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800461
462 ContentValues values = new ContentValues();
463 values.put(Groups.ACCOUNT_TYPE, accountType);
464 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700465 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800466 values.put(Groups.TITLE, label);
467
Katherine Kuan717e3432011-07-13 17:03:24 -0700468 final ContentResolver resolver = getContentResolver();
469
470 // Create the new group
471 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
472
473 // If there's no URI, then the insertion failed. Abort early because group members can't be
474 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800475 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700476 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800477 return;
478 }
479
Katherine Kuan717e3432011-07-13 17:03:24 -0700480 // Add new group members
481 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
482
483 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
484 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800485 values.clear();
486 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
487 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
488
489 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700490 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700491 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800492 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800493 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800494 }
495
496 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800497 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800498 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700499 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
500 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800501 Intent serviceIntent = new Intent(context, ContactSaveService.class);
502 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
503 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
504 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700505
506 // Callback intent will be invoked by the service once the group is renamed.
507 Intent callbackIntent = new Intent(context, callbackActivity);
508 callbackIntent.setAction(callbackAction);
509 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
510
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800511 return serviceIntent;
512 }
513
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800514 private void renameGroup(Intent intent) {
515 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
516 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
517
518 if (groupId == -1) {
519 Log.e(TAG, "Invalid arguments for renameGroup request");
520 return;
521 }
522
523 ContentValues values = new ContentValues();
524 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700525 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
526 getContentResolver().update(groupUri, values, null, null);
527
528 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
529 callbackIntent.setData(groupUri);
530 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800531 }
532
533 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800534 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800535 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800536 public static Intent createGroupDeletionIntent(Context context, long groupId) {
537 Intent serviceIntent = new Intent(context, ContactSaveService.class);
538 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800539 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800540 return serviceIntent;
541 }
542
543 private void deleteGroup(Intent intent) {
544 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
545 if (groupId == -1) {
546 Log.e(TAG, "Invalid arguments for deleteGroup request");
547 return;
548 }
549
550 getContentResolver().delete(
551 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
552 }
553
554 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700555 * Creates an intent that can be sent to this service to rename a group as
556 * well as add and remove members from the group.
557 *
558 * @param context of the application
559 * @param groupId of the group that should be modified
560 * @param newLabel is the updated name of the group (can be null if the name
561 * should not be updated)
562 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
563 * should be added to the group
564 * @param rawContactsToRemove is an array of raw contact IDs for contacts
565 * that should be removed from the group
566 * @param callbackActivity is the activity to send the callback intent to
567 * @param callbackAction is the intent action for the callback intent
568 */
569 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
570 long[] rawContactsToAdd, long[] rawContactsToRemove,
571 Class<?> callbackActivity, String callbackAction) {
572 Intent serviceIntent = new Intent(context, ContactSaveService.class);
573 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
574 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
575 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
576 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
577 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
578 rawContactsToRemove);
579
580 // Callback intent will be invoked by the service once the group is updated
581 Intent callbackIntent = new Intent(context, callbackActivity);
582 callbackIntent.setAction(callbackAction);
583 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
584
585 return serviceIntent;
586 }
587
588 private void updateGroup(Intent intent) {
589 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
590 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
591 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
592 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
593
594 if (groupId == -1) {
595 Log.e(TAG, "Invalid arguments for updateGroup request");
596 return;
597 }
598
599 final ContentResolver resolver = getContentResolver();
600 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
601
602 // Update group name if necessary
603 if (label != null) {
604 ContentValues values = new ContentValues();
605 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700606 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700607 }
608
Katherine Kuan717e3432011-07-13 17:03:24 -0700609 // Add and remove members if necessary
610 addMembersToGroup(resolver, rawContactsToAdd, groupId);
611 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
612
613 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
614 callbackIntent.setData(groupUri);
615 deliverCallback(callbackIntent);
616 }
617
618 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
619 long groupId) {
620 if (rawContactsToAdd == null) {
621 return;
622 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700623 for (long rawContactId : rawContactsToAdd) {
624 try {
625 final ArrayList<ContentProviderOperation> rawContactOperations =
626 new ArrayList<ContentProviderOperation>();
627
628 // Build an assert operation to ensure the contact is not already in the group
629 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
630 .newAssertQuery(Data.CONTENT_URI);
631 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
632 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
633 new String[] { String.valueOf(rawContactId),
634 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
635 assertBuilder.withExpectedCount(0);
636 rawContactOperations.add(assertBuilder.build());
637
638 // Build an insert operation to add the contact to the group
639 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
640 .newInsert(Data.CONTENT_URI);
641 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
642 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
643 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
644 rawContactOperations.add(insertBuilder.build());
645
646 if (DEBUG) {
647 for (ContentProviderOperation operation : rawContactOperations) {
648 Log.v(TAG, operation.toString());
649 }
650 }
651
652 // Apply batch
653 ContentProviderResult[] results = null;
654 if (!rawContactOperations.isEmpty()) {
655 results = resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
656 }
657 } catch (RemoteException e) {
658 // Something went wrong, bail without success
659 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
660 String.valueOf(rawContactId), e);
661 } catch (OperationApplicationException e) {
662 // The assert could have failed because the contact is already in the group,
663 // just continue to the next contact
664 Log.w(TAG, "Assert failed in adding raw contact ID " +
665 String.valueOf(rawContactId) + ". Already exists in group " +
666 String.valueOf(groupId), e);
667 }
668 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700669 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700670
Katherine Kuan717e3432011-07-13 17:03:24 -0700671 private void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
672 long groupId) {
673 if (rawContactsToRemove == null) {
674 return;
675 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700676 for (long rawContactId : rawContactsToRemove) {
677 // Apply the delete operation on the data row for the given raw contact's
678 // membership in the given group. If no contact matches the provided selection, then
679 // nothing will be done. Just continue to the next contact.
680 getContentResolver().delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
681 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
682 new String[] { String.valueOf(rawContactId),
683 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
684 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700685 }
686
687 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800688 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800689 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800690 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
691 Intent serviceIntent = new Intent(context, ContactSaveService.class);
692 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
693 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
694 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
695
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800696 return serviceIntent;
697 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800698
699 private void setStarred(Intent intent) {
700 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
701 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
702 if (contactUri == null) {
703 Log.e(TAG, "Invalid arguments for setStarred request");
704 return;
705 }
706
707 final ContentValues values = new ContentValues(1);
708 values.put(Contacts.STARRED, value);
709 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800710 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800711
712 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700713 * Creates an intent that can be sent to this service to set the redirect to voicemail.
714 */
715 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
716 boolean value) {
717 Intent serviceIntent = new Intent(context, ContactSaveService.class);
718 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
719 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
720 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
721
722 return serviceIntent;
723 }
724
725 private void setSendToVoicemail(Intent intent) {
726 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
727 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
728 if (contactUri == null) {
729 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
730 return;
731 }
732
733 final ContentValues values = new ContentValues(1);
734 values.put(Contacts.SEND_TO_VOICEMAIL, value);
735 getContentResolver().update(contactUri, values, null, null);
736 }
737
738 /**
739 * Creates an intent that can be sent to this service to save the contact's ringtone.
740 */
741 public static Intent createSetRingtone(Context context, Uri contactUri,
742 String value) {
743 Intent serviceIntent = new Intent(context, ContactSaveService.class);
744 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
745 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
746 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
747
748 return serviceIntent;
749 }
750
751 private void setRingtone(Intent intent) {
752 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
753 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
754 if (contactUri == null) {
755 Log.e(TAG, "Invalid arguments for setRingtone");
756 return;
757 }
758 ContentValues values = new ContentValues(1);
759 values.put(Contacts.CUSTOM_RINGTONE, value);
760 getContentResolver().update(contactUri, values, null, null);
761 }
762
763 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800764 * Creates an intent that sets the selected data item as super primary (default)
765 */
766 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
767 Intent serviceIntent = new Intent(context, ContactSaveService.class);
768 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
769 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
770 return serviceIntent;
771 }
772
773 private void setSuperPrimary(Intent intent) {
774 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
775 if (dataId == -1) {
776 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
777 return;
778 }
779
780 // Update the primary values in the data record.
781 ContentValues values = new ContentValues(1);
782 values.put(Data.IS_SUPER_PRIMARY, 1);
783 values.put(Data.IS_PRIMARY, 1);
784
785 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
786 values, null, null);
787 }
788
789 /**
790 * Creates an intent that clears the primary flag of all data items that belong to the same
791 * raw_contact as the given data item. Will only clear, if the data item was primary before
792 * this call
793 */
794 public static Intent createClearPrimaryIntent(Context context, long dataId) {
795 Intent serviceIntent = new Intent(context, ContactSaveService.class);
796 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
797 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
798 return serviceIntent;
799 }
800
801 private void clearPrimary(Intent intent) {
802 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
803 if (dataId == -1) {
804 Log.e(TAG, "Invalid arguments for clearPrimary request");
805 return;
806 }
807
808 // Update the primary values in the data record.
809 ContentValues values = new ContentValues(1);
810 values.put(Data.IS_SUPER_PRIMARY, 0);
811 values.put(Data.IS_PRIMARY, 0);
812
813 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
814 values, null, null);
815 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800816
817 /**
818 * Creates an intent that can be sent to this service to delete a contact.
819 */
820 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
821 Intent serviceIntent = new Intent(context, ContactSaveService.class);
822 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
823 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
824 return serviceIntent;
825 }
826
827 private void deleteContact(Intent intent) {
828 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
829 if (contactUri == null) {
830 Log.e(TAG, "Invalid arguments for deleteContact request");
831 return;
832 }
833
834 getContentResolver().delete(contactUri, null, null);
835 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800836
837 /**
838 * Creates an intent that can be sent to this service to join two contacts.
839 */
840 public static Intent createJoinContactsIntent(Context context, long contactId1,
841 long contactId2, boolean contactWritable,
842 Class<?> callbackActivity, String callbackAction) {
843 Intent serviceIntent = new Intent(context, ContactSaveService.class);
844 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
845 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
846 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
847 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
848
849 // Callback intent will be invoked by the service once the contacts are joined.
850 Intent callbackIntent = new Intent(context, callbackActivity);
851 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800852 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
853
854 return serviceIntent;
855 }
856
857
858 private interface JoinContactQuery {
859 String[] PROJECTION = {
860 RawContacts._ID,
861 RawContacts.CONTACT_ID,
862 RawContacts.NAME_VERIFIED,
863 RawContacts.DISPLAY_NAME_SOURCE,
864 };
865
866 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
867
868 int _ID = 0;
869 int CONTACT_ID = 1;
870 int NAME_VERIFIED = 2;
871 int DISPLAY_NAME_SOURCE = 3;
872 }
873
874 private void joinContacts(Intent intent) {
875 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
876 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
877 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
878 if (contactId1 == -1 || contactId2 == -1) {
879 Log.e(TAG, "Invalid arguments for joinContacts request");
880 return;
881 }
882
883 final ContentResolver resolver = getContentResolver();
884
885 // Load raw contact IDs for all raw contacts involved - currently edited and selected
886 // in the join UIs
887 Cursor c = resolver.query(RawContacts.CONTENT_URI,
888 JoinContactQuery.PROJECTION,
889 JoinContactQuery.SELECTION,
890 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
891
892 long rawContactIds[];
893 long verifiedNameRawContactId = -1;
894 try {
895 int maxDisplayNameSource = -1;
896 rawContactIds = new long[c.getCount()];
897 for (int i = 0; i < rawContactIds.length; i++) {
898 c.moveToPosition(i);
899 long rawContactId = c.getLong(JoinContactQuery._ID);
900 rawContactIds[i] = rawContactId;
901 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
902 if (nameSource > maxDisplayNameSource) {
903 maxDisplayNameSource = nameSource;
904 }
905 }
906
907 // Find an appropriate display name for the joined contact:
908 // if should have a higher DisplayNameSource or be the name
909 // of the original contact that we are joining with another.
910 if (writable) {
911 for (int i = 0; i < rawContactIds.length; i++) {
912 c.moveToPosition(i);
913 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
914 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
915 if (nameSource == maxDisplayNameSource
916 && (verifiedNameRawContactId == -1
917 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
918 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
919 }
920 }
921 }
922 }
923 } finally {
924 c.close();
925 }
926
927 // For each pair of raw contacts, insert an aggregation exception
928 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
929 for (int i = 0; i < rawContactIds.length; i++) {
930 for (int j = 0; j < rawContactIds.length; j++) {
931 if (i != j) {
932 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
933 }
934 }
935 }
936
937 // Mark the original contact as "name verified" to make sure that the contact
938 // display name does not change as a result of the join
939 if (verifiedNameRawContactId != -1) {
940 Builder builder = ContentProviderOperation.newUpdate(
941 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
942 builder.withValue(RawContacts.NAME_VERIFIED, 1);
943 operations.add(builder.build());
944 }
945
946 boolean success = false;
947 // Apply all aggregation exceptions as one batch
948 try {
949 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800950 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800951 success = true;
952 } catch (RemoteException e) {
953 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800954 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800955 } catch (OperationApplicationException e) {
956 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800957 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800958 }
959
960 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
961 if (success) {
962 Uri uri = RawContacts.getContactLookupUri(resolver,
963 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
964 callbackIntent.setData(uri);
965 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800966 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800967 }
968
969 /**
970 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
971 */
972 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
973 long rawContactId1, long rawContactId2) {
974 Builder builder =
975 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
976 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
977 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
978 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
979 operations.add(builder.build());
980 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800981
982 /**
983 * Shows a toast on the UI thread.
984 */
985 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800986 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800987
988 @Override
989 public void run() {
990 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
991 }
992 });
993 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800994
995 private void deliverCallback(final Intent callbackIntent) {
996 mMainHandler.post(new Runnable() {
997
998 @Override
999 public void run() {
1000 deliverCallbackOnUiThread(callbackIntent);
1001 }
1002 });
1003 }
1004
1005 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1006 // TODO: this assumes that if there are multiple instances of the same
1007 // activity registered, the last one registered is the one waiting for
1008 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001009 for (Listener listener : sListeners) {
1010 if (callbackIntent.getComponent().equals(
1011 ((Activity) listener).getIntent().getComponent())) {
1012 listener.onServiceCompleted(callbackIntent);
1013 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001014 }
1015 }
1016 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001017}