blob: 194d0ba333b77b9900fe6ac94b912f009e657f85 [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";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070081 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080082 public static final String EXTRA_GROUP_ID = "groupId";
83 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070084 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
85 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080086
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080087 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -080088 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080089 public static final String EXTRA_CONTACT_URI = "contactUri";
90 public static final String EXTRA_STARRED_FLAG = "starred";
91
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -080092 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
93 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
94 public static final String EXTRA_DATA_ID = "dataId";
95
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080096 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
97 public static final String EXTRA_CONTACT_ID1 = "contactId1";
98 public static final String EXTRA_CONTACT_ID2 = "contactId2";
99 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
100
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700101 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
102 Data.MIMETYPE,
103 Data.IS_PRIMARY,
104 Data.DATA1,
105 Data.DATA2,
106 Data.DATA3,
107 Data.DATA4,
108 Data.DATA5,
109 Data.DATA6,
110 Data.DATA7,
111 Data.DATA8,
112 Data.DATA9,
113 Data.DATA10,
114 Data.DATA11,
115 Data.DATA12,
116 Data.DATA13,
117 Data.DATA14,
118 Data.DATA15
119 );
120
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800121 private static final int PERSIST_TRIES = 3;
122
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800123 public interface Listener {
124 public void onServiceCompleted(Intent callbackIntent);
125 }
126
127 private static final LinkedList<Listener> sListeners = new LinkedList<Listener>();
128
129 private Handler mMainHandler;
130
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700131 public ContactSaveService() {
132 super(TAG);
133 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800134 mMainHandler = new Handler(Looper.getMainLooper());
135 }
136
137 public static void registerListener(Listener listener) {
138 if (!(listener instanceof Activity)) {
139 throw new ClassCastException("Only activities can be registered to"
140 + " receive callback from " + ContactSaveService.class.getName());
141 }
142 synchronized (sListeners) {
143 sListeners.addFirst(listener);
144 }
145 }
146
147 public static void unregisterListener(Listener listener) {
148 synchronized (sListeners) {
149 sListeners.remove(listener);
150 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700151 }
152
153 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800154 public Object getSystemService(String name) {
155 Object service = super.getSystemService(name);
156 if (service != null) {
157 return service;
158 }
159
160 return getApplicationContext().getSystemService(name);
161 }
162
163 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700164 protected void onHandleIntent(Intent intent) {
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700165 String action = intent.getAction();
166 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
167 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800168 } else if (ACTION_SAVE_CONTACT.equals(action)) {
169 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800170 } else if (ACTION_CREATE_GROUP.equals(action)) {
171 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800172 } else if (ACTION_RENAME_GROUP.equals(action)) {
173 renameGroup(intent);
174 } else if (ACTION_DELETE_GROUP.equals(action)) {
175 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700176 } else if (ACTION_UPDATE_GROUP.equals(action)) {
177 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800178 } else if (ACTION_SET_STARRED.equals(action)) {
179 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800180 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
181 setSuperPrimary(intent);
182 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
183 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800184 } else if (ACTION_DELETE_CONTACT.equals(action)) {
185 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800186 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
187 joinContacts(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700188 }
189 }
190
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800191 /**
192 * Creates an intent that can be sent to this service to create a new raw contact
193 * using data presented as a set of ContentValues.
194 */
195 public static Intent createNewRawContactIntent(Context context,
196 ArrayList<ContentValues> values, Account account, Class<?> callbackActivity,
197 String callbackAction) {
198 Intent serviceIntent = new Intent(
199 context, ContactSaveService.class);
200 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
201 if (account != null) {
202 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
203 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
204 }
205 serviceIntent.putParcelableArrayListExtra(
206 ContactSaveService.EXTRA_CONTENT_VALUES, values);
207
208 // Callback intent will be invoked by the service once the new contact is
209 // created. The service will put the URI of the new contact as "data" on
210 // the callback intent.
211 Intent callbackIntent = new Intent(context, callbackActivity);
212 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800213 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
214 return serviceIntent;
215 }
216
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700217 private void createRawContact(Intent intent) {
218 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
219 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
220 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
221 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
222
223 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
224 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
225 .withValue(RawContacts.ACCOUNT_NAME, accountName)
226 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
227 .build());
228
229 int size = valueList.size();
230 for (int i = 0; i < size; i++) {
231 ContentValues values = valueList.get(i);
232 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
233 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
234 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
235 .withValues(values)
236 .build());
237 }
238
239 ContentResolver resolver = getContentResolver();
240 ContentProviderResult[] results;
241 try {
242 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
243 } catch (Exception e) {
244 throw new RuntimeException("Failed to store new contact", e);
245 }
246
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700247 Uri rawContactUri = results[0].uri;
248 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
249
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800250 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700251 }
252
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700253 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800254 * Creates an intent that can be sent to this service to create a new raw contact
255 * using data presented as a set of ContentValues.
256 */
257 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
258 String saveModeExtraKey, int saveMode, Class<?> callbackActivity,
259 String callbackAction) {
260 Intent serviceIntent = new Intent(
261 context, ContactSaveService.class);
262 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
263 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
264
265 // Callback intent will be invoked by the service once the contact is
266 // saved. The service will put the URI of the new contact as "data" on
267 // the callback intent.
268 Intent callbackIntent = new Intent(context, callbackActivity);
269 callbackIntent.putExtra(saveModeExtraKey, saveMode);
270 callbackIntent.setAction(callbackAction);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800271 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
272 return serviceIntent;
273 }
274
275 private void saveContact(Intent intent) {
276 EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
277 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
278
279 // Trim any empty fields, and RawContacts, before persisting
280 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
281 EntityModifier.trimEmpty(state, accountTypes);
282
283 Uri lookupUri = null;
284
285 final ContentResolver resolver = getContentResolver();
286
287 // Attempt to persist changes
288 int tries = 0;
289 while (tries++ < PERSIST_TRIES) {
290 try {
291 // Build operations and try applying
292 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700293 if (DEBUG) {
294 Log.v(TAG, "Content Provider Operations:");
295 for (ContentProviderOperation operation : diff) {
296 Log.v(TAG, operation.toString());
297 }
298 }
299
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800300 ContentProviderResult[] results = null;
301 if (!diff.isEmpty()) {
302 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
303 }
304
305 final long rawContactId = getRawContactId(state, diff, results);
306 if (rawContactId == -1) {
307 throw new IllegalStateException("Could not determine RawContact ID after save");
308 }
309 final Uri rawContactUri = ContentUris.withAppendedId(
310 RawContacts.CONTENT_URI, rawContactId);
311 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
312 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
313 break;
314
315 } catch (RemoteException e) {
316 // Something went wrong, bail without success
317 Log.e(TAG, "Problem persisting user edits", e);
318 break;
319
320 } catch (OperationApplicationException e) {
321 // Version consistency failed, re-parent change and try again
322 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
323 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
324 boolean first = true;
325 final int count = state.size();
326 for (int i = 0; i < count; i++) {
327 Long rawContactId = state.getRawContactId(i);
328 if (rawContactId != null && rawContactId != -1) {
329 if (!first) {
330 sb.append(',');
331 }
332 sb.append(rawContactId);
333 first = false;
334 }
335 }
336 sb.append(")");
337
338 if (first) {
339 throw new IllegalStateException("Version consistency failed for a new contact");
340 }
341
342 final EntityDeltaList newState = EntityDeltaList.fromQuery(resolver,
343 sb.toString(), null, null);
344 state = EntityDeltaList.mergeAfter(newState, state);
345 }
346 }
347
348 callbackIntent.setData(lookupUri);
349
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800350 deliverCallback(callbackIntent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800351 }
352
353 private long getRawContactId(EntityDeltaList state,
354 final ArrayList<ContentProviderOperation> diff,
355 final ContentProviderResult[] results) {
356 long rawContactId = state.findRawContactId();
357 if (rawContactId != -1) {
358 return rawContactId;
359 }
360
361 final int diffSize = diff.size();
362 for (int i = 0; i < diffSize; i++) {
363 ContentProviderOperation operation = diff.get(i);
364 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
365 && operation.getUri().getEncodedPath().contains(
366 RawContacts.CONTENT_URI.getEncodedPath())) {
367 return ContentUris.parseId(results[i].uri);
368 }
369 }
370 return -1;
371 }
372
373 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800374 * Creates an intent that can be sent to this service to create a new group.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700375 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800376 public static Intent createNewGroupIntent(Context context, Account account, String label,
377 Class<?> callbackActivity, String callbackAction) {
378 Intent serviceIntent = new Intent(context, ContactSaveService.class);
379 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
380 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
381 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
382 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700383
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800384 // Callback intent will be invoked by the service once the new group is
385 // created. The service will put a group membership row in the extras
386 // of the callback intent.
387 Intent callbackIntent = new Intent(context, callbackActivity);
388 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700389 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800390
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700391 return serviceIntent;
392 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800393
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800394 private void createGroup(Intent intent) {
395 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
396 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
397 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
398
399 ContentValues values = new ContentValues();
400 values.put(Groups.ACCOUNT_TYPE, accountType);
401 values.put(Groups.ACCOUNT_NAME, accountName);
402 values.put(Groups.TITLE, label);
403
404 Uri groupUri = getContentResolver().insert(Groups.CONTENT_URI, values);
405 if (groupUri == null) {
406 return;
407 }
408
409 values.clear();
410 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
411 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
412
413 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700414 callbackIntent.setData(groupUri);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800415 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
416
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800417 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800418 }
419
420 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800421 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800422 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700423 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
424 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800425 Intent serviceIntent = new Intent(context, ContactSaveService.class);
426 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
427 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
428 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700429
430 // Callback intent will be invoked by the service once the group is renamed.
431 Intent callbackIntent = new Intent(context, callbackActivity);
432 callbackIntent.setAction(callbackAction);
433 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
434
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800435 return serviceIntent;
436 }
437
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800438 private void renameGroup(Intent intent) {
439 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
440 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
441
442 if (groupId == -1) {
443 Log.e(TAG, "Invalid arguments for renameGroup request");
444 return;
445 }
446
447 ContentValues values = new ContentValues();
448 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700449 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
450 getContentResolver().update(groupUri, values, null, null);
451
452 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
453 callbackIntent.setData(groupUri);
454 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800455 }
456
457 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800458 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800459 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800460 public static Intent createGroupDeletionIntent(Context context, long groupId) {
461 Intent serviceIntent = new Intent(context, ContactSaveService.class);
462 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800463 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800464 return serviceIntent;
465 }
466
467 private void deleteGroup(Intent intent) {
468 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
469 if (groupId == -1) {
470 Log.e(TAG, "Invalid arguments for deleteGroup request");
471 return;
472 }
473
474 getContentResolver().delete(
475 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
476 }
477
478 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700479 * Creates an intent that can be sent to this service to rename a group as
480 * well as add and remove members from the group.
481 *
482 * @param context of the application
483 * @param groupId of the group that should be modified
484 * @param newLabel is the updated name of the group (can be null if the name
485 * should not be updated)
486 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
487 * should be added to the group
488 * @param rawContactsToRemove is an array of raw contact IDs for contacts
489 * that should be removed from the group
490 * @param callbackActivity is the activity to send the callback intent to
491 * @param callbackAction is the intent action for the callback intent
492 */
493 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
494 long[] rawContactsToAdd, long[] rawContactsToRemove,
495 Class<?> callbackActivity, String callbackAction) {
496 Intent serviceIntent = new Intent(context, ContactSaveService.class);
497 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
498 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
499 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
500 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
501 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
502 rawContactsToRemove);
503
504 // Callback intent will be invoked by the service once the group is updated
505 Intent callbackIntent = new Intent(context, callbackActivity);
506 callbackIntent.setAction(callbackAction);
507 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
508
509 return serviceIntent;
510 }
511
512 private void updateGroup(Intent intent) {
513 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
514 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
515 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
516 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
517
518 if (groupId == -1) {
519 Log.e(TAG, "Invalid arguments for updateGroup request");
520 return;
521 }
522
523 final ContentResolver resolver = getContentResolver();
524 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
525
526 // Update group name if necessary
527 if (label != null) {
528 ContentValues values = new ContentValues();
529 values.put(Groups.TITLE, label);
530 getContentResolver().update(groupUri, values, null, null);
531 }
532
533 // Add new group members
534 for (long rawContactId : rawContactsToAdd) {
535 try {
536 final ArrayList<ContentProviderOperation> rawContactOperations =
537 new ArrayList<ContentProviderOperation>();
538
539 // Build an assert operation to ensure the contact is not already in the group
540 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
541 .newAssertQuery(Data.CONTENT_URI);
542 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
543 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
544 new String[] { String.valueOf(rawContactId),
545 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
546 assertBuilder.withExpectedCount(0);
547 rawContactOperations.add(assertBuilder.build());
548
549 // Build an insert operation to add the contact to the group
550 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
551 .newInsert(Data.CONTENT_URI);
552 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
553 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
554 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
555 rawContactOperations.add(insertBuilder.build());
556
557 if (DEBUG) {
558 for (ContentProviderOperation operation : rawContactOperations) {
559 Log.v(TAG, operation.toString());
560 }
561 }
562
563 // Apply batch
564 ContentProviderResult[] results = null;
565 if (!rawContactOperations.isEmpty()) {
566 results = resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
567 }
568 } catch (RemoteException e) {
569 // Something went wrong, bail without success
570 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
571 String.valueOf(rawContactId), e);
572 } catch (OperationApplicationException e) {
573 // The assert could have failed because the contact is already in the group,
574 // just continue to the next contact
575 Log.w(TAG, "Assert failed in adding raw contact ID " +
576 String.valueOf(rawContactId) + ". Already exists in group " +
577 String.valueOf(groupId), e);
578 }
579 }
580
581 // Remove group members
582 for (long rawContactId : rawContactsToRemove) {
583 // Apply the delete operation on the data row for the given raw contact's
584 // membership in the given group. If no contact matches the provided selection, then
585 // nothing will be done. Just continue to the next contact.
586 getContentResolver().delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
587 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
588 new String[] { String.valueOf(rawContactId),
589 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
590 }
591
592 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
593 callbackIntent.setData(groupUri);
594 deliverCallback(callbackIntent);
595 }
596
597 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800598 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800599 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800600 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
601 Intent serviceIntent = new Intent(context, ContactSaveService.class);
602 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
603 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
604 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
605
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800606 return serviceIntent;
607 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800608
609 private void setStarred(Intent intent) {
610 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
611 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
612 if (contactUri == null) {
613 Log.e(TAG, "Invalid arguments for setStarred request");
614 return;
615 }
616
617 final ContentValues values = new ContentValues(1);
618 values.put(Contacts.STARRED, value);
619 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800620 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800621
622 /**
623 * Creates an intent that sets the selected data item as super primary (default)
624 */
625 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
626 Intent serviceIntent = new Intent(context, ContactSaveService.class);
627 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
628 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
629 return serviceIntent;
630 }
631
632 private void setSuperPrimary(Intent intent) {
633 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
634 if (dataId == -1) {
635 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
636 return;
637 }
638
639 // Update the primary values in the data record.
640 ContentValues values = new ContentValues(1);
641 values.put(Data.IS_SUPER_PRIMARY, 1);
642 values.put(Data.IS_PRIMARY, 1);
643
644 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
645 values, null, null);
646 }
647
648 /**
649 * Creates an intent that clears the primary flag of all data items that belong to the same
650 * raw_contact as the given data item. Will only clear, if the data item was primary before
651 * this call
652 */
653 public static Intent createClearPrimaryIntent(Context context, long dataId) {
654 Intent serviceIntent = new Intent(context, ContactSaveService.class);
655 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
656 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
657 return serviceIntent;
658 }
659
660 private void clearPrimary(Intent intent) {
661 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
662 if (dataId == -1) {
663 Log.e(TAG, "Invalid arguments for clearPrimary request");
664 return;
665 }
666
667 // Update the primary values in the data record.
668 ContentValues values = new ContentValues(1);
669 values.put(Data.IS_SUPER_PRIMARY, 0);
670 values.put(Data.IS_PRIMARY, 0);
671
672 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
673 values, null, null);
674 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800675
676 /**
677 * Creates an intent that can be sent to this service to delete a contact.
678 */
679 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
680 Intent serviceIntent = new Intent(context, ContactSaveService.class);
681 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
682 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
683 return serviceIntent;
684 }
685
686 private void deleteContact(Intent intent) {
687 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
688 if (contactUri == null) {
689 Log.e(TAG, "Invalid arguments for deleteContact request");
690 return;
691 }
692
693 getContentResolver().delete(contactUri, null, null);
694 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800695
696 /**
697 * Creates an intent that can be sent to this service to join two contacts.
698 */
699 public static Intent createJoinContactsIntent(Context context, long contactId1,
700 long contactId2, boolean contactWritable,
701 Class<?> callbackActivity, String callbackAction) {
702 Intent serviceIntent = new Intent(context, ContactSaveService.class);
703 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
704 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
705 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
706 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
707
708 // Callback intent will be invoked by the service once the contacts are joined.
709 Intent callbackIntent = new Intent(context, callbackActivity);
710 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800711 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
712
713 return serviceIntent;
714 }
715
716
717 private interface JoinContactQuery {
718 String[] PROJECTION = {
719 RawContacts._ID,
720 RawContacts.CONTACT_ID,
721 RawContacts.NAME_VERIFIED,
722 RawContacts.DISPLAY_NAME_SOURCE,
723 };
724
725 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
726
727 int _ID = 0;
728 int CONTACT_ID = 1;
729 int NAME_VERIFIED = 2;
730 int DISPLAY_NAME_SOURCE = 3;
731 }
732
733 private void joinContacts(Intent intent) {
734 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
735 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
736 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
737 if (contactId1 == -1 || contactId2 == -1) {
738 Log.e(TAG, "Invalid arguments for joinContacts request");
739 return;
740 }
741
742 final ContentResolver resolver = getContentResolver();
743
744 // Load raw contact IDs for all raw contacts involved - currently edited and selected
745 // in the join UIs
746 Cursor c = resolver.query(RawContacts.CONTENT_URI,
747 JoinContactQuery.PROJECTION,
748 JoinContactQuery.SELECTION,
749 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
750
751 long rawContactIds[];
752 long verifiedNameRawContactId = -1;
753 try {
754 int maxDisplayNameSource = -1;
755 rawContactIds = new long[c.getCount()];
756 for (int i = 0; i < rawContactIds.length; i++) {
757 c.moveToPosition(i);
758 long rawContactId = c.getLong(JoinContactQuery._ID);
759 rawContactIds[i] = rawContactId;
760 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
761 if (nameSource > maxDisplayNameSource) {
762 maxDisplayNameSource = nameSource;
763 }
764 }
765
766 // Find an appropriate display name for the joined contact:
767 // if should have a higher DisplayNameSource or be the name
768 // of the original contact that we are joining with another.
769 if (writable) {
770 for (int i = 0; i < rawContactIds.length; i++) {
771 c.moveToPosition(i);
772 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
773 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
774 if (nameSource == maxDisplayNameSource
775 && (verifiedNameRawContactId == -1
776 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
777 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
778 }
779 }
780 }
781 }
782 } finally {
783 c.close();
784 }
785
786 // For each pair of raw contacts, insert an aggregation exception
787 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
788 for (int i = 0; i < rawContactIds.length; i++) {
789 for (int j = 0; j < rawContactIds.length; j++) {
790 if (i != j) {
791 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
792 }
793 }
794 }
795
796 // Mark the original contact as "name verified" to make sure that the contact
797 // display name does not change as a result of the join
798 if (verifiedNameRawContactId != -1) {
799 Builder builder = ContentProviderOperation.newUpdate(
800 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
801 builder.withValue(RawContacts.NAME_VERIFIED, 1);
802 operations.add(builder.build());
803 }
804
805 boolean success = false;
806 // Apply all aggregation exceptions as one batch
807 try {
808 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800809 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800810 success = true;
811 } catch (RemoteException e) {
812 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800813 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800814 } catch (OperationApplicationException e) {
815 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800816 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800817 }
818
819 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
820 if (success) {
821 Uri uri = RawContacts.getContactLookupUri(resolver,
822 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
823 callbackIntent.setData(uri);
824 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800825 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800826 }
827
828 /**
829 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
830 */
831 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
832 long rawContactId1, long rawContactId2) {
833 Builder builder =
834 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
835 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
836 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
837 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
838 operations.add(builder.build());
839 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800840
841 /**
842 * Shows a toast on the UI thread.
843 */
844 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800845 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -0800846
847 @Override
848 public void run() {
849 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
850 }
851 });
852 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800853
854 private void deliverCallback(final Intent callbackIntent) {
855 mMainHandler.post(new Runnable() {
856
857 @Override
858 public void run() {
859 deliverCallbackOnUiThread(callbackIntent);
860 }
861 });
862 }
863
864 void deliverCallbackOnUiThread(final Intent callbackIntent) {
865 // TODO: this assumes that if there are multiple instances of the same
866 // activity registered, the last one registered is the one waiting for
867 // the callback. Validity of this assumption needs to be verified.
868 synchronized (sListeners) {
869 for (Listener listener : sListeners) {
870 if (callbackIntent.getComponent().equals(
871 ((Activity) listener).getIntent().getComponent())) {
872 listener.onServiceCompleted(callbackIntent);
873 return;
874 }
875 }
876 }
877 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700878}