blob: 591222501b16ec4a546ba9ac53b9ef32e8c90b19 [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
Katherine Kuana007e442011-07-07 09:25:34 -070064 /** Set to true in order to view logs on content provider operations */
65 private static final boolean DEBUG = false;
66
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070067 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
68
69 public static final String EXTRA_ACCOUNT_NAME = "accountName";
70 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
71 public static final String EXTRA_CONTENT_VALUES = "contentValues";
72 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
73
Dmitri Plotnikova0114142011-02-15 13:53:21 -080074 public static final String ACTION_SAVE_CONTACT = "saveContact";
75 public static final String EXTRA_CONTACT_STATE = "state";
76 public static final String EXTRA_SAVE_MODE = "saveMode";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070077
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080078 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080079 public static final String ACTION_RENAME_GROUP = "renameGroup";
80 public static final String ACTION_DELETE_GROUP = "deleteGroup";
81 public static final String EXTRA_GROUP_ID = "groupId";
82 public static final String EXTRA_GROUP_LABEL = "groupLabel";
83
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080084 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -080085 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080086 public static final String EXTRA_CONTACT_URI = "contactUri";
87 public static final String EXTRA_STARRED_FLAG = "starred";
88
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -080089 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
90 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
91 public static final String EXTRA_DATA_ID = "dataId";
92
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080093 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
94 public static final String EXTRA_CONTACT_ID1 = "contactId1";
95 public static final String EXTRA_CONTACT_ID2 = "contactId2";
96 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
97
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070098 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
99 Data.MIMETYPE,
100 Data.IS_PRIMARY,
101 Data.DATA1,
102 Data.DATA2,
103 Data.DATA3,
104 Data.DATA4,
105 Data.DATA5,
106 Data.DATA6,
107 Data.DATA7,
108 Data.DATA8,
109 Data.DATA9,
110 Data.DATA10,
111 Data.DATA11,
112 Data.DATA12,
113 Data.DATA13,
114 Data.DATA14,
115 Data.DATA15
116 );
117
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800118 private static final int PERSIST_TRIES = 3;
119
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800120 public interface Listener {
121 public void onServiceCompleted(Intent callbackIntent);
122 }
123
124 private static final LinkedList<Listener> sListeners = new LinkedList<Listener>();
125
126 private Handler mMainHandler;
127
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700128 public ContactSaveService() {
129 super(TAG);
130 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800131 mMainHandler = new Handler(Looper.getMainLooper());
132 }
133
134 public static void registerListener(Listener listener) {
135 if (!(listener instanceof Activity)) {
136 throw new ClassCastException("Only activities can be registered to"
137 + " receive callback from " + ContactSaveService.class.getName());
138 }
139 synchronized (sListeners) {
140 sListeners.addFirst(listener);
141 }
142 }
143
144 public static void unregisterListener(Listener listener) {
145 synchronized (sListeners) {
146 sListeners.remove(listener);
147 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700148 }
149
150 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800151 public Object getSystemService(String name) {
152 Object service = super.getSystemService(name);
153 if (service != null) {
154 return service;
155 }
156
157 return getApplicationContext().getSystemService(name);
158 }
159
160 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700161 protected void onHandleIntent(Intent intent) {
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700162 String action = intent.getAction();
163 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
164 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800165 } else if (ACTION_SAVE_CONTACT.equals(action)) {
166 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800167 } else if (ACTION_CREATE_GROUP.equals(action)) {
168 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800169 } else if (ACTION_RENAME_GROUP.equals(action)) {
170 renameGroup(intent);
171 } else if (ACTION_DELETE_GROUP.equals(action)) {
172 deleteGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800173 } else if (ACTION_SET_STARRED.equals(action)) {
174 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800175 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
176 setSuperPrimary(intent);
177 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
178 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800179 } else if (ACTION_DELETE_CONTACT.equals(action)) {
180 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800181 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
182 joinContacts(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700183 }
184 }
185
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800186 /**
187 * Creates an intent that can be sent to this service to create a new raw contact
188 * using data presented as a set of ContentValues.
189 */
190 public static Intent createNewRawContactIntent(Context context,
191 ArrayList<ContentValues> values, Account account, Class<?> callbackActivity,
192 String callbackAction) {
193 Intent serviceIntent = new Intent(
194 context, ContactSaveService.class);
195 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
196 if (account != null) {
197 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
198 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
199 }
200 serviceIntent.putParcelableArrayListExtra(
201 ContactSaveService.EXTRA_CONTENT_VALUES, values);
202
203 // Callback intent will be invoked by the service once the new contact is
204 // created. The service will put the URI of the new contact as "data" on
205 // the callback intent.
206 Intent callbackIntent = new Intent(context, callbackActivity);
207 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800208 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
209 return serviceIntent;
210 }
211
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700212 private void createRawContact(Intent intent) {
213 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
214 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
215 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
216 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
217
218 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
219 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
220 .withValue(RawContacts.ACCOUNT_NAME, accountName)
221 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
222 .build());
223
224 int size = valueList.size();
225 for (int i = 0; i < size; i++) {
226 ContentValues values = valueList.get(i);
227 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
228 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
229 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
230 .withValues(values)
231 .build());
232 }
233
234 ContentResolver resolver = getContentResolver();
235 ContentProviderResult[] results;
236 try {
237 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
238 } catch (Exception e) {
239 throw new RuntimeException("Failed to store new contact", e);
240 }
241
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700242 Uri rawContactUri = results[0].uri;
243 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
244
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800245 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700246 }
247
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700248 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800249 * Creates an intent that can be sent to this service to create a new raw contact
250 * using data presented as a set of ContentValues.
251 */
252 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
253 String saveModeExtraKey, int saveMode, Class<?> callbackActivity,
254 String callbackAction) {
255 Intent serviceIntent = new Intent(
256 context, ContactSaveService.class);
257 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
258 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
259
260 // Callback intent will be invoked by the service once the contact is
261 // saved. The service will put the URI of the new contact as "data" on
262 // the callback intent.
263 Intent callbackIntent = new Intent(context, callbackActivity);
264 callbackIntent.putExtra(saveModeExtraKey, saveMode);
265 callbackIntent.setAction(callbackAction);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800266 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
267 return serviceIntent;
268 }
269
270 private void saveContact(Intent intent) {
271 EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
272 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
273
274 // Trim any empty fields, and RawContacts, before persisting
275 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
276 EntityModifier.trimEmpty(state, accountTypes);
277
278 Uri lookupUri = null;
279
280 final ContentResolver resolver = getContentResolver();
281
282 // Attempt to persist changes
283 int tries = 0;
284 while (tries++ < PERSIST_TRIES) {
285 try {
286 // Build operations and try applying
287 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700288 if (DEBUG) {
289 Log.v(TAG, "Content Provider Operations:");
290 for (ContentProviderOperation operation : diff) {
291 Log.v(TAG, operation.toString());
292 }
293 }
294
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800295 ContentProviderResult[] results = null;
296 if (!diff.isEmpty()) {
297 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
298 }
299
300 final long rawContactId = getRawContactId(state, diff, results);
301 if (rawContactId == -1) {
302 throw new IllegalStateException("Could not determine RawContact ID after save");
303 }
304 final Uri rawContactUri = ContentUris.withAppendedId(
305 RawContacts.CONTENT_URI, rawContactId);
306 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
307 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
308 break;
309
310 } catch (RemoteException e) {
311 // Something went wrong, bail without success
312 Log.e(TAG, "Problem persisting user edits", e);
313 break;
314
315 } catch (OperationApplicationException e) {
316 // Version consistency failed, re-parent change and try again
317 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
318 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
319 boolean first = true;
320 final int count = state.size();
321 for (int i = 0; i < count; i++) {
322 Long rawContactId = state.getRawContactId(i);
323 if (rawContactId != null && rawContactId != -1) {
324 if (!first) {
325 sb.append(',');
326 }
327 sb.append(rawContactId);
328 first = false;
329 }
330 }
331 sb.append(")");
332
333 if (first) {
334 throw new IllegalStateException("Version consistency failed for a new contact");
335 }
336
337 final EntityDeltaList newState = EntityDeltaList.fromQuery(resolver,
338 sb.toString(), null, null);
339 state = EntityDeltaList.mergeAfter(newState, state);
340 }
341 }
342
343 callbackIntent.setData(lookupUri);
344
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800345 deliverCallback(callbackIntent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800346 }
347
348 private long getRawContactId(EntityDeltaList state,
349 final ArrayList<ContentProviderOperation> diff,
350 final ContentProviderResult[] results) {
351 long rawContactId = state.findRawContactId();
352 if (rawContactId != -1) {
353 return rawContactId;
354 }
355
356 final int diffSize = diff.size();
357 for (int i = 0; i < diffSize; i++) {
358 ContentProviderOperation operation = diff.get(i);
359 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
360 && operation.getUri().getEncodedPath().contains(
361 RawContacts.CONTENT_URI.getEncodedPath())) {
362 return ContentUris.parseId(results[i].uri);
363 }
364 }
365 return -1;
366 }
367
368 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800369 * Creates an intent that can be sent to this service to create a new group.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700370 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800371 public static Intent createNewGroupIntent(Context context, Account account, String label,
372 Class<?> callbackActivity, String callbackAction) {
373 Intent serviceIntent = new Intent(context, ContactSaveService.class);
374 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
375 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
376 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
377 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700378
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800379 // Callback intent will be invoked by the service once the new group is
380 // created. The service will put a group membership row in the extras
381 // of the callback intent.
382 Intent callbackIntent = new Intent(context, callbackActivity);
383 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700384 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800385
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700386 return serviceIntent;
387 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800388
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800389 private void createGroup(Intent intent) {
390 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
391 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
392 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
393
394 ContentValues values = new ContentValues();
395 values.put(Groups.ACCOUNT_TYPE, accountType);
396 values.put(Groups.ACCOUNT_NAME, accountName);
397 values.put(Groups.TITLE, label);
398
399 Uri groupUri = getContentResolver().insert(Groups.CONTENT_URI, values);
400 if (groupUri == null) {
401 return;
402 }
403
404 values.clear();
405 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
406 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
407
408 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700409 callbackIntent.setData(groupUri);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800410 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
411
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800412 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800413 }
414
415 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800416 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800417 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700418 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
419 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800420 Intent serviceIntent = new Intent(context, ContactSaveService.class);
421 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
422 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
423 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700424
425 // Callback intent will be invoked by the service once the group is renamed.
426 Intent callbackIntent = new Intent(context, callbackActivity);
427 callbackIntent.setAction(callbackAction);
428 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
429
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800430 return serviceIntent;
431 }
432
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800433 private void renameGroup(Intent intent) {
434 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
435 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
436
437 if (groupId == -1) {
438 Log.e(TAG, "Invalid arguments for renameGroup request");
439 return;
440 }
441
442 ContentValues values = new ContentValues();
443 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700444 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
445 getContentResolver().update(groupUri, values, null, null);
446
447 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
448 callbackIntent.setData(groupUri);
449 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800450 }
451
452 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800453 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800454 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800455 public static Intent createGroupDeletionIntent(Context context, long groupId) {
456 Intent serviceIntent = new Intent(context, ContactSaveService.class);
457 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800458 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800459 return serviceIntent;
460 }
461
462 private void deleteGroup(Intent intent) {
463 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
464 if (groupId == -1) {
465 Log.e(TAG, "Invalid arguments for deleteGroup request");
466 return;
467 }
468
469 getContentResolver().delete(
470 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
471 }
472
473 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800474 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800475 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800476 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
477 Intent serviceIntent = new Intent(context, ContactSaveService.class);
478 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
479 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
480 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
481
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800482 return serviceIntent;
483 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800484
485 private void setStarred(Intent intent) {
486 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
487 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
488 if (contactUri == null) {
489 Log.e(TAG, "Invalid arguments for setStarred request");
490 return;
491 }
492
493 final ContentValues values = new ContentValues(1);
494 values.put(Contacts.STARRED, value);
495 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800496 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800497
498 /**
499 * Creates an intent that sets the selected data item as super primary (default)
500 */
501 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
502 Intent serviceIntent = new Intent(context, ContactSaveService.class);
503 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
504 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
505 return serviceIntent;
506 }
507
508 private void setSuperPrimary(Intent intent) {
509 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
510 if (dataId == -1) {
511 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
512 return;
513 }
514
515 // Update the primary values in the data record.
516 ContentValues values = new ContentValues(1);
517 values.put(Data.IS_SUPER_PRIMARY, 1);
518 values.put(Data.IS_PRIMARY, 1);
519
520 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
521 values, null, null);
522 }
523
524 /**
525 * Creates an intent that clears the primary flag of all data items that belong to the same
526 * raw_contact as the given data item. Will only clear, if the data item was primary before
527 * this call
528 */
529 public static Intent createClearPrimaryIntent(Context context, long dataId) {
530 Intent serviceIntent = new Intent(context, ContactSaveService.class);
531 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
532 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
533 return serviceIntent;
534 }
535
536 private void clearPrimary(Intent intent) {
537 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
538 if (dataId == -1) {
539 Log.e(TAG, "Invalid arguments for clearPrimary request");
540 return;
541 }
542
543 // Update the primary values in the data record.
544 ContentValues values = new ContentValues(1);
545 values.put(Data.IS_SUPER_PRIMARY, 0);
546 values.put(Data.IS_PRIMARY, 0);
547
548 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
549 values, null, null);
550 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800551
552 /**
553 * Creates an intent that can be sent to this service to delete a contact.
554 */
555 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
556 Intent serviceIntent = new Intent(context, ContactSaveService.class);
557 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
558 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
559 return serviceIntent;
560 }
561
562 private void deleteContact(Intent intent) {
563 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
564 if (contactUri == null) {
565 Log.e(TAG, "Invalid arguments for deleteContact request");
566 return;
567 }
568
569 getContentResolver().delete(contactUri, null, null);
570 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800571
572 /**
573 * Creates an intent that can be sent to this service to join two contacts.
574 */
575 public static Intent createJoinContactsIntent(Context context, long contactId1,
576 long contactId2, boolean contactWritable,
577 Class<?> callbackActivity, String callbackAction) {
578 Intent serviceIntent = new Intent(context, ContactSaveService.class);
579 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
580 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
581 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
582 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
583
584 // Callback intent will be invoked by the service once the contacts are joined.
585 Intent callbackIntent = new Intent(context, callbackActivity);
586 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800587 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
588
589 return serviceIntent;
590 }
591
592
593 private interface JoinContactQuery {
594 String[] PROJECTION = {
595 RawContacts._ID,
596 RawContacts.CONTACT_ID,
597 RawContacts.NAME_VERIFIED,
598 RawContacts.DISPLAY_NAME_SOURCE,
599 };
600
601 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
602
603 int _ID = 0;
604 int CONTACT_ID = 1;
605 int NAME_VERIFIED = 2;
606 int DISPLAY_NAME_SOURCE = 3;
607 }
608
609 private void joinContacts(Intent intent) {
610 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
611 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
612 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
613 if (contactId1 == -1 || contactId2 == -1) {
614 Log.e(TAG, "Invalid arguments for joinContacts request");
615 return;
616 }
617
618 final ContentResolver resolver = getContentResolver();
619
620 // Load raw contact IDs for all raw contacts involved - currently edited and selected
621 // in the join UIs
622 Cursor c = resolver.query(RawContacts.CONTENT_URI,
623 JoinContactQuery.PROJECTION,
624 JoinContactQuery.SELECTION,
625 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
626
627 long rawContactIds[];
628 long verifiedNameRawContactId = -1;
629 try {
630 int maxDisplayNameSource = -1;
631 rawContactIds = new long[c.getCount()];
632 for (int i = 0; i < rawContactIds.length; i++) {
633 c.moveToPosition(i);
634 long rawContactId = c.getLong(JoinContactQuery._ID);
635 rawContactIds[i] = rawContactId;
636 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
637 if (nameSource > maxDisplayNameSource) {
638 maxDisplayNameSource = nameSource;
639 }
640 }
641
642 // Find an appropriate display name for the joined contact:
643 // if should have a higher DisplayNameSource or be the name
644 // of the original contact that we are joining with another.
645 if (writable) {
646 for (int i = 0; i < rawContactIds.length; i++) {
647 c.moveToPosition(i);
648 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
649 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
650 if (nameSource == maxDisplayNameSource
651 && (verifiedNameRawContactId == -1
652 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
653 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
654 }
655 }
656 }
657 }
658 } finally {
659 c.close();
660 }
661
662 // For each pair of raw contacts, insert an aggregation exception
663 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
664 for (int i = 0; i < rawContactIds.length; i++) {
665 for (int j = 0; j < rawContactIds.length; j++) {
666 if (i != j) {
667 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
668 }
669 }
670 }
671
672 // Mark the original contact as "name verified" to make sure that the contact
673 // display name does not change as a result of the join
674 if (verifiedNameRawContactId != -1) {
675 Builder builder = ContentProviderOperation.newUpdate(
676 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
677 builder.withValue(RawContacts.NAME_VERIFIED, 1);
678 operations.add(builder.build());
679 }
680
681 boolean success = false;
682 // Apply all aggregation exceptions as one batch
683 try {
684 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800685 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800686 success = true;
687 } catch (RemoteException e) {
688 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800689 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800690 } catch (OperationApplicationException e) {
691 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800692 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800693 }
694
695 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
696 if (success) {
697 Uri uri = RawContacts.getContactLookupUri(resolver,
698 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
699 callbackIntent.setData(uri);
700 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800701 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800702 }
703
704 /**
705 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
706 */
707 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
708 long rawContactId1, long rawContactId2) {
709 Builder builder =
710 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
711 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
712 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
713 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
714 operations.add(builder.build());
715 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800716
717 /**
718 * Shows a toast on the UI thread.
719 */
720 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800721 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800722
723 @Override
724 public void run() {
725 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
726 }
727 });
728 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800729
730 private void deliverCallback(final Intent callbackIntent) {
731 mMainHandler.post(new Runnable() {
732
733 @Override
734 public void run() {
735 deliverCallbackOnUiThread(callbackIntent);
736 }
737 });
738 }
739
740 void deliverCallbackOnUiThread(final Intent callbackIntent) {
741 // TODO: this assumes that if there are multiple instances of the same
742 // activity registered, the last one registered is the one waiting for
743 // the callback. Validity of this assumption needs to be verified.
744 synchronized (sListeners) {
745 for (Listener listener : sListeners) {
746 if (callbackIntent.getComponent().equals(
747 ((Activity) listener).getIntent().getComponent())) {
748 listener.onServiceCompleted(callbackIntent);
749 return;
750 }
751 }
752 }
753 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700754}