blob: f7ede904d80b1c26a6a24e3f732969e5de9d9a9f [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;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070026import android.app.IntentService;
27import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080028import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070029import android.content.ContentProviderResult;
30import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080031import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070032import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080033import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070034import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080035import android.content.OperationApplicationException;
36import android.database.Cursor;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070037import android.net.Uri;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080038import android.os.Handler;
39import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080040import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080041import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070042import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080043import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080044import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080045import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070046import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080047import android.provider.ContactsContract.Groups;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070048import android.provider.ContactsContract.RawContacts;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070049import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080050import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070051
52import java.util.ArrayList;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070053import java.util.HashSet;
54import java.util.List;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070055
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080056/**
57 * A service responsible for saving changes to the content provider.
58 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070059public class ContactSaveService extends IntentService {
60 private static final String TAG = "ContactSaveService";
61
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070062 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
63
64 public static final String EXTRA_ACCOUNT_NAME = "accountName";
65 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
66 public static final String EXTRA_CONTENT_VALUES = "contentValues";
67 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
68
Dmitri Plotnikova0114142011-02-15 13:53:21 -080069 public static final String ACTION_SAVE_CONTACT = "saveContact";
70 public static final String EXTRA_CONTACT_STATE = "state";
71 public static final String EXTRA_SAVE_MODE = "saveMode";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070072
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080073 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080074 public static final String ACTION_RENAME_GROUP = "renameGroup";
75 public static final String ACTION_DELETE_GROUP = "deleteGroup";
76 public static final String EXTRA_GROUP_ID = "groupId";
77 public static final String EXTRA_GROUP_LABEL = "groupLabel";
78
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080079 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -080080 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080081 public static final String EXTRA_CONTACT_URI = "contactUri";
82 public static final String EXTRA_STARRED_FLAG = "starred";
83
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -080084 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
85 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
86 public static final String EXTRA_DATA_ID = "dataId";
87
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080088 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
89 public static final String EXTRA_CONTACT_ID1 = "contactId1";
90 public static final String EXTRA_CONTACT_ID2 = "contactId2";
91 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
92
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070093 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
94 Data.MIMETYPE,
95 Data.IS_PRIMARY,
96 Data.DATA1,
97 Data.DATA2,
98 Data.DATA3,
99 Data.DATA4,
100 Data.DATA5,
101 Data.DATA6,
102 Data.DATA7,
103 Data.DATA8,
104 Data.DATA9,
105 Data.DATA10,
106 Data.DATA11,
107 Data.DATA12,
108 Data.DATA13,
109 Data.DATA14,
110 Data.DATA15
111 );
112
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800113 private static final int PERSIST_TRIES = 3;
114
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700115 public ContactSaveService() {
116 super(TAG);
117 setIntentRedelivery(true);
118 }
119
120 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800121 public Object getSystemService(String name) {
122 Object service = super.getSystemService(name);
123 if (service != null) {
124 return service;
125 }
126
127 return getApplicationContext().getSystemService(name);
128 }
129
130 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700131 protected void onHandleIntent(Intent intent) {
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700132 String action = intent.getAction();
133 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
134 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800135 } else if (ACTION_SAVE_CONTACT.equals(action)) {
136 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800137 } else if (ACTION_CREATE_GROUP.equals(action)) {
138 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800139 } else if (ACTION_RENAME_GROUP.equals(action)) {
140 renameGroup(intent);
141 } else if (ACTION_DELETE_GROUP.equals(action)) {
142 deleteGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800143 } else if (ACTION_SET_STARRED.equals(action)) {
144 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800145 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
146 setSuperPrimary(intent);
147 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
148 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800149 } else if (ACTION_DELETE_CONTACT.equals(action)) {
150 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800151 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
152 joinContacts(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700153 }
154 }
155
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800156 /**
157 * Creates an intent that can be sent to this service to create a new raw contact
158 * using data presented as a set of ContentValues.
159 */
160 public static Intent createNewRawContactIntent(Context context,
161 ArrayList<ContentValues> values, Account account, Class<?> callbackActivity,
162 String callbackAction) {
163 Intent serviceIntent = new Intent(
164 context, ContactSaveService.class);
165 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
166 if (account != null) {
167 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
168 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
169 }
170 serviceIntent.putParcelableArrayListExtra(
171 ContactSaveService.EXTRA_CONTENT_VALUES, values);
172
173 // Callback intent will be invoked by the service once the new contact is
174 // created. The service will put the URI of the new contact as "data" on
175 // the callback intent.
176 Intent callbackIntent = new Intent(context, callbackActivity);
177 callbackIntent.setAction(callbackAction);
178 callbackIntent.setFlags(
179 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
180 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
181 return serviceIntent;
182 }
183
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700184 private void createRawContact(Intent intent) {
185 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
186 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
187 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
188 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
189
190 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
191 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
192 .withValue(RawContacts.ACCOUNT_NAME, accountName)
193 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
194 .build());
195
196 int size = valueList.size();
197 for (int i = 0; i < size; i++) {
198 ContentValues values = valueList.get(i);
199 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
200 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
201 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
202 .withValues(values)
203 .build());
204 }
205
206 ContentResolver resolver = getContentResolver();
207 ContentProviderResult[] results;
208 try {
209 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
210 } catch (Exception e) {
211 throw new RuntimeException("Failed to store new contact", e);
212 }
213
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700214 Uri rawContactUri = results[0].uri;
215 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
216
217 startActivity(callbackIntent);
218 }
219
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700220 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800221 * Creates an intent that can be sent to this service to create a new raw contact
222 * using data presented as a set of ContentValues.
223 */
224 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
225 String saveModeExtraKey, int saveMode, Class<?> callbackActivity,
226 String callbackAction) {
227 Intent serviceIntent = new Intent(
228 context, ContactSaveService.class);
229 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
230 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
231
232 // Callback intent will be invoked by the service once the contact is
233 // saved. The service will put the URI of the new contact as "data" on
234 // the callback intent.
235 Intent callbackIntent = new Intent(context, callbackActivity);
236 callbackIntent.putExtra(saveModeExtraKey, saveMode);
237 callbackIntent.setAction(callbackAction);
238 callbackIntent.setFlags(
239 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
240 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
241 return serviceIntent;
242 }
243
244 private void saveContact(Intent intent) {
245 EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
246 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
247
248 // Trim any empty fields, and RawContacts, before persisting
249 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
250 EntityModifier.trimEmpty(state, accountTypes);
251
252 Uri lookupUri = null;
253
254 final ContentResolver resolver = getContentResolver();
255
256 // Attempt to persist changes
257 int tries = 0;
258 while (tries++ < PERSIST_TRIES) {
259 try {
260 // Build operations and try applying
261 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
262 ContentProviderResult[] results = null;
263 if (!diff.isEmpty()) {
264 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
265 }
266
267 final long rawContactId = getRawContactId(state, diff, results);
268 if (rawContactId == -1) {
269 throw new IllegalStateException("Could not determine RawContact ID after save");
270 }
271 final Uri rawContactUri = ContentUris.withAppendedId(
272 RawContacts.CONTENT_URI, rawContactId);
273 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
274 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
275 break;
276
277 } catch (RemoteException e) {
278 // Something went wrong, bail without success
279 Log.e(TAG, "Problem persisting user edits", e);
280 break;
281
282 } catch (OperationApplicationException e) {
283 // Version consistency failed, re-parent change and try again
284 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
285 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
286 boolean first = true;
287 final int count = state.size();
288 for (int i = 0; i < count; i++) {
289 Long rawContactId = state.getRawContactId(i);
290 if (rawContactId != null && rawContactId != -1) {
291 if (!first) {
292 sb.append(',');
293 }
294 sb.append(rawContactId);
295 first = false;
296 }
297 }
298 sb.append(")");
299
300 if (first) {
301 throw new IllegalStateException("Version consistency failed for a new contact");
302 }
303
304 final EntityDeltaList newState = EntityDeltaList.fromQuery(resolver,
305 sb.toString(), null, null);
306 state = EntityDeltaList.mergeAfter(newState, state);
307 }
308 }
309
310 callbackIntent.setData(lookupUri);
311
312 startActivity(callbackIntent);
313 }
314
315 private long getRawContactId(EntityDeltaList state,
316 final ArrayList<ContentProviderOperation> diff,
317 final ContentProviderResult[] results) {
318 long rawContactId = state.findRawContactId();
319 if (rawContactId != -1) {
320 return rawContactId;
321 }
322
323 final int diffSize = diff.size();
324 for (int i = 0; i < diffSize; i++) {
325 ContentProviderOperation operation = diff.get(i);
326 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
327 && operation.getUri().getEncodedPath().contains(
328 RawContacts.CONTENT_URI.getEncodedPath())) {
329 return ContentUris.parseId(results[i].uri);
330 }
331 }
332 return -1;
333 }
334
335 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800336 * Creates an intent that can be sent to this service to create a new group.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700337 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800338 public static Intent createNewGroupIntent(Context context, Account account, String label,
339 Class<?> callbackActivity, String callbackAction) {
340 Intent serviceIntent = new Intent(context, ContactSaveService.class);
341 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
342 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
343 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
344 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700345
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800346 // Callback intent will be invoked by the service once the new group is
347 // created. The service will put a group membership row in the extras
348 // of the callback intent.
349 Intent callbackIntent = new Intent(context, callbackActivity);
350 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700351 callbackIntent.setFlags(
352 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
353 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800354
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700355 return serviceIntent;
356 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800357
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800358 private void createGroup(Intent intent) {
359 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
360 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
361 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
362
363 ContentValues values = new ContentValues();
364 values.put(Groups.ACCOUNT_TYPE, accountType);
365 values.put(Groups.ACCOUNT_NAME, accountName);
366 values.put(Groups.TITLE, label);
367
368 Uri groupUri = getContentResolver().insert(Groups.CONTENT_URI, values);
369 if (groupUri == null) {
370 return;
371 }
372
373 values.clear();
374 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
375 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
376
377 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
378 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
379
380 startActivity(callbackIntent);
381 }
382
383 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800384 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800385 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800386 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel) {
387 Intent serviceIntent = new Intent(context, ContactSaveService.class);
388 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
389 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
390 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800391 return serviceIntent;
392 }
393
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800394 private void renameGroup(Intent intent) {
395 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
396 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
397
398 if (groupId == -1) {
399 Log.e(TAG, "Invalid arguments for renameGroup request");
400 return;
401 }
402
403 ContentValues values = new ContentValues();
404 values.put(Groups.TITLE, label);
405 getContentResolver().update(
406 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), values, null, null);
407 }
408
409 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800410 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800411 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800412 public static Intent createGroupDeletionIntent(Context context, long groupId) {
413 Intent serviceIntent = new Intent(context, ContactSaveService.class);
414 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800415 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800416 return serviceIntent;
417 }
418
419 private void deleteGroup(Intent intent) {
420 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
421 if (groupId == -1) {
422 Log.e(TAG, "Invalid arguments for deleteGroup request");
423 return;
424 }
425
426 getContentResolver().delete(
427 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
428 }
429
430 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800431 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800432 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800433 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
434 Intent serviceIntent = new Intent(context, ContactSaveService.class);
435 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
436 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
437 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
438
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800439 return serviceIntent;
440 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800441
442 private void setStarred(Intent intent) {
443 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
444 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
445 if (contactUri == null) {
446 Log.e(TAG, "Invalid arguments for setStarred request");
447 return;
448 }
449
450 final ContentValues values = new ContentValues(1);
451 values.put(Contacts.STARRED, value);
452 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800453 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800454
455 /**
456 * Creates an intent that sets the selected data item as super primary (default)
457 */
458 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
459 Intent serviceIntent = new Intent(context, ContactSaveService.class);
460 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
461 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
462 return serviceIntent;
463 }
464
465 private void setSuperPrimary(Intent intent) {
466 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
467 if (dataId == -1) {
468 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
469 return;
470 }
471
472 // Update the primary values in the data record.
473 ContentValues values = new ContentValues(1);
474 values.put(Data.IS_SUPER_PRIMARY, 1);
475 values.put(Data.IS_PRIMARY, 1);
476
477 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
478 values, null, null);
479 }
480
481 /**
482 * Creates an intent that clears the primary flag of all data items that belong to the same
483 * raw_contact as the given data item. Will only clear, if the data item was primary before
484 * this call
485 */
486 public static Intent createClearPrimaryIntent(Context context, long dataId) {
487 Intent serviceIntent = new Intent(context, ContactSaveService.class);
488 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
489 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
490 return serviceIntent;
491 }
492
493 private void clearPrimary(Intent intent) {
494 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
495 if (dataId == -1) {
496 Log.e(TAG, "Invalid arguments for clearPrimary request");
497 return;
498 }
499
500 // Update the primary values in the data record.
501 ContentValues values = new ContentValues(1);
502 values.put(Data.IS_SUPER_PRIMARY, 0);
503 values.put(Data.IS_PRIMARY, 0);
504
505 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
506 values, null, null);
507 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800508
509 /**
510 * Creates an intent that can be sent to this service to delete a contact.
511 */
512 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
513 Intent serviceIntent = new Intent(context, ContactSaveService.class);
514 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
515 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
516 return serviceIntent;
517 }
518
519 private void deleteContact(Intent intent) {
520 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
521 if (contactUri == null) {
522 Log.e(TAG, "Invalid arguments for deleteContact request");
523 return;
524 }
525
526 getContentResolver().delete(contactUri, null, null);
527 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800528
529 /**
530 * Creates an intent that can be sent to this service to join two contacts.
531 */
532 public static Intent createJoinContactsIntent(Context context, long contactId1,
533 long contactId2, boolean contactWritable,
534 Class<?> callbackActivity, String callbackAction) {
535 Intent serviceIntent = new Intent(context, ContactSaveService.class);
536 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
537 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
538 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
539 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
540
541 // Callback intent will be invoked by the service once the contacts are joined.
542 Intent callbackIntent = new Intent(context, callbackActivity);
543 callbackIntent.setAction(callbackAction);
544 callbackIntent.setFlags(
545 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
546 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
547
548 return serviceIntent;
549 }
550
551
552 private interface JoinContactQuery {
553 String[] PROJECTION = {
554 RawContacts._ID,
555 RawContacts.CONTACT_ID,
556 RawContacts.NAME_VERIFIED,
557 RawContacts.DISPLAY_NAME_SOURCE,
558 };
559
560 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
561
562 int _ID = 0;
563 int CONTACT_ID = 1;
564 int NAME_VERIFIED = 2;
565 int DISPLAY_NAME_SOURCE = 3;
566 }
567
568 private void joinContacts(Intent intent) {
569 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
570 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
571 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
572 if (contactId1 == -1 || contactId2 == -1) {
573 Log.e(TAG, "Invalid arguments for joinContacts request");
574 return;
575 }
576
577 final ContentResolver resolver = getContentResolver();
578
579 // Load raw contact IDs for all raw contacts involved - currently edited and selected
580 // in the join UIs
581 Cursor c = resolver.query(RawContacts.CONTENT_URI,
582 JoinContactQuery.PROJECTION,
583 JoinContactQuery.SELECTION,
584 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
585
586 long rawContactIds[];
587 long verifiedNameRawContactId = -1;
588 try {
589 int maxDisplayNameSource = -1;
590 rawContactIds = new long[c.getCount()];
591 for (int i = 0; i < rawContactIds.length; i++) {
592 c.moveToPosition(i);
593 long rawContactId = c.getLong(JoinContactQuery._ID);
594 rawContactIds[i] = rawContactId;
595 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
596 if (nameSource > maxDisplayNameSource) {
597 maxDisplayNameSource = nameSource;
598 }
599 }
600
601 // Find an appropriate display name for the joined contact:
602 // if should have a higher DisplayNameSource or be the name
603 // of the original contact that we are joining with another.
604 if (writable) {
605 for (int i = 0; i < rawContactIds.length; i++) {
606 c.moveToPosition(i);
607 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
608 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
609 if (nameSource == maxDisplayNameSource
610 && (verifiedNameRawContactId == -1
611 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
612 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
613 }
614 }
615 }
616 }
617 } finally {
618 c.close();
619 }
620
621 // For each pair of raw contacts, insert an aggregation exception
622 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
623 for (int i = 0; i < rawContactIds.length; i++) {
624 for (int j = 0; j < rawContactIds.length; j++) {
625 if (i != j) {
626 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
627 }
628 }
629 }
630
631 // Mark the original contact as "name verified" to make sure that the contact
632 // display name does not change as a result of the join
633 if (verifiedNameRawContactId != -1) {
634 Builder builder = ContentProviderOperation.newUpdate(
635 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
636 builder.withValue(RawContacts.NAME_VERIFIED, 1);
637 operations.add(builder.build());
638 }
639
640 boolean success = false;
641 // Apply all aggregation exceptions as one batch
642 try {
643 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800644 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800645 success = true;
646 } catch (RemoteException e) {
647 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800648 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800649 } catch (OperationApplicationException e) {
650 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800651 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800652 }
653
654 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
655 if (success) {
656 Uri uri = RawContacts.getContactLookupUri(resolver,
657 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
658 callbackIntent.setData(uri);
659 }
660 startActivity(callbackIntent);
661 }
662
663 /**
664 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
665 */
666 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
667 long rawContactId1, long rawContactId2) {
668 Builder builder =
669 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
670 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
671 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
672 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
673 operations.add(builder.build());
674 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800675
676 /**
677 * Shows a toast on the UI thread.
678 */
679 private void showToast(final int message) {
680 new Handler(Looper.getMainLooper()).post(new Runnable() {
681
682 @Override
683 public void run() {
684 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
685 }
686 });
687 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700688}