blob: d8dbdbc8d8dc87e6208e28d88277c481c00ffc97 [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;
20import com.android.contacts.model.EntityDeltaList;
21import com.android.contacts.model.EntityModifier;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080022import com.google.android.collect.Lists;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070023import com.google.android.collect.Sets;
24
25import android.accounts.Account;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080026import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070027import android.app.IntentService;
28import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080029import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070030import android.content.ContentProviderResult;
31import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080032import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070033import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080034import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070035import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080036import android.content.OperationApplicationException;
37import android.database.Cursor;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070038import android.net.Uri;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080039import android.os.Handler;
40import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080041import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080042import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070043import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080044import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080045import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080046import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070047import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080048import android.provider.ContactsContract.Groups;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070049import android.provider.ContactsContract.RawContacts;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070050import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080051import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070052
53import java.util.ArrayList;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070054import java.util.HashSet;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080055import java.util.LinkedList;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070056import java.util.List;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070057
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080058/**
59 * A service responsible for saving changes to the content provider.
60 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070061public class ContactSaveService extends IntentService {
62 private static final String TAG = "ContactSaveService";
63
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070064 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
65
66 public static final String EXTRA_ACCOUNT_NAME = "accountName";
67 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
68 public static final String EXTRA_CONTENT_VALUES = "contentValues";
69 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
70
Dmitri Plotnikova0114142011-02-15 13:53:21 -080071 public static final String ACTION_SAVE_CONTACT = "saveContact";
72 public static final String EXTRA_CONTACT_STATE = "state";
73 public static final String EXTRA_SAVE_MODE = "saveMode";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070074
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080075 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080076 public static final String ACTION_RENAME_GROUP = "renameGroup";
77 public static final String ACTION_DELETE_GROUP = "deleteGroup";
78 public static final String EXTRA_GROUP_ID = "groupId";
79 public static final String EXTRA_GROUP_LABEL = "groupLabel";
80
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080081 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -080082 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080083 public static final String EXTRA_CONTACT_URI = "contactUri";
84 public static final String EXTRA_STARRED_FLAG = "starred";
85
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -080086 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
87 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
88 public static final String EXTRA_DATA_ID = "dataId";
89
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080090 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
91 public static final String EXTRA_CONTACT_ID1 = "contactId1";
92 public static final String EXTRA_CONTACT_ID2 = "contactId2";
93 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
94
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070095 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
96 Data.MIMETYPE,
97 Data.IS_PRIMARY,
98 Data.DATA1,
99 Data.DATA2,
100 Data.DATA3,
101 Data.DATA4,
102 Data.DATA5,
103 Data.DATA6,
104 Data.DATA7,
105 Data.DATA8,
106 Data.DATA9,
107 Data.DATA10,
108 Data.DATA11,
109 Data.DATA12,
110 Data.DATA13,
111 Data.DATA14,
112 Data.DATA15
113 );
114
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800115 private static final int PERSIST_TRIES = 3;
116
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800117 public interface Listener {
118 public void onServiceCompleted(Intent callbackIntent);
119 }
120
121 private static final LinkedList<Listener> sListeners = new LinkedList<Listener>();
122
123 private Handler mMainHandler;
124
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700125 public ContactSaveService() {
126 super(TAG);
127 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800128 mMainHandler = new Handler(Looper.getMainLooper());
129 }
130
131 public static void registerListener(Listener listener) {
132 if (!(listener instanceof Activity)) {
133 throw new ClassCastException("Only activities can be registered to"
134 + " receive callback from " + ContactSaveService.class.getName());
135 }
136 synchronized (sListeners) {
137 sListeners.addFirst(listener);
138 }
139 }
140
141 public static void unregisterListener(Listener listener) {
142 synchronized (sListeners) {
143 sListeners.remove(listener);
144 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700145 }
146
147 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800148 public Object getSystemService(String name) {
149 Object service = super.getSystemService(name);
150 if (service != null) {
151 return service;
152 }
153
154 return getApplicationContext().getSystemService(name);
155 }
156
157 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700158 protected void onHandleIntent(Intent intent) {
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700159 String action = intent.getAction();
160 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
161 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800162 } else if (ACTION_SAVE_CONTACT.equals(action)) {
163 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800164 } else if (ACTION_CREATE_GROUP.equals(action)) {
165 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800166 } else if (ACTION_RENAME_GROUP.equals(action)) {
167 renameGroup(intent);
168 } else if (ACTION_DELETE_GROUP.equals(action)) {
169 deleteGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800170 } else if (ACTION_SET_STARRED.equals(action)) {
171 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800172 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
173 setSuperPrimary(intent);
174 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
175 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800176 } else if (ACTION_DELETE_CONTACT.equals(action)) {
177 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800178 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
179 joinContacts(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700180 }
181 }
182
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800183 /**
184 * Creates an intent that can be sent to this service to create a new raw contact
185 * using data presented as a set of ContentValues.
186 */
187 public static Intent createNewRawContactIntent(Context context,
188 ArrayList<ContentValues> values, Account account, Class<?> callbackActivity,
189 String callbackAction) {
190 Intent serviceIntent = new Intent(
191 context, ContactSaveService.class);
192 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
193 if (account != null) {
194 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
195 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
196 }
197 serviceIntent.putParcelableArrayListExtra(
198 ContactSaveService.EXTRA_CONTENT_VALUES, values);
199
200 // Callback intent will be invoked by the service once the new contact is
201 // created. The service will put the URI of the new contact as "data" on
202 // the callback intent.
203 Intent callbackIntent = new Intent(context, callbackActivity);
204 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800205 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
206 return serviceIntent;
207 }
208
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700209 private void createRawContact(Intent intent) {
210 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
211 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
212 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
213 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
214
215 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
216 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
217 .withValue(RawContacts.ACCOUNT_NAME, accountName)
218 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
219 .build());
220
221 int size = valueList.size();
222 for (int i = 0; i < size; i++) {
223 ContentValues values = valueList.get(i);
224 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
225 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
226 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
227 .withValues(values)
228 .build());
229 }
230
231 ContentResolver resolver = getContentResolver();
232 ContentProviderResult[] results;
233 try {
234 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
235 } catch (Exception e) {
236 throw new RuntimeException("Failed to store new contact", e);
237 }
238
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700239 Uri rawContactUri = results[0].uri;
240 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
241
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800242 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700243 }
244
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700245 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800246 * Creates an intent that can be sent to this service to create a new raw contact
247 * using data presented as a set of ContentValues.
248 */
249 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
250 String saveModeExtraKey, int saveMode, Class<?> callbackActivity,
251 String callbackAction) {
252 Intent serviceIntent = new Intent(
253 context, ContactSaveService.class);
254 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
255 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
256
257 // Callback intent will be invoked by the service once the contact is
258 // saved. The service will put the URI of the new contact as "data" on
259 // the callback intent.
260 Intent callbackIntent = new Intent(context, callbackActivity);
261 callbackIntent.putExtra(saveModeExtraKey, saveMode);
262 callbackIntent.setAction(callbackAction);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800263 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
264 return serviceIntent;
265 }
266
267 private void saveContact(Intent intent) {
268 EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
269 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
270
271 // Trim any empty fields, and RawContacts, before persisting
272 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
273 EntityModifier.trimEmpty(state, accountTypes);
274
275 Uri lookupUri = null;
276
277 final ContentResolver resolver = getContentResolver();
278
279 // Attempt to persist changes
280 int tries = 0;
281 while (tries++ < PERSIST_TRIES) {
282 try {
283 // Build operations and try applying
284 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
285 ContentProviderResult[] results = null;
286 if (!diff.isEmpty()) {
287 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
288 }
289
290 final long rawContactId = getRawContactId(state, diff, results);
291 if (rawContactId == -1) {
292 throw new IllegalStateException("Could not determine RawContact ID after save");
293 }
294 final Uri rawContactUri = ContentUris.withAppendedId(
295 RawContacts.CONTENT_URI, rawContactId);
296 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
297 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
298 break;
299
300 } catch (RemoteException e) {
301 // Something went wrong, bail without success
302 Log.e(TAG, "Problem persisting user edits", e);
303 break;
304
305 } catch (OperationApplicationException e) {
306 // Version consistency failed, re-parent change and try again
307 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
308 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
309 boolean first = true;
310 final int count = state.size();
311 for (int i = 0; i < count; i++) {
312 Long rawContactId = state.getRawContactId(i);
313 if (rawContactId != null && rawContactId != -1) {
314 if (!first) {
315 sb.append(',');
316 }
317 sb.append(rawContactId);
318 first = false;
319 }
320 }
321 sb.append(")");
322
323 if (first) {
324 throw new IllegalStateException("Version consistency failed for a new contact");
325 }
326
327 final EntityDeltaList newState = EntityDeltaList.fromQuery(resolver,
328 sb.toString(), null, null);
329 state = EntityDeltaList.mergeAfter(newState, state);
330 }
331 }
332
333 callbackIntent.setData(lookupUri);
334
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800335 deliverCallback(callbackIntent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800336 }
337
338 private long getRawContactId(EntityDeltaList state,
339 final ArrayList<ContentProviderOperation> diff,
340 final ContentProviderResult[] results) {
341 long rawContactId = state.findRawContactId();
342 if (rawContactId != -1) {
343 return rawContactId;
344 }
345
346 final int diffSize = diff.size();
347 for (int i = 0; i < diffSize; i++) {
348 ContentProviderOperation operation = diff.get(i);
349 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
350 && operation.getUri().getEncodedPath().contains(
351 RawContacts.CONTENT_URI.getEncodedPath())) {
352 return ContentUris.parseId(results[i].uri);
353 }
354 }
355 return -1;
356 }
357
358 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800359 * Creates an intent that can be sent to this service to create a new group.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700360 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800361 public static Intent createNewGroupIntent(Context context, Account account, String label,
362 Class<?> callbackActivity, String callbackAction) {
363 Intent serviceIntent = new Intent(context, ContactSaveService.class);
364 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
365 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
366 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
367 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700368
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800369 // Callback intent will be invoked by the service once the new group is
370 // created. The service will put a group membership row in the extras
371 // of the callback intent.
372 Intent callbackIntent = new Intent(context, callbackActivity);
373 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700374 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800375
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700376 return serviceIntent;
377 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800378
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800379 private void createGroup(Intent intent) {
380 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
381 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
382 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
383
384 ContentValues values = new ContentValues();
385 values.put(Groups.ACCOUNT_TYPE, accountType);
386 values.put(Groups.ACCOUNT_NAME, accountName);
387 values.put(Groups.TITLE, label);
388
389 Uri groupUri = getContentResolver().insert(Groups.CONTENT_URI, values);
390 if (groupUri == null) {
391 return;
392 }
393
394 values.clear();
395 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
396 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
397
398 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700399 callbackIntent.setData(groupUri);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800400 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
401
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800402 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800403 }
404
405 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800406 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800407 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700408 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
409 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800410 Intent serviceIntent = new Intent(context, ContactSaveService.class);
411 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
412 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
413 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700414
415 // Callback intent will be invoked by the service once the group is renamed.
416 Intent callbackIntent = new Intent(context, callbackActivity);
417 callbackIntent.setAction(callbackAction);
418 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
419
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800420 return serviceIntent;
421 }
422
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800423 private void renameGroup(Intent intent) {
424 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
425 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
426
427 if (groupId == -1) {
428 Log.e(TAG, "Invalid arguments for renameGroup request");
429 return;
430 }
431
432 ContentValues values = new ContentValues();
433 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700434 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
435 getContentResolver().update(groupUri, values, null, null);
436
437 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
438 callbackIntent.setData(groupUri);
439 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800440 }
441
442 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800443 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800444 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800445 public static Intent createGroupDeletionIntent(Context context, long groupId) {
446 Intent serviceIntent = new Intent(context, ContactSaveService.class);
447 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800448 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800449 return serviceIntent;
450 }
451
452 private void deleteGroup(Intent intent) {
453 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
454 if (groupId == -1) {
455 Log.e(TAG, "Invalid arguments for deleteGroup request");
456 return;
457 }
458
459 getContentResolver().delete(
460 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
461 }
462
463 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800464 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800465 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800466 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
467 Intent serviceIntent = new Intent(context, ContactSaveService.class);
468 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
469 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
470 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
471
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800472 return serviceIntent;
473 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800474
475 private void setStarred(Intent intent) {
476 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
477 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
478 if (contactUri == null) {
479 Log.e(TAG, "Invalid arguments for setStarred request");
480 return;
481 }
482
483 final ContentValues values = new ContentValues(1);
484 values.put(Contacts.STARRED, value);
485 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800486 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800487
488 /**
489 * Creates an intent that sets the selected data item as super primary (default)
490 */
491 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
492 Intent serviceIntent = new Intent(context, ContactSaveService.class);
493 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
494 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
495 return serviceIntent;
496 }
497
498 private void setSuperPrimary(Intent intent) {
499 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
500 if (dataId == -1) {
501 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
502 return;
503 }
504
505 // Update the primary values in the data record.
506 ContentValues values = new ContentValues(1);
507 values.put(Data.IS_SUPER_PRIMARY, 1);
508 values.put(Data.IS_PRIMARY, 1);
509
510 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
511 values, null, null);
512 }
513
514 /**
515 * Creates an intent that clears the primary flag of all data items that belong to the same
516 * raw_contact as the given data item. Will only clear, if the data item was primary before
517 * this call
518 */
519 public static Intent createClearPrimaryIntent(Context context, long dataId) {
520 Intent serviceIntent = new Intent(context, ContactSaveService.class);
521 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
522 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
523 return serviceIntent;
524 }
525
526 private void clearPrimary(Intent intent) {
527 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
528 if (dataId == -1) {
529 Log.e(TAG, "Invalid arguments for clearPrimary request");
530 return;
531 }
532
533 // Update the primary values in the data record.
534 ContentValues values = new ContentValues(1);
535 values.put(Data.IS_SUPER_PRIMARY, 0);
536 values.put(Data.IS_PRIMARY, 0);
537
538 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
539 values, null, null);
540 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800541
542 /**
543 * Creates an intent that can be sent to this service to delete a contact.
544 */
545 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
546 Intent serviceIntent = new Intent(context, ContactSaveService.class);
547 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
548 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
549 return serviceIntent;
550 }
551
552 private void deleteContact(Intent intent) {
553 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
554 if (contactUri == null) {
555 Log.e(TAG, "Invalid arguments for deleteContact request");
556 return;
557 }
558
559 getContentResolver().delete(contactUri, null, null);
560 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800561
562 /**
563 * Creates an intent that can be sent to this service to join two contacts.
564 */
565 public static Intent createJoinContactsIntent(Context context, long contactId1,
566 long contactId2, boolean contactWritable,
567 Class<?> callbackActivity, String callbackAction) {
568 Intent serviceIntent = new Intent(context, ContactSaveService.class);
569 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
570 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
571 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
572 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
573
574 // Callback intent will be invoked by the service once the contacts are joined.
575 Intent callbackIntent = new Intent(context, callbackActivity);
576 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800577 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
578
579 return serviceIntent;
580 }
581
582
583 private interface JoinContactQuery {
584 String[] PROJECTION = {
585 RawContacts._ID,
586 RawContacts.CONTACT_ID,
587 RawContacts.NAME_VERIFIED,
588 RawContacts.DISPLAY_NAME_SOURCE,
589 };
590
591 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
592
593 int _ID = 0;
594 int CONTACT_ID = 1;
595 int NAME_VERIFIED = 2;
596 int DISPLAY_NAME_SOURCE = 3;
597 }
598
599 private void joinContacts(Intent intent) {
600 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
601 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
602 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
603 if (contactId1 == -1 || contactId2 == -1) {
604 Log.e(TAG, "Invalid arguments for joinContacts request");
605 return;
606 }
607
608 final ContentResolver resolver = getContentResolver();
609
610 // Load raw contact IDs for all raw contacts involved - currently edited and selected
611 // in the join UIs
612 Cursor c = resolver.query(RawContacts.CONTENT_URI,
613 JoinContactQuery.PROJECTION,
614 JoinContactQuery.SELECTION,
615 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
616
617 long rawContactIds[];
618 long verifiedNameRawContactId = -1;
619 try {
620 int maxDisplayNameSource = -1;
621 rawContactIds = new long[c.getCount()];
622 for (int i = 0; i < rawContactIds.length; i++) {
623 c.moveToPosition(i);
624 long rawContactId = c.getLong(JoinContactQuery._ID);
625 rawContactIds[i] = rawContactId;
626 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
627 if (nameSource > maxDisplayNameSource) {
628 maxDisplayNameSource = nameSource;
629 }
630 }
631
632 // Find an appropriate display name for the joined contact:
633 // if should have a higher DisplayNameSource or be the name
634 // of the original contact that we are joining with another.
635 if (writable) {
636 for (int i = 0; i < rawContactIds.length; i++) {
637 c.moveToPosition(i);
638 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
639 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
640 if (nameSource == maxDisplayNameSource
641 && (verifiedNameRawContactId == -1
642 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
643 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
644 }
645 }
646 }
647 }
648 } finally {
649 c.close();
650 }
651
652 // For each pair of raw contacts, insert an aggregation exception
653 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
654 for (int i = 0; i < rawContactIds.length; i++) {
655 for (int j = 0; j < rawContactIds.length; j++) {
656 if (i != j) {
657 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
658 }
659 }
660 }
661
662 // Mark the original contact as "name verified" to make sure that the contact
663 // display name does not change as a result of the join
664 if (verifiedNameRawContactId != -1) {
665 Builder builder = ContentProviderOperation.newUpdate(
666 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
667 builder.withValue(RawContacts.NAME_VERIFIED, 1);
668 operations.add(builder.build());
669 }
670
671 boolean success = false;
672 // Apply all aggregation exceptions as one batch
673 try {
674 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800675 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800676 success = true;
677 } catch (RemoteException e) {
678 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800679 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800680 } catch (OperationApplicationException e) {
681 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800682 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800683 }
684
685 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
686 if (success) {
687 Uri uri = RawContacts.getContactLookupUri(resolver,
688 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
689 callbackIntent.setData(uri);
690 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800691 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800692 }
693
694 /**
695 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
696 */
697 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
698 long rawContactId1, long rawContactId2) {
699 Builder builder =
700 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
701 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
702 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
703 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
704 operations.add(builder.build());
705 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800706
707 /**
708 * Shows a toast on the UI thread.
709 */
710 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800711 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800712
713 @Override
714 public void run() {
715 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
716 }
717 });
718 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800719
720 private void deliverCallback(final Intent callbackIntent) {
721 mMainHandler.post(new Runnable() {
722
723 @Override
724 public void run() {
725 deliverCallbackOnUiThread(callbackIntent);
726 }
727 });
728 }
729
730 void deliverCallbackOnUiThread(final Intent callbackIntent) {
731 // TODO: this assumes that if there are multiple instances of the same
732 // activity registered, the last one registered is the one waiting for
733 // the callback. Validity of this assumption needs to be verified.
734 synchronized (sListeners) {
735 for (Listener listener : sListeners) {
736 if (callbackIntent.getComponent().equals(
737 ((Activity) listener).getIntent().getComponent())) {
738 listener.onServiceCompleted(callbackIntent);
739 return;
740 }
741 }
742 }
743 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700744}