blob: 5dd1942fd8c46a6579c9053ff94d10d6d3d79ca1 [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.util.Iterator;
64import java.io.File;
65import java.io.FileInputStream;
66import java.io.FileOutputStream;
67import java.io.IOException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070068
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080069/**
70 * A service responsible for saving changes to the content provider.
71 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070072public class ContactSaveService extends IntentService {
73 private static final String TAG = "ContactSaveService";
74
Katherine Kuana007e442011-07-07 09:25:34 -070075 /** Set to true in order to view logs on content provider operations */
76 private static final boolean DEBUG = false;
77
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070078 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
79
80 public static final String EXTRA_ACCOUNT_NAME = "accountName";
81 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070082 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070083 public static final String EXTRA_CONTENT_VALUES = "contentValues";
84 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
85
Dmitri Plotnikova0114142011-02-15 13:53:21 -080086 public static final String ACTION_SAVE_CONTACT = "saveContact";
87 public static final String EXTRA_CONTACT_STATE = "state";
88 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070089 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070090 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080091 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070092
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080093 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080094 public static final String ACTION_RENAME_GROUP = "renameGroup";
95 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070096 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080097 public static final String EXTRA_GROUP_ID = "groupId";
98 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070099 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
100 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800101
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800102 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800103 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800104 public static final String EXTRA_CONTACT_URI = "contactUri";
105 public static final String EXTRA_STARRED_FLAG = "starred";
106
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800107 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
108 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
109 public static final String EXTRA_DATA_ID = "dataId";
110
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800111 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
112 public static final String EXTRA_CONTACT_ID1 = "contactId1";
113 public static final String EXTRA_CONTACT_ID2 = "contactId2";
114 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
115
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700116 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
117 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
118
119 public static final String ACTION_SET_RINGTONE = "setRingtone";
120 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
121
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700122 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
123 Data.MIMETYPE,
124 Data.IS_PRIMARY,
125 Data.DATA1,
126 Data.DATA2,
127 Data.DATA3,
128 Data.DATA4,
129 Data.DATA5,
130 Data.DATA6,
131 Data.DATA7,
132 Data.DATA8,
133 Data.DATA9,
134 Data.DATA10,
135 Data.DATA11,
136 Data.DATA12,
137 Data.DATA13,
138 Data.DATA14,
139 Data.DATA15
140 );
141
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800142 private static final int PERSIST_TRIES = 3;
143
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800144 public interface Listener {
145 public void onServiceCompleted(Intent callbackIntent);
146 }
147
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100148 private static final CopyOnWriteArrayList<Listener> sListeners =
149 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800150
151 private Handler mMainHandler;
152
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700153 public ContactSaveService() {
154 super(TAG);
155 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800156 mMainHandler = new Handler(Looper.getMainLooper());
157 }
158
159 public static void registerListener(Listener listener) {
160 if (!(listener instanceof Activity)) {
161 throw new ClassCastException("Only activities can be registered to"
162 + " receive callback from " + ContactSaveService.class.getName());
163 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100164 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800165 }
166
167 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100168 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700169 }
170
171 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800172 public Object getSystemService(String name) {
173 Object service = super.getSystemService(name);
174 if (service != null) {
175 return service;
176 }
177
178 return getApplicationContext().getSystemService(name);
179 }
180
181 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700182 protected void onHandleIntent(Intent intent) {
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700183 String action = intent.getAction();
184 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
185 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800186 } else if (ACTION_SAVE_CONTACT.equals(action)) {
187 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800188 } else if (ACTION_CREATE_GROUP.equals(action)) {
189 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800190 } else if (ACTION_RENAME_GROUP.equals(action)) {
191 renameGroup(intent);
192 } else if (ACTION_DELETE_GROUP.equals(action)) {
193 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700194 } else if (ACTION_UPDATE_GROUP.equals(action)) {
195 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800196 } else if (ACTION_SET_STARRED.equals(action)) {
197 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800198 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
199 setSuperPrimary(intent);
200 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
201 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800202 } else if (ACTION_DELETE_CONTACT.equals(action)) {
203 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800204 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
205 joinContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700206 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
207 setSendToVoicemail(intent);
208 } else if (ACTION_SET_RINGTONE.equals(action)) {
209 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700210 }
211 }
212
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800213 /**
214 * Creates an intent that can be sent to this service to create a new raw contact
215 * using data presented as a set of ContentValues.
216 */
217 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700218 ArrayList<ContentValues> values, AccountWithDataSet account,
219 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800220 Intent serviceIntent = new Intent(
221 context, ContactSaveService.class);
222 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
223 if (account != null) {
224 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
225 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700226 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800227 }
228 serviceIntent.putParcelableArrayListExtra(
229 ContactSaveService.EXTRA_CONTENT_VALUES, values);
230
231 // Callback intent will be invoked by the service once the new contact is
232 // created. The service will put the URI of the new contact as "data" on
233 // the callback intent.
234 Intent callbackIntent = new Intent(context, callbackActivity);
235 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800236 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
237 return serviceIntent;
238 }
239
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700240 private void createRawContact(Intent intent) {
241 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
242 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700243 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700244 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
245 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
246
247 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
248 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
249 .withValue(RawContacts.ACCOUNT_NAME, accountName)
250 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700251 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700252 .build());
253
254 int size = valueList.size();
255 for (int i = 0; i < size; i++) {
256 ContentValues values = valueList.get(i);
257 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
258 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
259 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
260 .withValues(values)
261 .build());
262 }
263
264 ContentResolver resolver = getContentResolver();
265 ContentProviderResult[] results;
266 try {
267 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
268 } catch (Exception e) {
269 throw new RuntimeException("Failed to store new contact", e);
270 }
271
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700272 Uri rawContactUri = results[0].uri;
273 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
274
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800275 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700276 }
277
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700278 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800279 * Creates an intent that can be sent to this service to create a new raw contact
280 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800281 * This variant is more convenient to use when there is only one photo that can
282 * possibly be updated, as in the Contact Details screen.
283 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
284 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800285 */
286 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700287 String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
Josh Garguse692e012012-01-18 14:53:11 -0800288 String callbackAction, long rawContactId, String updatedPhotoPath) {
289 Bundle bundle = new Bundle();
290 bundle.putString(String.valueOf(rawContactId), updatedPhotoPath);
291 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
292 callbackActivity, callbackAction, bundle);
293 }
294
295 /**
296 * Creates an intent that can be sent to this service to create a new raw contact
297 * using data presented as a set of ContentValues.
298 * This variant is used when multiple contacts' photos may be updated, as in the
299 * Contact Editor.
300 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
301 */
302 public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
303 String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
304 String callbackAction, Bundle updatedPhotos) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800305 Intent serviceIntent = new Intent(
306 context, ContactSaveService.class);
307 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
308 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700309 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Josh Garguse692e012012-01-18 14:53:11 -0800310 if (updatedPhotos != null) {
311 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
312 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800313
314 // Callback intent will be invoked by the service once the contact is
315 // saved. The service will put the URI of the new contact as "data" on
316 // the callback intent.
317 Intent callbackIntent = new Intent(context, callbackActivity);
318 callbackIntent.putExtra(saveModeExtraKey, saveMode);
319 callbackIntent.setAction(callbackAction);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800320 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
321 return serviceIntent;
322 }
323
324 private void saveContact(Intent intent) {
325 EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
326 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700327 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800328 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800329
330 // Trim any empty fields, and RawContacts, before persisting
331 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
332 EntityModifier.trimEmpty(state, accountTypes);
333
334 Uri lookupUri = null;
335
336 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800337 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800338
339 // Attempt to persist changes
340 int tries = 0;
341 while (tries++ < PERSIST_TRIES) {
342 try {
343 // Build operations and try applying
344 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700345 if (DEBUG) {
346 Log.v(TAG, "Content Provider Operations:");
347 for (ContentProviderOperation operation : diff) {
348 Log.v(TAG, operation.toString());
349 }
350 }
351
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800352 ContentProviderResult[] results = null;
353 if (!diff.isEmpty()) {
354 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
355 }
356
357 final long rawContactId = getRawContactId(state, diff, results);
358 if (rawContactId == -1) {
359 throw new IllegalStateException("Could not determine RawContact ID after save");
360 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700361 if (isProfile) {
362 // Since the profile supports local raw contacts, which may have been completely
363 // removed if all information was removed, we need to do a special query to
364 // get the lookup URI for the profile contact (if it still exists).
365 Cursor c = resolver.query(Profile.CONTENT_URI,
366 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
367 null, null, null);
368 try {
Erik162b7e32011-09-20 15:23:55 -0700369 if (c.moveToFirst()) {
370 final long contactId = c.getLong(0);
371 final String lookupKey = c.getString(1);
372 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
373 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700374 } finally {
375 c.close();
376 }
377 } else {
378 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
379 rawContactId);
380 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
381 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800382 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
Josh Garguse692e012012-01-18 14:53:11 -0800383
384 // We can change this back to false later, if we fail to save the contact photo.
385 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800386 break;
387
388 } catch (RemoteException e) {
389 // Something went wrong, bail without success
390 Log.e(TAG, "Problem persisting user edits", e);
391 break;
392
393 } catch (OperationApplicationException e) {
394 // Version consistency failed, re-parent change and try again
395 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
396 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
397 boolean first = true;
398 final int count = state.size();
399 for (int i = 0; i < count; i++) {
400 Long rawContactId = state.getRawContactId(i);
401 if (rawContactId != null && rawContactId != -1) {
402 if (!first) {
403 sb.append(',');
404 }
405 sb.append(rawContactId);
406 first = false;
407 }
408 }
409 sb.append(")");
410
411 if (first) {
412 throw new IllegalStateException("Version consistency failed for a new contact");
413 }
414
Dave Santoroc90f95e2011-09-07 17:47:15 -0700415 final EntityDeltaList newState = EntityDeltaList.fromQuery(
416 isProfile
417 ? RawContactsEntity.PROFILE_CONTENT_URI
418 : RawContactsEntity.CONTENT_URI,
419 resolver, sb.toString(), null, null);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800420 state = EntityDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700421
422 // Update the new state to use profile URIs if appropriate.
423 if (isProfile) {
424 for (EntityDelta delta : state) {
425 delta.setProfileQueryUri();
426 }
427 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800428 }
429 }
430
Josh Garguse692e012012-01-18 14:53:11 -0800431 // Now save any updated photos. We do this at the end to ensure that
432 // the ContactProvider already knows about newly-created contacts.
433 if (updatedPhotos != null) {
434 for (String key : updatedPhotos.keySet()) {
435 String photoFilePath = updatedPhotos.getString(key);
436 long rawContactId = Long.parseLong(key);
437 File photoFile = new File(photoFilePath);
438 if (!saveUpdatedPhoto(rawContactId, photoFile)) succeeded = false;
439 }
440 }
441
442 if (succeeded) {
443 // Mark the intent to indicate that the save was successful (even if the lookup URI
444 // is now null). For local contacts or the local profile, it's possible that the
445 // save triggered removal of the contact, so no lookup URI would exist..
446 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
447 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800448 callbackIntent.setData(lookupUri);
449
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800450 deliverCallback(callbackIntent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800451 }
452
Josh Garguse692e012012-01-18 14:53:11 -0800453 /**
454 * Save updated photo for the specified raw-contact.
455 * @return true for success, false for failure
456 */
457 private boolean saveUpdatedPhoto(long rawContactId, File photoFile) {
458 Uri outputUri = Uri.withAppendedPath(
459 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
460 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
461
462 FileOutputStream outputStream = null;
463 FileInputStream inputStream = null;
464 byte[] buffer = new byte[16 * 1024];
465 int length;
466 int totalLength = 0;
467 try {
468 AssetFileDescriptor fd = getContentResolver().openAssetFileDescriptor(outputUri, "rw");
469 outputStream = fd.createOutputStream();
470 inputStream = new FileInputStream(photoFile);
471 while ((length = inputStream.read(buffer)) > 0) {
472 outputStream.write(buffer, 0, length);
473 totalLength += length;
474 }
475 return true; // yay!
476 } catch(IOException e) {
477 Log.e(TAG, "Failed to write photo: " + photoFile.toString() + " because: " + e);
478 } finally {
479 Log.v(TAG, "Wrote " + totalLength + " bytes for photo " + photoFile.toString());
480 try {
481 inputStream.close();
482 } catch(IOException e) {
483 Log.e(TAG, "Failed to close photo input stream");
484 }
485 try {
486 outputStream.close();
487 } catch(IOException e) {
488 Log.e(TAG, "Failed to close photo output stream");
489 }
490 }
491 return false; // failed
492 }
493
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800494 private long getRawContactId(EntityDeltaList state,
495 final ArrayList<ContentProviderOperation> diff,
496 final ContentProviderResult[] results) {
497 long rawContactId = state.findRawContactId();
498 if (rawContactId != -1) {
499 return rawContactId;
500 }
501
502 final int diffSize = diff.size();
503 for (int i = 0; i < diffSize; i++) {
504 ContentProviderOperation operation = diff.get(i);
505 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
506 && operation.getUri().getEncodedPath().contains(
507 RawContacts.CONTENT_URI.getEncodedPath())) {
508 return ContentUris.parseId(results[i].uri);
509 }
510 }
511 return -1;
512 }
513
514 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700515 * Creates an intent that can be sent to this service to create a new group as
516 * well as add new members at the same time.
517 *
518 * @param context of the application
519 * @param account in which the group should be created
520 * @param label is the name of the group (cannot be null)
521 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
522 * should be added to the group
523 * @param callbackActivity is the activity to send the callback intent to
524 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700525 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700526 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Katherine Kuan717e3432011-07-13 17:03:24 -0700527 String label, long[] rawContactsToAdd, Class<?> callbackActivity,
528 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800529 Intent serviceIntent = new Intent(context, ContactSaveService.class);
530 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
531 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
532 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700533 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800534 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700535 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700536
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800537 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700538 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800539 Intent callbackIntent = new Intent(context, callbackActivity);
540 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700541 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800542
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700543 return serviceIntent;
544 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800545
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800546 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700547 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
548 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
549 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
550 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700551 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800552
553 ContentValues values = new ContentValues();
554 values.put(Groups.ACCOUNT_TYPE, accountType);
555 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700556 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800557 values.put(Groups.TITLE, label);
558
Katherine Kuan717e3432011-07-13 17:03:24 -0700559 final ContentResolver resolver = getContentResolver();
560
561 // Create the new group
562 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
563
564 // If there's no URI, then the insertion failed. Abort early because group members can't be
565 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800566 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700567 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800568 return;
569 }
570
Katherine Kuan717e3432011-07-13 17:03:24 -0700571 // Add new group members
572 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
573
574 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
575 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800576 values.clear();
577 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
578 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
579
580 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700581 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700582 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800583 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800584 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800585 }
586
587 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800588 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800589 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700590 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
591 Class<?> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800592 Intent serviceIntent = new Intent(context, ContactSaveService.class);
593 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
594 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
595 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700596
597 // Callback intent will be invoked by the service once the group is renamed.
598 Intent callbackIntent = new Intent(context, callbackActivity);
599 callbackIntent.setAction(callbackAction);
600 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
601
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800602 return serviceIntent;
603 }
604
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800605 private void renameGroup(Intent intent) {
606 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
607 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
608
609 if (groupId == -1) {
610 Log.e(TAG, "Invalid arguments for renameGroup request");
611 return;
612 }
613
614 ContentValues values = new ContentValues();
615 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700616 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
617 getContentResolver().update(groupUri, values, null, null);
618
619 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
620 callbackIntent.setData(groupUri);
621 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800622 }
623
624 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800625 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800626 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800627 public static Intent createGroupDeletionIntent(Context context, long groupId) {
628 Intent serviceIntent = new Intent(context, ContactSaveService.class);
629 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800630 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800631 return serviceIntent;
632 }
633
634 private void deleteGroup(Intent intent) {
635 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
636 if (groupId == -1) {
637 Log.e(TAG, "Invalid arguments for deleteGroup request");
638 return;
639 }
640
641 getContentResolver().delete(
642 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
643 }
644
645 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700646 * Creates an intent that can be sent to this service to rename a group as
647 * well as add and remove members from the group.
648 *
649 * @param context of the application
650 * @param groupId of the group that should be modified
651 * @param newLabel is the updated name of the group (can be null if the name
652 * should not be updated)
653 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
654 * should be added to the group
655 * @param rawContactsToRemove is an array of raw contact IDs for contacts
656 * that should be removed from the group
657 * @param callbackActivity is the activity to send the callback intent to
658 * @param callbackAction is the intent action for the callback intent
659 */
660 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
661 long[] rawContactsToAdd, long[] rawContactsToRemove,
662 Class<?> callbackActivity, String callbackAction) {
663 Intent serviceIntent = new Intent(context, ContactSaveService.class);
664 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
665 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
666 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
667 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
668 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
669 rawContactsToRemove);
670
671 // Callback intent will be invoked by the service once the group is updated
672 Intent callbackIntent = new Intent(context, callbackActivity);
673 callbackIntent.setAction(callbackAction);
674 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
675
676 return serviceIntent;
677 }
678
679 private void updateGroup(Intent intent) {
680 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
681 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
682 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
683 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
684
685 if (groupId == -1) {
686 Log.e(TAG, "Invalid arguments for updateGroup request");
687 return;
688 }
689
690 final ContentResolver resolver = getContentResolver();
691 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
692
693 // Update group name if necessary
694 if (label != null) {
695 ContentValues values = new ContentValues();
696 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700697 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700698 }
699
Katherine Kuan717e3432011-07-13 17:03:24 -0700700 // Add and remove members if necessary
701 addMembersToGroup(resolver, rawContactsToAdd, groupId);
702 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
703
704 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
705 callbackIntent.setData(groupUri);
706 deliverCallback(callbackIntent);
707 }
708
709 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
710 long groupId) {
711 if (rawContactsToAdd == null) {
712 return;
713 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700714 for (long rawContactId : rawContactsToAdd) {
715 try {
716 final ArrayList<ContentProviderOperation> rawContactOperations =
717 new ArrayList<ContentProviderOperation>();
718
719 // Build an assert operation to ensure the contact is not already in the group
720 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
721 .newAssertQuery(Data.CONTENT_URI);
722 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
723 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
724 new String[] { String.valueOf(rawContactId),
725 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
726 assertBuilder.withExpectedCount(0);
727 rawContactOperations.add(assertBuilder.build());
728
729 // Build an insert operation to add the contact to the group
730 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
731 .newInsert(Data.CONTENT_URI);
732 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
733 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
734 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
735 rawContactOperations.add(insertBuilder.build());
736
737 if (DEBUG) {
738 for (ContentProviderOperation operation : rawContactOperations) {
739 Log.v(TAG, operation.toString());
740 }
741 }
742
743 // Apply batch
744 ContentProviderResult[] results = null;
745 if (!rawContactOperations.isEmpty()) {
746 results = resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
747 }
748 } catch (RemoteException e) {
749 // Something went wrong, bail without success
750 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
751 String.valueOf(rawContactId), e);
752 } catch (OperationApplicationException e) {
753 // The assert could have failed because the contact is already in the group,
754 // just continue to the next contact
755 Log.w(TAG, "Assert failed in adding raw contact ID " +
756 String.valueOf(rawContactId) + ". Already exists in group " +
757 String.valueOf(groupId), e);
758 }
759 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700760 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700761
Katherine Kuan717e3432011-07-13 17:03:24 -0700762 private void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
763 long groupId) {
764 if (rawContactsToRemove == null) {
765 return;
766 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700767 for (long rawContactId : rawContactsToRemove) {
768 // Apply the delete operation on the data row for the given raw contact's
769 // membership in the given group. If no contact matches the provided selection, then
770 // nothing will be done. Just continue to the next contact.
771 getContentResolver().delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
772 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
773 new String[] { String.valueOf(rawContactId),
774 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
775 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700776 }
777
778 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800779 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800780 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800781 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
782 Intent serviceIntent = new Intent(context, ContactSaveService.class);
783 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
784 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
785 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
786
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800787 return serviceIntent;
788 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800789
790 private void setStarred(Intent intent) {
791 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
792 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
793 if (contactUri == null) {
794 Log.e(TAG, "Invalid arguments for setStarred request");
795 return;
796 }
797
798 final ContentValues values = new ContentValues(1);
799 values.put(Contacts.STARRED, value);
800 getContentResolver().update(contactUri, values, null, null);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800801 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800802
803 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700804 * Creates an intent that can be sent to this service to set the redirect to voicemail.
805 */
806 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
807 boolean value) {
808 Intent serviceIntent = new Intent(context, ContactSaveService.class);
809 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
810 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
811 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
812
813 return serviceIntent;
814 }
815
816 private void setSendToVoicemail(Intent intent) {
817 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
818 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
819 if (contactUri == null) {
820 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
821 return;
822 }
823
824 final ContentValues values = new ContentValues(1);
825 values.put(Contacts.SEND_TO_VOICEMAIL, value);
826 getContentResolver().update(contactUri, values, null, null);
827 }
828
829 /**
830 * Creates an intent that can be sent to this service to save the contact's ringtone.
831 */
832 public static Intent createSetRingtone(Context context, Uri contactUri,
833 String value) {
834 Intent serviceIntent = new Intent(context, ContactSaveService.class);
835 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
836 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
837 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
838
839 return serviceIntent;
840 }
841
842 private void setRingtone(Intent intent) {
843 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
844 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
845 if (contactUri == null) {
846 Log.e(TAG, "Invalid arguments for setRingtone");
847 return;
848 }
849 ContentValues values = new ContentValues(1);
850 values.put(Contacts.CUSTOM_RINGTONE, value);
851 getContentResolver().update(contactUri, values, null, null);
852 }
853
854 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800855 * Creates an intent that sets the selected data item as super primary (default)
856 */
857 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
858 Intent serviceIntent = new Intent(context, ContactSaveService.class);
859 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
860 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
861 return serviceIntent;
862 }
863
864 private void setSuperPrimary(Intent intent) {
865 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
866 if (dataId == -1) {
867 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
868 return;
869 }
870
871 // Update the primary values in the data record.
872 ContentValues values = new ContentValues(1);
873 values.put(Data.IS_SUPER_PRIMARY, 1);
874 values.put(Data.IS_PRIMARY, 1);
875
876 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
877 values, null, null);
878 }
879
880 /**
881 * Creates an intent that clears the primary flag of all data items that belong to the same
882 * raw_contact as the given data item. Will only clear, if the data item was primary before
883 * this call
884 */
885 public static Intent createClearPrimaryIntent(Context context, long dataId) {
886 Intent serviceIntent = new Intent(context, ContactSaveService.class);
887 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
888 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
889 return serviceIntent;
890 }
891
892 private void clearPrimary(Intent intent) {
893 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
894 if (dataId == -1) {
895 Log.e(TAG, "Invalid arguments for clearPrimary request");
896 return;
897 }
898
899 // Update the primary values in the data record.
900 ContentValues values = new ContentValues(1);
901 values.put(Data.IS_SUPER_PRIMARY, 0);
902 values.put(Data.IS_PRIMARY, 0);
903
904 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
905 values, null, null);
906 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800907
908 /**
909 * Creates an intent that can be sent to this service to delete a contact.
910 */
911 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
912 Intent serviceIntent = new Intent(context, ContactSaveService.class);
913 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
914 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
915 return serviceIntent;
916 }
917
918 private void deleteContact(Intent intent) {
919 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
920 if (contactUri == null) {
921 Log.e(TAG, "Invalid arguments for deleteContact request");
922 return;
923 }
924
925 getContentResolver().delete(contactUri, null, null);
926 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800927
928 /**
929 * Creates an intent that can be sent to this service to join two contacts.
930 */
931 public static Intent createJoinContactsIntent(Context context, long contactId1,
932 long contactId2, boolean contactWritable,
933 Class<?> callbackActivity, String callbackAction) {
934 Intent serviceIntent = new Intent(context, ContactSaveService.class);
935 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
936 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
937 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
938 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
939
940 // Callback intent will be invoked by the service once the contacts are joined.
941 Intent callbackIntent = new Intent(context, callbackActivity);
942 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800943 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
944
945 return serviceIntent;
946 }
947
948
949 private interface JoinContactQuery {
950 String[] PROJECTION = {
951 RawContacts._ID,
952 RawContacts.CONTACT_ID,
953 RawContacts.NAME_VERIFIED,
954 RawContacts.DISPLAY_NAME_SOURCE,
955 };
956
957 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
958
959 int _ID = 0;
960 int CONTACT_ID = 1;
961 int NAME_VERIFIED = 2;
962 int DISPLAY_NAME_SOURCE = 3;
963 }
964
965 private void joinContacts(Intent intent) {
966 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
967 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
968 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
969 if (contactId1 == -1 || contactId2 == -1) {
970 Log.e(TAG, "Invalid arguments for joinContacts request");
971 return;
972 }
973
974 final ContentResolver resolver = getContentResolver();
975
976 // Load raw contact IDs for all raw contacts involved - currently edited and selected
977 // in the join UIs
978 Cursor c = resolver.query(RawContacts.CONTENT_URI,
979 JoinContactQuery.PROJECTION,
980 JoinContactQuery.SELECTION,
981 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
982
983 long rawContactIds[];
984 long verifiedNameRawContactId = -1;
985 try {
986 int maxDisplayNameSource = -1;
987 rawContactIds = new long[c.getCount()];
988 for (int i = 0; i < rawContactIds.length; i++) {
989 c.moveToPosition(i);
990 long rawContactId = c.getLong(JoinContactQuery._ID);
991 rawContactIds[i] = rawContactId;
992 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
993 if (nameSource > maxDisplayNameSource) {
994 maxDisplayNameSource = nameSource;
995 }
996 }
997
998 // Find an appropriate display name for the joined contact:
999 // if should have a higher DisplayNameSource or be the name
1000 // of the original contact that we are joining with another.
1001 if (writable) {
1002 for (int i = 0; i < rawContactIds.length; i++) {
1003 c.moveToPosition(i);
1004 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1005 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1006 if (nameSource == maxDisplayNameSource
1007 && (verifiedNameRawContactId == -1
1008 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1009 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1010 }
1011 }
1012 }
1013 }
1014 } finally {
1015 c.close();
1016 }
1017
1018 // For each pair of raw contacts, insert an aggregation exception
1019 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1020 for (int i = 0; i < rawContactIds.length; i++) {
1021 for (int j = 0; j < rawContactIds.length; j++) {
1022 if (i != j) {
1023 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1024 }
1025 }
1026 }
1027
1028 // Mark the original contact as "name verified" to make sure that the contact
1029 // display name does not change as a result of the join
1030 if (verifiedNameRawContactId != -1) {
1031 Builder builder = ContentProviderOperation.newUpdate(
1032 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1033 builder.withValue(RawContacts.NAME_VERIFIED, 1);
1034 operations.add(builder.build());
1035 }
1036
1037 boolean success = false;
1038 // Apply all aggregation exceptions as one batch
1039 try {
1040 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001041 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001042 success = true;
1043 } catch (RemoteException e) {
1044 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001045 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001046 } catch (OperationApplicationException e) {
1047 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001048 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001049 }
1050
1051 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1052 if (success) {
1053 Uri uri = RawContacts.getContactLookupUri(resolver,
1054 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1055 callbackIntent.setData(uri);
1056 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001057 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001058 }
1059
1060 /**
1061 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1062 */
1063 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1064 long rawContactId1, long rawContactId2) {
1065 Builder builder =
1066 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1067 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1068 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1069 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1070 operations.add(builder.build());
1071 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001072
1073 /**
1074 * Shows a toast on the UI thread.
1075 */
1076 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001077 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001078
1079 @Override
1080 public void run() {
1081 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1082 }
1083 });
1084 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001085
1086 private void deliverCallback(final Intent callbackIntent) {
1087 mMainHandler.post(new Runnable() {
1088
1089 @Override
1090 public void run() {
1091 deliverCallbackOnUiThread(callbackIntent);
1092 }
1093 });
1094 }
1095
1096 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1097 // TODO: this assumes that if there are multiple instances of the same
1098 // activity registered, the last one registered is the one waiting for
1099 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001100 for (Listener listener : sListeners) {
1101 if (callbackIntent.getComponent().equals(
1102 ((Activity) listener).getIntent().getComponent())) {
1103 listener.onServiceCompleted(callbackIntent);
1104 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001105 }
1106 }
1107 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001108}