blob: 9b56f5b11a3e3576e98af789d4d9c6aa270add0f [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);
399 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
400
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800401 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800402 }
403
404 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800405 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800406 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800407 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel) {
408 Intent serviceIntent = new Intent(context, ContactSaveService.class);
409 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
410 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
411 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800412 return serviceIntent;
413 }
414
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800415 private void renameGroup(Intent intent) {
416 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
417 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
418
419 if (groupId == -1) {
420 Log.e(TAG, "Invalid arguments for renameGroup request");
421 return;
422 }
423
424 ContentValues values = new ContentValues();
425 values.put(Groups.TITLE, label);
426 getContentResolver().update(
427 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), values, null, null);
428 }
429
430 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800431 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800432 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800433 public static Intent createGroupDeletionIntent(Context context, long groupId) {
434 Intent serviceIntent = new Intent(context, ContactSaveService.class);
435 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800436 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800437 return serviceIntent;
438 }
439
440 private void deleteGroup(Intent intent) {
441 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
442 if (groupId == -1) {
443 Log.e(TAG, "Invalid arguments for deleteGroup request");
444 return;
445 }
446
447 getContentResolver().delete(
448 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
449 }
450
451 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800452 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800453 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800454 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
455 Intent serviceIntent = new Intent(context, ContactSaveService.class);
456 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
457 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
458 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
459
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800460 return serviceIntent;
461 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800462
463 private void setStarred(Intent intent) {
464 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
465 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
466 if (contactUri == null) {
467 Log.e(TAG, "Invalid arguments for setStarred request");
468 return;
469 }
470
471 final ContentValues values = new ContentValues(1);
472 values.put(Contacts.STARRED, value);
473 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800474 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800475
476 /**
477 * Creates an intent that sets the selected data item as super primary (default)
478 */
479 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
480 Intent serviceIntent = new Intent(context, ContactSaveService.class);
481 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
482 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
483 return serviceIntent;
484 }
485
486 private void setSuperPrimary(Intent intent) {
487 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
488 if (dataId == -1) {
489 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
490 return;
491 }
492
493 // Update the primary values in the data record.
494 ContentValues values = new ContentValues(1);
495 values.put(Data.IS_SUPER_PRIMARY, 1);
496 values.put(Data.IS_PRIMARY, 1);
497
498 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
499 values, null, null);
500 }
501
502 /**
503 * Creates an intent that clears the primary flag of all data items that belong to the same
504 * raw_contact as the given data item. Will only clear, if the data item was primary before
505 * this call
506 */
507 public static Intent createClearPrimaryIntent(Context context, long dataId) {
508 Intent serviceIntent = new Intent(context, ContactSaveService.class);
509 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
510 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
511 return serviceIntent;
512 }
513
514 private void clearPrimary(Intent intent) {
515 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
516 if (dataId == -1) {
517 Log.e(TAG, "Invalid arguments for clearPrimary request");
518 return;
519 }
520
521 // Update the primary values in the data record.
522 ContentValues values = new ContentValues(1);
523 values.put(Data.IS_SUPER_PRIMARY, 0);
524 values.put(Data.IS_PRIMARY, 0);
525
526 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
527 values, null, null);
528 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800529
530 /**
531 * Creates an intent that can be sent to this service to delete a contact.
532 */
533 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
534 Intent serviceIntent = new Intent(context, ContactSaveService.class);
535 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
536 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
537 return serviceIntent;
538 }
539
540 private void deleteContact(Intent intent) {
541 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
542 if (contactUri == null) {
543 Log.e(TAG, "Invalid arguments for deleteContact request");
544 return;
545 }
546
547 getContentResolver().delete(contactUri, null, null);
548 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800549
550 /**
551 * Creates an intent that can be sent to this service to join two contacts.
552 */
553 public static Intent createJoinContactsIntent(Context context, long contactId1,
554 long contactId2, boolean contactWritable,
555 Class<?> callbackActivity, String callbackAction) {
556 Intent serviceIntent = new Intent(context, ContactSaveService.class);
557 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
558 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
559 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
560 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
561
562 // Callback intent will be invoked by the service once the contacts are joined.
563 Intent callbackIntent = new Intent(context, callbackActivity);
564 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800565 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
566
567 return serviceIntent;
568 }
569
570
571 private interface JoinContactQuery {
572 String[] PROJECTION = {
573 RawContacts._ID,
574 RawContacts.CONTACT_ID,
575 RawContacts.NAME_VERIFIED,
576 RawContacts.DISPLAY_NAME_SOURCE,
577 };
578
579 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
580
581 int _ID = 0;
582 int CONTACT_ID = 1;
583 int NAME_VERIFIED = 2;
584 int DISPLAY_NAME_SOURCE = 3;
585 }
586
587 private void joinContacts(Intent intent) {
588 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
589 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
590 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
591 if (contactId1 == -1 || contactId2 == -1) {
592 Log.e(TAG, "Invalid arguments for joinContacts request");
593 return;
594 }
595
596 final ContentResolver resolver = getContentResolver();
597
598 // Load raw contact IDs for all raw contacts involved - currently edited and selected
599 // in the join UIs
600 Cursor c = resolver.query(RawContacts.CONTENT_URI,
601 JoinContactQuery.PROJECTION,
602 JoinContactQuery.SELECTION,
603 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
604
605 long rawContactIds[];
606 long verifiedNameRawContactId = -1;
607 try {
608 int maxDisplayNameSource = -1;
609 rawContactIds = new long[c.getCount()];
610 for (int i = 0; i < rawContactIds.length; i++) {
611 c.moveToPosition(i);
612 long rawContactId = c.getLong(JoinContactQuery._ID);
613 rawContactIds[i] = rawContactId;
614 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
615 if (nameSource > maxDisplayNameSource) {
616 maxDisplayNameSource = nameSource;
617 }
618 }
619
620 // Find an appropriate display name for the joined contact:
621 // if should have a higher DisplayNameSource or be the name
622 // of the original contact that we are joining with another.
623 if (writable) {
624 for (int i = 0; i < rawContactIds.length; i++) {
625 c.moveToPosition(i);
626 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
627 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
628 if (nameSource == maxDisplayNameSource
629 && (verifiedNameRawContactId == -1
630 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
631 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
632 }
633 }
634 }
635 }
636 } finally {
637 c.close();
638 }
639
640 // For each pair of raw contacts, insert an aggregation exception
641 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
642 for (int i = 0; i < rawContactIds.length; i++) {
643 for (int j = 0; j < rawContactIds.length; j++) {
644 if (i != j) {
645 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
646 }
647 }
648 }
649
650 // Mark the original contact as "name verified" to make sure that the contact
651 // display name does not change as a result of the join
652 if (verifiedNameRawContactId != -1) {
653 Builder builder = ContentProviderOperation.newUpdate(
654 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
655 builder.withValue(RawContacts.NAME_VERIFIED, 1);
656 operations.add(builder.build());
657 }
658
659 boolean success = false;
660 // Apply all aggregation exceptions as one batch
661 try {
662 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800663 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800664 success = true;
665 } catch (RemoteException e) {
666 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800667 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800668 } catch (OperationApplicationException e) {
669 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800670 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800671 }
672
673 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
674 if (success) {
675 Uri uri = RawContacts.getContactLookupUri(resolver,
676 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
677 callbackIntent.setData(uri);
678 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800679 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800680 }
681
682 /**
683 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
684 */
685 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
686 long rawContactId1, long rawContactId2) {
687 Builder builder =
688 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
689 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
690 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
691 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
692 operations.add(builder.build());
693 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800694
695 /**
696 * Shows a toast on the UI thread.
697 */
698 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800699 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800700
701 @Override
702 public void run() {
703 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
704 }
705 });
706 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800707
708 private void deliverCallback(final Intent callbackIntent) {
709 mMainHandler.post(new Runnable() {
710
711 @Override
712 public void run() {
713 deliverCallbackOnUiThread(callbackIntent);
714 }
715 });
716 }
717
718 void deliverCallbackOnUiThread(final Intent callbackIntent) {
719 // TODO: this assumes that if there are multiple instances of the same
720 // activity registered, the last one registered is the one waiting for
721 // the callback. Validity of this assumption needs to be verified.
722 synchronized (sListeners) {
723 for (Listener listener : sListeners) {
724 if (callbackIntent.getComponent().equals(
725 ((Activity) listener).getIntent().getComponent())) {
726 listener.onServiceCompleted(callbackIntent);
727 return;
728 }
729 }
730 }
731 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700732}