blob: fb549d26156cc23ceca2823b200b4ced89ff2290 [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;
Dave Santoro2b3f3c52011-07-26 17:35:42 -070020import com.android.contacts.model.AccountWithDataSet;
Dave Santoroc90f95e2011-09-07 17:47:15 -070021import com.android.contacts.model.EntityDelta;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080022import com.android.contacts.model.EntityDeltaList;
23import com.android.contacts.model.EntityModifier;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080024import com.google.android.collect.Lists;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070025import com.google.android.collect.Sets;
26
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080027import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070028import android.app.IntentService;
29import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080030import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070031import android.content.ContentProviderResult;
32import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080033import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070034import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080035import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070036import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080037import android.content.OperationApplicationException;
Josh Garguse692e012012-01-18 14:53:11 -080038import android.content.res.AssetFileDescriptor;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080039import android.database.Cursor;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070040import android.net.Uri;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080041import android.os.Handler;
42import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080043import android.os.Parcelable;
Josh Garguse692e012012-01-18 14:53:11 -080044import android.os.Bundle;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080045import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070046import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080047import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080048import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080049import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070050import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080051import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070052import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070053import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070054import android.provider.ContactsContract.RawContactsEntity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070055import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080056import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070057
Josh Garguse692e012012-01-18 14:53:11 -080058import java.lang.Long;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070059import java.util.ArrayList;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070060import java.util.HashSet;
61import java.util.List;
Hugo Hudsona831c0b2011-08-13 11:50:15 +010062import java.util.concurrent.CopyOnWriteArrayList;
Josh Garguse692e012012-01-18 14:53:11 -080063import java.io.File;
64import java.io.FileInputStream;
65import java.io.FileOutputStream;
66import java.io.IOException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070067
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080068/**
69 * A service responsible for saving changes to the content provider.
70 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070071public class ContactSaveService extends IntentService {
72 private static final String TAG = "ContactSaveService";
73
Katherine Kuana007e442011-07-07 09:25:34 -070074 /** Set to true in order to view logs on content provider operations */
75 private static final boolean DEBUG = false;
76
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070077 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
78
79 public static final String EXTRA_ACCOUNT_NAME = "accountName";
80 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070081 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070082 public static final String EXTRA_CONTENT_VALUES = "contentValues";
83 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
84
Dmitri Plotnikova0114142011-02-15 13:53:21 -080085 public static final String ACTION_SAVE_CONTACT = "saveContact";
86 public static final String EXTRA_CONTACT_STATE = "state";
87 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070088 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070089 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080090 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070091
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080092 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080093 public static final String ACTION_RENAME_GROUP = "renameGroup";
94 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070095 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080096 public static final String EXTRA_GROUP_ID = "groupId";
97 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070098 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
99 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800100
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800101 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800102 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800103 public static final String EXTRA_CONTACT_URI = "contactUri";
104 public static final String EXTRA_STARRED_FLAG = "starred";
105
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800106 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
107 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
108 public static final String EXTRA_DATA_ID = "dataId";
109
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800110 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
111 public static final String EXTRA_CONTACT_ID1 = "contactId1";
112 public static final String EXTRA_CONTACT_ID2 = "contactId2";
113 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
114
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700115 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
116 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
117
118 public static final String ACTION_SET_RINGTONE = "setRingtone";
119 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
120
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700121 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
122 Data.MIMETYPE,
123 Data.IS_PRIMARY,
124 Data.DATA1,
125 Data.DATA2,
126 Data.DATA3,
127 Data.DATA4,
128 Data.DATA5,
129 Data.DATA6,
130 Data.DATA7,
131 Data.DATA8,
132 Data.DATA9,
133 Data.DATA10,
134 Data.DATA11,
135 Data.DATA12,
136 Data.DATA13,
137 Data.DATA14,
138 Data.DATA15
139 );
140
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800141 private static final int PERSIST_TRIES = 3;
142
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800143 public interface Listener {
144 public void onServiceCompleted(Intent callbackIntent);
145 }
146
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100147 private static final CopyOnWriteArrayList<Listener> sListeners =
148 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800149
150 private Handler mMainHandler;
151
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700152 public ContactSaveService() {
153 super(TAG);
154 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800155 mMainHandler = new Handler(Looper.getMainLooper());
156 }
157
158 public static void registerListener(Listener listener) {
159 if (!(listener instanceof Activity)) {
160 throw new ClassCastException("Only activities can be registered to"
161 + " receive callback from " + ContactSaveService.class.getName());
162 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100163 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800164 }
165
166 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100167 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700168 }
169
170 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800171 public Object getSystemService(String name) {
172 Object service = super.getSystemService(name);
173 if (service != null) {
174 return service;
175 }
176
177 return getApplicationContext().getSystemService(name);
178 }
179
180 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700181 protected void onHandleIntent(Intent intent) {
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700182 String action = intent.getAction();
183 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
184 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800185 } else if (ACTION_SAVE_CONTACT.equals(action)) {
186 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800187 } else if (ACTION_CREATE_GROUP.equals(action)) {
188 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800189 } else if (ACTION_RENAME_GROUP.equals(action)) {
190 renameGroup(intent);
191 } else if (ACTION_DELETE_GROUP.equals(action)) {
192 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700193 } else if (ACTION_UPDATE_GROUP.equals(action)) {
194 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800195 } else if (ACTION_SET_STARRED.equals(action)) {
196 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800197 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
198 setSuperPrimary(intent);
199 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
200 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800201 } else if (ACTION_DELETE_CONTACT.equals(action)) {
202 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800203 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
204 joinContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700205 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
206 setSendToVoicemail(intent);
207 } else if (ACTION_SET_RINGTONE.equals(action)) {
208 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700209 }
210 }
211
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800212 /**
213 * Creates an intent that can be sent to this service to create a new raw contact
214 * using data presented as a set of ContentValues.
215 */
216 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700217 ArrayList<ContentValues> values, AccountWithDataSet account,
218 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800219 Intent serviceIntent = new Intent(
220 context, ContactSaveService.class);
221 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
222 if (account != null) {
223 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
224 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700225 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800226 }
227 serviceIntent.putParcelableArrayListExtra(
228 ContactSaveService.EXTRA_CONTENT_VALUES, values);
229
230 // Callback intent will be invoked by the service once the new contact is
231 // created. The service will put the URI of the new contact as "data" on
232 // the callback intent.
233 Intent callbackIntent = new Intent(context, callbackActivity);
234 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800235 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
236 return serviceIntent;
237 }
238
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700239 private void createRawContact(Intent intent) {
240 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
241 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700242 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700243 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
244 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
245
246 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
247 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
248 .withValue(RawContacts.ACCOUNT_NAME, accountName)
249 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700250 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700251 .build());
252
253 int size = valueList.size();
254 for (int i = 0; i < size; i++) {
255 ContentValues values = valueList.get(i);
256 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
257 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
258 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
259 .withValues(values)
260 .build());
261 }
262
263 ContentResolver resolver = getContentResolver();
264 ContentProviderResult[] results;
265 try {
266 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
267 } catch (Exception e) {
268 throw new RuntimeException("Failed to store new contact", e);
269 }
270
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700271 Uri rawContactUri = results[0].uri;
272 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
273
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800274 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700275 }
276
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700277 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800278 * Creates an intent that can be sent to this service to create a new raw contact
279 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800280 * This variant is more convenient to use when there is only one photo that can
281 * possibly be updated, as in the Contact Details screen.
282 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
283 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800284 */
285 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700286 String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
Josh Garguse692e012012-01-18 14:53:11 -0800287 String callbackAction, long rawContactId, String updatedPhotoPath) {
288 Bundle bundle = new Bundle();
289 bundle.putString(String.valueOf(rawContactId), updatedPhotoPath);
290 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
291 callbackActivity, callbackAction, bundle);
292 }
293
294 /**
295 * Creates an intent that can be sent to this service to create a new raw contact
296 * using data presented as a set of ContentValues.
297 * This variant is used when multiple contacts' photos may be updated, as in the
298 * Contact Editor.
299 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
300 */
301 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
302 String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
303 String callbackAction, Bundle updatedPhotos) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800304 Intent serviceIntent = new Intent(
305 context, ContactSaveService.class);
306 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
307 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700308 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Josh Garguse692e012012-01-18 14:53:11 -0800309 if (updatedPhotos != null) {
310 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
311 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800312
313 // Callback intent will be invoked by the service once the contact is
314 // saved. The service will put the URI of the new contact as "data" on
315 // the callback intent.
316 Intent callbackIntent = new Intent(context, callbackActivity);
317 callbackIntent.putExtra(saveModeExtraKey, saveMode);
318 callbackIntent.setAction(callbackAction);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800319 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
320 return serviceIntent;
321 }
322
323 private void saveContact(Intent intent) {
324 EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
325 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700326 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800327 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800328
329 // Trim any empty fields, and RawContacts, before persisting
330 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
331 EntityModifier.trimEmpty(state, accountTypes);
332
333 Uri lookupUri = null;
334
335 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800336 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800337
Josh Gargusef15c8e2012-01-30 16:42:02 -0800338 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
339 long insertedRawContactId = -1;
340
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800341 // Attempt to persist changes
342 int tries = 0;
343 while (tries++ < PERSIST_TRIES) {
344 try {
345 // Build operations and try applying
346 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700347 if (DEBUG) {
348 Log.v(TAG, "Content Provider Operations:");
349 for (ContentProviderOperation operation : diff) {
350 Log.v(TAG, operation.toString());
351 }
352 }
353
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800354 ContentProviderResult[] results = null;
355 if (!diff.isEmpty()) {
356 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
357 }
358
359 final long rawContactId = getRawContactId(state, diff, results);
360 if (rawContactId == -1) {
361 throw new IllegalStateException("Could not determine RawContact ID after save");
362 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800363 // We don't have to check to see if the value is still -1. If we reach here,
364 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
365 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700366 if (isProfile) {
367 // Since the profile supports local raw contacts, which may have been completely
368 // removed if all information was removed, we need to do a special query to
369 // get the lookup URI for the profile contact (if it still exists).
370 Cursor c = resolver.query(Profile.CONTENT_URI,
371 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
372 null, null, null);
373 try {
Erik162b7e32011-09-20 15:23:55 -0700374 if (c.moveToFirst()) {
375 final long contactId = c.getLong(0);
376 final String lookupKey = c.getString(1);
377 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
378 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700379 } finally {
380 c.close();
381 }
382 } else {
383 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
384 rawContactId);
385 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
386 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800387 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
Josh Garguse692e012012-01-18 14:53:11 -0800388
389 // We can change this back to false later, if we fail to save the contact photo.
390 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800391 break;
392
393 } catch (RemoteException e) {
394 // Something went wrong, bail without success
395 Log.e(TAG, "Problem persisting user edits", e);
396 break;
397
398 } catch (OperationApplicationException e) {
399 // Version consistency failed, re-parent change and try again
400 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
401 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
402 boolean first = true;
403 final int count = state.size();
404 for (int i = 0; i < count; i++) {
405 Long rawContactId = state.getRawContactId(i);
406 if (rawContactId != null && rawContactId != -1) {
407 if (!first) {
408 sb.append(',');
409 }
410 sb.append(rawContactId);
411 first = false;
412 }
413 }
414 sb.append(")");
415
416 if (first) {
417 throw new IllegalStateException("Version consistency failed for a new contact");
418 }
419
Dave Santoroc90f95e2011-09-07 17:47:15 -0700420 final EntityDeltaList newState = EntityDeltaList.fromQuery(
421 isProfile
422 ? RawContactsEntity.PROFILE_CONTENT_URI
423 : RawContactsEntity.CONTENT_URI,
424 resolver, sb.toString(), null, null);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800425 state = EntityDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700426
427 // Update the new state to use profile URIs if appropriate.
428 if (isProfile) {
429 for (EntityDelta delta : state) {
430 delta.setProfileQueryUri();
431 }
432 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800433 }
434 }
435
Josh Garguse692e012012-01-18 14:53:11 -0800436 // Now save any updated photos. We do this at the end to ensure that
437 // the ContactProvider already knows about newly-created contacts.
438 if (updatedPhotos != null) {
439 for (String key : updatedPhotos.keySet()) {
440 String photoFilePath = updatedPhotos.getString(key);
441 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800442
443 // If the raw-contact ID is negative, we are saving a new raw-contact;
444 // replace the bogus ID with the new one that we actually saved the contact at.
445 if (rawContactId < 0) {
446 rawContactId = insertedRawContactId;
447 if (rawContactId == -1) {
448 throw new IllegalStateException(
449 "Could not determine RawContact ID for image insertion");
450 }
451 }
452
Josh Garguse692e012012-01-18 14:53:11 -0800453 File photoFile = new File(photoFilePath);
454 if (!saveUpdatedPhoto(rawContactId, photoFile)) succeeded = false;
455 }
456 }
457
458 if (succeeded) {
459 // Mark the intent to indicate that the save was successful (even if the lookup URI
460 // is now null). For local contacts or the local profile, it's possible that the
461 // save triggered removal of the contact, so no lookup URI would exist..
462 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
463 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800464 callbackIntent.setData(lookupUri);
465
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800466 deliverCallback(callbackIntent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800467 }
468
Josh Garguse692e012012-01-18 14:53:11 -0800469 /**
470 * Save updated photo for the specified raw-contact.
471 * @return true for success, false for failure
472 */
473 private boolean saveUpdatedPhoto(long rawContactId, File photoFile) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800474 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800475 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
476 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
477
Josh Garguse692e012012-01-18 14:53:11 -0800478 try {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800479 final FileOutputStream outputStream = getContentResolver()
480 .openAssetFileDescriptor(outputUri, "rw").createOutputStream();
Josh Garguse692e012012-01-18 14:53:11 -0800481 try {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800482 final FileInputStream inputStream = new FileInputStream(photoFile);
483 try {
484 final byte[] buffer = new byte[16 * 1024];
485 int length;
486 int totalLength = 0;
487 while ((length = inputStream.read(buffer)) > 0) {
488 outputStream.write(buffer, 0, length);
489 totalLength += length;
490 }
491 Log.v(TAG, "Wrote " + totalLength + " bytes for photo " + photoFile.toString());
492 } finally {
493 inputStream.close();
494 }
495 } finally {
Josh Garguse692e012012-01-18 14:53:11 -0800496 outputStream.close();
Josh Garguse692e012012-01-18 14:53:11 -0800497 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800498 } catch (IOException e) {
499 Log.e(TAG, "Failed to write photo: " + photoFile.toString() + " because: " + e);
500 return false;
Josh Garguse692e012012-01-18 14:53:11 -0800501 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800502 return true;
Josh Garguse692e012012-01-18 14:53:11 -0800503 }
504
Josh Gargusef15c8e2012-01-30 16:42:02 -0800505 /**
506 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
507 */
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800508 private long getRawContactId(EntityDeltaList state,
509 final ArrayList<ContentProviderOperation> diff,
510 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800511 long existingRawContactId = state.findRawContactId();
512 if (existingRawContactId != -1) {
513 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800514 }
515
Josh Gargusef15c8e2012-01-30 16:42:02 -0800516 return getInsertedRawContactId(diff, results);
517 }
518
519 /**
520 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
521 */
522 private long getInsertedRawContactId(
523 final ArrayList<ContentProviderOperation> diff,
524 final ContentProviderResult[] results) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800525 final int diffSize = diff.size();
526 for (int i = 0; i < diffSize; i++) {
527 ContentProviderOperation operation = diff.get(i);
528 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
529 && operation.getUri().getEncodedPath().contains(
530 RawContacts.CONTENT_URI.getEncodedPath())) {
531 return ContentUris.parseId(results[i].uri);
532 }
533 }
534 return -1;
535 }
536
537 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700538 * Creates an intent that can be sent to this service to create a new group as
539 * well as add new members at the same time.
540 *
541 * @param context of the application
542 * @param account in which the group should be created
543 * @param label is the name of the group (cannot be null)
544 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
545 * should be added to the group
546 * @param callbackActivity is the activity to send the callback intent to
547 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700548 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700549 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Katherine Kuan717e3432011-07-13 17:03:24 -0700550 String label, long[] rawContactsToAdd, Class<?> callbackActivity,
551 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800552 Intent serviceIntent = new Intent(context, ContactSaveService.class);
553 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
554 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
555 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700556 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800557 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700558 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700559
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800560 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700561 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800562 Intent callbackIntent = new Intent(context, callbackActivity);
563 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700564 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800565
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700566 return serviceIntent;
567 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800568
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800569 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700570 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
571 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
572 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
573 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700574 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800575
576 ContentValues values = new ContentValues();
577 values.put(Groups.ACCOUNT_TYPE, accountType);
578 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700579 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800580 values.put(Groups.TITLE, label);
581
Katherine Kuan717e3432011-07-13 17:03:24 -0700582 final ContentResolver resolver = getContentResolver();
583
584 // Create the new group
585 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
586
587 // If there's no URI, then the insertion failed. Abort early because group members can't be
588 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800589 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700590 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800591 return;
592 }
593
Katherine Kuan717e3432011-07-13 17:03:24 -0700594 // Add new group members
595 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
596
597 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
598 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800599 values.clear();
600 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
601 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
602
603 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700604 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700605 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800606 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800607 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800608 }
609
610 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800611 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800612 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700613 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
614 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800615 Intent serviceIntent = new Intent(context, ContactSaveService.class);
616 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
617 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
618 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700619
620 // Callback intent will be invoked by the service once the group is renamed.
621 Intent callbackIntent = new Intent(context, callbackActivity);
622 callbackIntent.setAction(callbackAction);
623 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
624
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800625 return serviceIntent;
626 }
627
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800628 private void renameGroup(Intent intent) {
629 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
630 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
631
632 if (groupId == -1) {
633 Log.e(TAG, "Invalid arguments for renameGroup request");
634 return;
635 }
636
637 ContentValues values = new ContentValues();
638 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700639 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
640 getContentResolver().update(groupUri, values, null, null);
641
642 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
643 callbackIntent.setData(groupUri);
644 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800645 }
646
647 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800648 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800649 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800650 public static Intent createGroupDeletionIntent(Context context, long groupId) {
651 Intent serviceIntent = new Intent(context, ContactSaveService.class);
652 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800653 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800654 return serviceIntent;
655 }
656
657 private void deleteGroup(Intent intent) {
658 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
659 if (groupId == -1) {
660 Log.e(TAG, "Invalid arguments for deleteGroup request");
661 return;
662 }
663
664 getContentResolver().delete(
665 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
666 }
667
668 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700669 * Creates an intent that can be sent to this service to rename a group as
670 * well as add and remove members from the group.
671 *
672 * @param context of the application
673 * @param groupId of the group that should be modified
674 * @param newLabel is the updated name of the group (can be null if the name
675 * should not be updated)
676 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
677 * should be added to the group
678 * @param rawContactsToRemove is an array of raw contact IDs for contacts
679 * that should be removed from the group
680 * @param callbackActivity is the activity to send the callback intent to
681 * @param callbackAction is the intent action for the callback intent
682 */
683 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
684 long[] rawContactsToAdd, long[] rawContactsToRemove,
685 Class<?> callbackActivity, String callbackAction) {
686 Intent serviceIntent = new Intent(context, ContactSaveService.class);
687 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
688 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
689 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
690 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
691 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
692 rawContactsToRemove);
693
694 // Callback intent will be invoked by the service once the group is updated
695 Intent callbackIntent = new Intent(context, callbackActivity);
696 callbackIntent.setAction(callbackAction);
697 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
698
699 return serviceIntent;
700 }
701
702 private void updateGroup(Intent intent) {
703 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
704 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
705 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
706 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
707
708 if (groupId == -1) {
709 Log.e(TAG, "Invalid arguments for updateGroup request");
710 return;
711 }
712
713 final ContentResolver resolver = getContentResolver();
714 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
715
716 // Update group name if necessary
717 if (label != null) {
718 ContentValues values = new ContentValues();
719 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700720 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700721 }
722
Katherine Kuan717e3432011-07-13 17:03:24 -0700723 // Add and remove members if necessary
724 addMembersToGroup(resolver, rawContactsToAdd, groupId);
725 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
726
727 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
728 callbackIntent.setData(groupUri);
729 deliverCallback(callbackIntent);
730 }
731
732 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
733 long groupId) {
734 if (rawContactsToAdd == null) {
735 return;
736 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700737 for (long rawContactId : rawContactsToAdd) {
738 try {
739 final ArrayList<ContentProviderOperation> rawContactOperations =
740 new ArrayList<ContentProviderOperation>();
741
742 // Build an assert operation to ensure the contact is not already in the group
743 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
744 .newAssertQuery(Data.CONTENT_URI);
745 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
746 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
747 new String[] { String.valueOf(rawContactId),
748 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
749 assertBuilder.withExpectedCount(0);
750 rawContactOperations.add(assertBuilder.build());
751
752 // Build an insert operation to add the contact to the group
753 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
754 .newInsert(Data.CONTENT_URI);
755 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
756 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
757 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
758 rawContactOperations.add(insertBuilder.build());
759
760 if (DEBUG) {
761 for (ContentProviderOperation operation : rawContactOperations) {
762 Log.v(TAG, operation.toString());
763 }
764 }
765
766 // Apply batch
767 ContentProviderResult[] results = null;
768 if (!rawContactOperations.isEmpty()) {
769 results = resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
770 }
771 } catch (RemoteException e) {
772 // Something went wrong, bail without success
773 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
774 String.valueOf(rawContactId), e);
775 } catch (OperationApplicationException e) {
776 // The assert could have failed because the contact is already in the group,
777 // just continue to the next contact
778 Log.w(TAG, "Assert failed in adding raw contact ID " +
779 String.valueOf(rawContactId) + ". Already exists in group " +
780 String.valueOf(groupId), e);
781 }
782 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700783 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700784
Katherine Kuan717e3432011-07-13 17:03:24 -0700785 private void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
786 long groupId) {
787 if (rawContactsToRemove == null) {
788 return;
789 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700790 for (long rawContactId : rawContactsToRemove) {
791 // Apply the delete operation on the data row for the given raw contact's
792 // membership in the given group. If no contact matches the provided selection, then
793 // nothing will be done. Just continue to the next contact.
794 getContentResolver().delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
795 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
796 new String[] { String.valueOf(rawContactId),
797 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
798 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700799 }
800
801 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800802 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800803 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800804 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
805 Intent serviceIntent = new Intent(context, ContactSaveService.class);
806 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
807 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
808 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
809
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800810 return serviceIntent;
811 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800812
813 private void setStarred(Intent intent) {
814 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
815 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
816 if (contactUri == null) {
817 Log.e(TAG, "Invalid arguments for setStarred request");
818 return;
819 }
820
821 final ContentValues values = new ContentValues(1);
822 values.put(Contacts.STARRED, value);
823 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800824 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800825
826 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700827 * Creates an intent that can be sent to this service to set the redirect to voicemail.
828 */
829 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
830 boolean value) {
831 Intent serviceIntent = new Intent(context, ContactSaveService.class);
832 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
833 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
834 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
835
836 return serviceIntent;
837 }
838
839 private void setSendToVoicemail(Intent intent) {
840 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
841 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
842 if (contactUri == null) {
843 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
844 return;
845 }
846
847 final ContentValues values = new ContentValues(1);
848 values.put(Contacts.SEND_TO_VOICEMAIL, value);
849 getContentResolver().update(contactUri, values, null, null);
850 }
851
852 /**
853 * Creates an intent that can be sent to this service to save the contact's ringtone.
854 */
855 public static Intent createSetRingtone(Context context, Uri contactUri,
856 String value) {
857 Intent serviceIntent = new Intent(context, ContactSaveService.class);
858 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
859 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
860 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
861
862 return serviceIntent;
863 }
864
865 private void setRingtone(Intent intent) {
866 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
867 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
868 if (contactUri == null) {
869 Log.e(TAG, "Invalid arguments for setRingtone");
870 return;
871 }
872 ContentValues values = new ContentValues(1);
873 values.put(Contacts.CUSTOM_RINGTONE, value);
874 getContentResolver().update(contactUri, values, null, null);
875 }
876
877 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800878 * Creates an intent that sets the selected data item as super primary (default)
879 */
880 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
881 Intent serviceIntent = new Intent(context, ContactSaveService.class);
882 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
883 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
884 return serviceIntent;
885 }
886
887 private void setSuperPrimary(Intent intent) {
888 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
889 if (dataId == -1) {
890 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
891 return;
892 }
893
894 // Update the primary values in the data record.
895 ContentValues values = new ContentValues(1);
896 values.put(Data.IS_SUPER_PRIMARY, 1);
897 values.put(Data.IS_PRIMARY, 1);
898
899 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
900 values, null, null);
901 }
902
903 /**
904 * Creates an intent that clears the primary flag of all data items that belong to the same
905 * raw_contact as the given data item. Will only clear, if the data item was primary before
906 * this call
907 */
908 public static Intent createClearPrimaryIntent(Context context, long dataId) {
909 Intent serviceIntent = new Intent(context, ContactSaveService.class);
910 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
911 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
912 return serviceIntent;
913 }
914
915 private void clearPrimary(Intent intent) {
916 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
917 if (dataId == -1) {
918 Log.e(TAG, "Invalid arguments for clearPrimary request");
919 return;
920 }
921
922 // Update the primary values in the data record.
923 ContentValues values = new ContentValues(1);
924 values.put(Data.IS_SUPER_PRIMARY, 0);
925 values.put(Data.IS_PRIMARY, 0);
926
927 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
928 values, null, null);
929 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800930
931 /**
932 * Creates an intent that can be sent to this service to delete a contact.
933 */
934 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
935 Intent serviceIntent = new Intent(context, ContactSaveService.class);
936 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
937 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
938 return serviceIntent;
939 }
940
941 private void deleteContact(Intent intent) {
942 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
943 if (contactUri == null) {
944 Log.e(TAG, "Invalid arguments for deleteContact request");
945 return;
946 }
947
948 getContentResolver().delete(contactUri, null, null);
949 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800950
951 /**
952 * Creates an intent that can be sent to this service to join two contacts.
953 */
954 public static Intent createJoinContactsIntent(Context context, long contactId1,
955 long contactId2, boolean contactWritable,
956 Class<?> callbackActivity, String callbackAction) {
957 Intent serviceIntent = new Intent(context, ContactSaveService.class);
958 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
959 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
960 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
961 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
962
963 // Callback intent will be invoked by the service once the contacts are joined.
964 Intent callbackIntent = new Intent(context, callbackActivity);
965 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800966 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
967
968 return serviceIntent;
969 }
970
971
972 private interface JoinContactQuery {
973 String[] PROJECTION = {
974 RawContacts._ID,
975 RawContacts.CONTACT_ID,
976 RawContacts.NAME_VERIFIED,
977 RawContacts.DISPLAY_NAME_SOURCE,
978 };
979
980 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
981
982 int _ID = 0;
983 int CONTACT_ID = 1;
984 int NAME_VERIFIED = 2;
985 int DISPLAY_NAME_SOURCE = 3;
986 }
987
988 private void joinContacts(Intent intent) {
989 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
990 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
991 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
992 if (contactId1 == -1 || contactId2 == -1) {
993 Log.e(TAG, "Invalid arguments for joinContacts request");
994 return;
995 }
996
997 final ContentResolver resolver = getContentResolver();
998
999 // Load raw contact IDs for all raw contacts involved - currently edited and selected
1000 // in the join UIs
1001 Cursor c = resolver.query(RawContacts.CONTENT_URI,
1002 JoinContactQuery.PROJECTION,
1003 JoinContactQuery.SELECTION,
1004 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1005
1006 long rawContactIds[];
1007 long verifiedNameRawContactId = -1;
1008 try {
1009 int maxDisplayNameSource = -1;
1010 rawContactIds = new long[c.getCount()];
1011 for (int i = 0; i < rawContactIds.length; i++) {
1012 c.moveToPosition(i);
1013 long rawContactId = c.getLong(JoinContactQuery._ID);
1014 rawContactIds[i] = rawContactId;
1015 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1016 if (nameSource > maxDisplayNameSource) {
1017 maxDisplayNameSource = nameSource;
1018 }
1019 }
1020
1021 // Find an appropriate display name for the joined contact:
1022 // if should have a higher DisplayNameSource or be the name
1023 // of the original contact that we are joining with another.
1024 if (writable) {
1025 for (int i = 0; i < rawContactIds.length; i++) {
1026 c.moveToPosition(i);
1027 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1028 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1029 if (nameSource == maxDisplayNameSource
1030 && (verifiedNameRawContactId == -1
1031 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1032 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1033 }
1034 }
1035 }
1036 }
1037 } finally {
1038 c.close();
1039 }
1040
1041 // For each pair of raw contacts, insert an aggregation exception
1042 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1043 for (int i = 0; i < rawContactIds.length; i++) {
1044 for (int j = 0; j < rawContactIds.length; j++) {
1045 if (i != j) {
1046 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1047 }
1048 }
1049 }
1050
1051 // Mark the original contact as "name verified" to make sure that the contact
1052 // display name does not change as a result of the join
1053 if (verifiedNameRawContactId != -1) {
1054 Builder builder = ContentProviderOperation.newUpdate(
1055 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1056 builder.withValue(RawContacts.NAME_VERIFIED, 1);
1057 operations.add(builder.build());
1058 }
1059
1060 boolean success = false;
1061 // Apply all aggregation exceptions as one batch
1062 try {
1063 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001064 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001065 success = true;
1066 } catch (RemoteException e) {
1067 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001068 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001069 } catch (OperationApplicationException e) {
1070 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001071 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001072 }
1073
1074 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1075 if (success) {
1076 Uri uri = RawContacts.getContactLookupUri(resolver,
1077 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1078 callbackIntent.setData(uri);
1079 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001080 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001081 }
1082
1083 /**
1084 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1085 */
1086 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1087 long rawContactId1, long rawContactId2) {
1088 Builder builder =
1089 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1090 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1091 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1092 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1093 operations.add(builder.build());
1094 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001095
1096 /**
1097 * Shows a toast on the UI thread.
1098 */
1099 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001100 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001101
1102 @Override
1103 public void run() {
1104 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1105 }
1106 });
1107 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001108
1109 private void deliverCallback(final Intent callbackIntent) {
1110 mMainHandler.post(new Runnable() {
1111
1112 @Override
1113 public void run() {
1114 deliverCallbackOnUiThread(callbackIntent);
1115 }
1116 });
1117 }
1118
1119 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1120 // TODO: this assumes that if there are multiple instances of the same
1121 // activity registered, the last one registered is the one waiting for
1122 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001123 for (Listener listener : sListeners) {
1124 if (callbackIntent.getComponent().equals(
1125 ((Activity) listener).getIntent().getComponent())) {
1126 listener.onServiceCompleted(callbackIntent);
1127 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001128 }
1129 }
1130 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001131}