blob: 8688a478846dcc574c792a66991a9f0e0a94940a [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 Plotnikov3a6a9052011-03-02 10:14:43 -080019import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070020import android.app.IntentService;
21import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080022import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070023import android.content.ContentProviderResult;
24import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080025import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070026import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080027import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070028import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080029import android.content.OperationApplicationException;
30import android.database.Cursor;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070031import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080032import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080033import android.os.Handler;
34import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080035import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080036import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070037import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080038import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080039import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080040import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070041import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080042import android.provider.ContactsContract.Groups;
Yorke Leee8e3fb82013-09-12 17:53:31 -070043import android.provider.ContactsContract.PinnedPositions;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070044import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070045import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070046import android.provider.ContactsContract.RawContactsEntity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070047import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080048import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070049
Chiao Chengd7ca03e2012-10-24 15:14:08 -070050import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080051import com.android.contacts.common.model.AccountTypeManager;
Yorke Leecd321f62013-10-28 15:20:15 -070052import com.android.contacts.common.model.RawContactDelta;
53import com.android.contacts.common.model.RawContactDeltaList;
54import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080055import com.android.contacts.common.model.account.AccountWithDataSet;
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070056import com.android.contacts.util.CallerInfoCacheUtils;
Yorke Lee637a38e2013-09-14 08:36:33 -070057import com.android.contacts.util.ContactPhotoUtils;
58
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070059import com.google.common.collect.Lists;
60import com.google.common.collect.Sets;
61
Josh Garguse692e012012-01-18 14:53:11 -080062import java.io.File;
63import java.io.FileInputStream;
64import java.io.FileOutputStream;
65import java.io.IOException;
Yorke Lee637a38e2013-09-14 08:36:33 -070066import java.io.InputStream;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080067import java.util.ArrayList;
68import java.util.HashSet;
69import java.util.List;
70import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070071
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080072/**
73 * A service responsible for saving changes to the content provider.
74 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070075public class ContactSaveService extends IntentService {
76 private static final String TAG = "ContactSaveService";
77
Katherine Kuana007e442011-07-07 09:25:34 -070078 /** Set to true in order to view logs on content provider operations */
79 private static final boolean DEBUG = false;
80
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070081 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
82
83 public static final String EXTRA_ACCOUNT_NAME = "accountName";
84 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070085 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070086 public static final String EXTRA_CONTENT_VALUES = "contentValues";
87 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
88
Dmitri Plotnikova0114142011-02-15 13:53:21 -080089 public static final String ACTION_SAVE_CONTACT = "saveContact";
90 public static final String EXTRA_CONTACT_STATE = "state";
91 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070092 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070093 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080094 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070095
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080096 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080097 public static final String ACTION_RENAME_GROUP = "renameGroup";
98 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070099 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800100 public static final String EXTRA_GROUP_ID = "groupId";
101 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700102 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
103 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800104
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800105 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800106 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800107 public static final String EXTRA_CONTACT_URI = "contactUri";
108 public static final String EXTRA_STARRED_FLAG = "starred";
109
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800110 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
111 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
112 public static final String EXTRA_DATA_ID = "dataId";
113
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800114 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
115 public static final String EXTRA_CONTACT_ID1 = "contactId1";
116 public static final String EXTRA_CONTACT_ID2 = "contactId2";
117 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
118
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700119 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
120 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
121
122 public static final String ACTION_SET_RINGTONE = "setRingtone";
123 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
124
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700125 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
126 Data.MIMETYPE,
127 Data.IS_PRIMARY,
128 Data.DATA1,
129 Data.DATA2,
130 Data.DATA3,
131 Data.DATA4,
132 Data.DATA5,
133 Data.DATA6,
134 Data.DATA7,
135 Data.DATA8,
136 Data.DATA9,
137 Data.DATA10,
138 Data.DATA11,
139 Data.DATA12,
140 Data.DATA13,
141 Data.DATA14,
142 Data.DATA15
143 );
144
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800145 private static final int PERSIST_TRIES = 3;
146
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800147 public interface Listener {
148 public void onServiceCompleted(Intent callbackIntent);
149 }
150
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100151 private static final CopyOnWriteArrayList<Listener> sListeners =
152 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800153
154 private Handler mMainHandler;
155
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700156 public ContactSaveService() {
157 super(TAG);
158 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800159 mMainHandler = new Handler(Looper.getMainLooper());
160 }
161
162 public static void registerListener(Listener listener) {
163 if (!(listener instanceof Activity)) {
164 throw new ClassCastException("Only activities can be registered to"
165 + " receive callback from " + ContactSaveService.class.getName());
166 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100167 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800168 }
169
170 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100171 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700172 }
173
174 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800175 public Object getSystemService(String name) {
176 Object service = super.getSystemService(name);
177 if (service != null) {
178 return service;
179 }
180
181 return getApplicationContext().getSystemService(name);
182 }
183
184 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700185 protected void onHandleIntent(Intent intent) {
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700186 // Call an appropriate method. If we're sure it affects how incoming phone calls are
187 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700188 String action = intent.getAction();
189 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
190 createRawContact(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700191 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800192 } else if (ACTION_SAVE_CONTACT.equals(action)) {
193 saveContact(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700194 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800195 } else if (ACTION_CREATE_GROUP.equals(action)) {
196 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800197 } else if (ACTION_RENAME_GROUP.equals(action)) {
198 renameGroup(intent);
199 } else if (ACTION_DELETE_GROUP.equals(action)) {
200 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700201 } else if (ACTION_UPDATE_GROUP.equals(action)) {
202 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800203 } else if (ACTION_SET_STARRED.equals(action)) {
204 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800205 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
206 setSuperPrimary(intent);
207 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
208 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800209 } else if (ACTION_DELETE_CONTACT.equals(action)) {
210 deleteContact(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700211 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800212 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
213 joinContacts(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700214 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700215 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
216 setSendToVoicemail(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700217 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700218 } else if (ACTION_SET_RINGTONE.equals(action)) {
219 setRingtone(intent);
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700220 CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700221 }
222 }
223
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800224 /**
225 * Creates an intent that can be sent to this service to create a new raw contact
226 * using data presented as a set of ContentValues.
227 */
228 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700229 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700230 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800231 Intent serviceIntent = new Intent(
232 context, ContactSaveService.class);
233 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
234 if (account != null) {
235 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
236 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700237 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800238 }
239 serviceIntent.putParcelableArrayListExtra(
240 ContactSaveService.EXTRA_CONTENT_VALUES, values);
241
242 // Callback intent will be invoked by the service once the new contact is
243 // created. The service will put the URI of the new contact as "data" on
244 // the callback intent.
245 Intent callbackIntent = new Intent(context, callbackActivity);
246 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800247 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
248 return serviceIntent;
249 }
250
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700251 private void createRawContact(Intent intent) {
252 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
253 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700254 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700255 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
256 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
257
258 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
259 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
260 .withValue(RawContacts.ACCOUNT_NAME, accountName)
261 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700262 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700263 .build());
264
265 int size = valueList.size();
266 for (int i = 0; i < size; i++) {
267 ContentValues values = valueList.get(i);
268 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
269 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
270 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
271 .withValues(values)
272 .build());
273 }
274
275 ContentResolver resolver = getContentResolver();
276 ContentProviderResult[] results;
277 try {
278 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
279 } catch (Exception e) {
280 throw new RuntimeException("Failed to store new contact", e);
281 }
282
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700283 Uri rawContactUri = results[0].uri;
284 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
285
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800286 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700287 }
288
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700289 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800290 * Creates an intent that can be sent to this service to create a new raw contact
291 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800292 * This variant is more convenient to use when there is only one photo that can
293 * possibly be updated, as in the Contact Details screen.
294 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
295 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800296 */
Maurice Chu851222a2012-06-21 11:43:08 -0700297 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700298 String saveModeExtraKey, int saveMode, boolean isProfile,
299 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700300 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800301 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700302 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800303 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
304 callbackActivity, callbackAction, bundle);
305 }
306
307 /**
308 * Creates an intent that can be sent to this service to create a new raw contact
309 * using data presented as a set of ContentValues.
310 * This variant is used when multiple contacts' photos may be updated, as in the
311 * Contact Editor.
312 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
313 */
Maurice Chu851222a2012-06-21 11:43:08 -0700314 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700315 String saveModeExtraKey, int saveMode, boolean isProfile,
316 Class<? extends Activity> callbackActivity, String callbackAction,
317 Bundle updatedPhotos) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800318 Intent serviceIntent = new Intent(
319 context, ContactSaveService.class);
320 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
321 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700322 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Josh Garguse692e012012-01-18 14:53:11 -0800323 if (updatedPhotos != null) {
324 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
325 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800326
Josh Garguse5d3f892012-04-11 11:56:15 -0700327 if (callbackActivity != null) {
328 // Callback intent will be invoked by the service once the contact is
329 // saved. The service will put the URI of the new contact as "data" on
330 // the callback intent.
331 Intent callbackIntent = new Intent(context, callbackActivity);
332 callbackIntent.putExtra(saveModeExtraKey, saveMode);
333 callbackIntent.setAction(callbackAction);
334 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
335 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800336 return serviceIntent;
337 }
338
339 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700340 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700341 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800342 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800343
344 // Trim any empty fields, and RawContacts, before persisting
345 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700346 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800347
348 Uri lookupUri = null;
349
350 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800351 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800352
Josh Gargusef15c8e2012-01-30 16:42:02 -0800353 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
354 long insertedRawContactId = -1;
355
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800356 // Attempt to persist changes
357 int tries = 0;
358 while (tries++ < PERSIST_TRIES) {
359 try {
360 // Build operations and try applying
361 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700362 if (DEBUG) {
363 Log.v(TAG, "Content Provider Operations:");
364 for (ContentProviderOperation operation : diff) {
365 Log.v(TAG, operation.toString());
366 }
367 }
368
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800369 ContentProviderResult[] results = null;
370 if (!diff.isEmpty()) {
371 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
372 }
373
374 final long rawContactId = getRawContactId(state, diff, results);
375 if (rawContactId == -1) {
376 throw new IllegalStateException("Could not determine RawContact ID after save");
377 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800378 // We don't have to check to see if the value is still -1. If we reach here,
379 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
380 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700381 if (isProfile) {
382 // Since the profile supports local raw contacts, which may have been completely
383 // removed if all information was removed, we need to do a special query to
384 // get the lookup URI for the profile contact (if it still exists).
385 Cursor c = resolver.query(Profile.CONTENT_URI,
386 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
387 null, null, null);
388 try {
Erik162b7e32011-09-20 15:23:55 -0700389 if (c.moveToFirst()) {
390 final long contactId = c.getLong(0);
391 final String lookupKey = c.getString(1);
392 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
393 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700394 } finally {
395 c.close();
396 }
397 } else {
398 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
399 rawContactId);
400 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
401 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800402 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
Josh Garguse692e012012-01-18 14:53:11 -0800403
404 // We can change this back to false later, if we fail to save the contact photo.
405 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800406 break;
407
408 } catch (RemoteException e) {
409 // Something went wrong, bail without success
410 Log.e(TAG, "Problem persisting user edits", e);
411 break;
412
Jay Shrauner57fca182014-01-17 14:20:50 -0800413 } catch (IllegalArgumentException e) {
414 // This is thrown by applyBatch on malformed requests
415 Log.e(TAG, "Problem persisting user edits", e);
416 showToast(R.string.contactSavedErrorToast);
417 break;
418
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800419 } catch (OperationApplicationException e) {
420 // Version consistency failed, re-parent change and try again
421 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
422 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
423 boolean first = true;
424 final int count = state.size();
425 for (int i = 0; i < count; i++) {
426 Long rawContactId = state.getRawContactId(i);
427 if (rawContactId != null && rawContactId != -1) {
428 if (!first) {
429 sb.append(',');
430 }
431 sb.append(rawContactId);
432 first = false;
433 }
434 }
435 sb.append(")");
436
437 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800438 throw new IllegalStateException(
439 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800440 }
441
Maurice Chu851222a2012-06-21 11:43:08 -0700442 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700443 isProfile
444 ? RawContactsEntity.PROFILE_CONTENT_URI
445 : RawContactsEntity.CONTENT_URI,
446 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700447 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700448
449 // Update the new state to use profile URIs if appropriate.
450 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700451 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700452 delta.setProfileQueryUri();
453 }
454 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800455 }
456 }
457
Josh Garguse692e012012-01-18 14:53:11 -0800458 // Now save any updated photos. We do this at the end to ensure that
459 // the ContactProvider already knows about newly-created contacts.
460 if (updatedPhotos != null) {
461 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700462 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800463 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800464
465 // If the raw-contact ID is negative, we are saving a new raw-contact;
466 // replace the bogus ID with the new one that we actually saved the contact at.
467 if (rawContactId < 0) {
468 rawContactId = insertedRawContactId;
469 if (rawContactId == -1) {
470 throw new IllegalStateException(
471 "Could not determine RawContact ID for image insertion");
472 }
473 }
474
Yorke Lee637a38e2013-09-14 08:36:33 -0700475 if (!saveUpdatedPhoto(rawContactId, photoUri)) succeeded = false;
Josh Garguse692e012012-01-18 14:53:11 -0800476 }
477 }
478
Josh Garguse5d3f892012-04-11 11:56:15 -0700479 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
480 if (callbackIntent != null) {
481 if (succeeded) {
482 // Mark the intent to indicate that the save was successful (even if the lookup URI
483 // is now null). For local contacts or the local profile, it's possible that the
484 // save triggered removal of the contact, so no lookup URI would exist..
485 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
486 }
487 callbackIntent.setData(lookupUri);
488 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800489 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800490 }
491
Josh Garguse692e012012-01-18 14:53:11 -0800492 /**
493 * Save updated photo for the specified raw-contact.
494 * @return true for success, false for failure
495 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700496 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800497 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800498 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
499 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
500
Yorke Lee637a38e2013-09-14 08:36:33 -0700501 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
Josh Garguse692e012012-01-18 14:53:11 -0800502 }
503
Josh Gargusef15c8e2012-01-30 16:42:02 -0800504 /**
505 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
506 */
Maurice Chu851222a2012-06-21 11:43:08 -0700507 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800508 final ArrayList<ContentProviderOperation> diff,
509 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800510 long existingRawContactId = state.findRawContactId();
511 if (existingRawContactId != -1) {
512 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800513 }
514
Josh Gargusef15c8e2012-01-30 16:42:02 -0800515 return getInsertedRawContactId(diff, results);
516 }
517
518 /**
519 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
520 */
521 private long getInsertedRawContactId(
522 final ArrayList<ContentProviderOperation> diff,
523 final ContentProviderResult[] results) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800524 final int diffSize = diff.size();
525 for (int i = 0; i < diffSize; i++) {
526 ContentProviderOperation operation = diff.get(i);
527 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
528 && operation.getUri().getEncodedPath().contains(
529 RawContacts.CONTENT_URI.getEncodedPath())) {
530 return ContentUris.parseId(results[i].uri);
531 }
532 }
533 return -1;
534 }
535
536 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700537 * Creates an intent that can be sent to this service to create a new group as
538 * well as add new members at the same time.
539 *
540 * @param context of the application
541 * @param account in which the group should be created
542 * @param label is the name of the group (cannot be null)
543 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
544 * should be added to the group
545 * @param callbackActivity is the activity to send the callback intent to
546 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700547 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700548 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700549 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700550 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800551 Intent serviceIntent = new Intent(context, ContactSaveService.class);
552 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
553 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
554 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700555 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800556 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700557 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700558
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800559 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700560 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800561 Intent callbackIntent = new Intent(context, callbackActivity);
562 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700563 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800564
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700565 return serviceIntent;
566 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800567
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800568 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700569 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
570 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
571 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
572 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700573 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800574
575 ContentValues values = new ContentValues();
576 values.put(Groups.ACCOUNT_TYPE, accountType);
577 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700578 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800579 values.put(Groups.TITLE, label);
580
Katherine Kuan717e3432011-07-13 17:03:24 -0700581 final ContentResolver resolver = getContentResolver();
582
583 // Create the new group
584 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
585
586 // If there's no URI, then the insertion failed. Abort early because group members can't be
587 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800588 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700589 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800590 return;
591 }
592
Katherine Kuan717e3432011-07-13 17:03:24 -0700593 // Add new group members
594 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
595
596 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
597 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800598 values.clear();
599 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
600 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
601
602 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700603 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700604 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800605 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800606 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800607 }
608
609 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800610 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800611 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700612 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700613 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800614 Intent serviceIntent = new Intent(context, ContactSaveService.class);
615 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
616 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
617 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700618
619 // Callback intent will be invoked by the service once the group is renamed.
620 Intent callbackIntent = new Intent(context, callbackActivity);
621 callbackIntent.setAction(callbackAction);
622 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
623
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800624 return serviceIntent;
625 }
626
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800627 private void renameGroup(Intent intent) {
628 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
629 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
630
631 if (groupId == -1) {
632 Log.e(TAG, "Invalid arguments for renameGroup request");
633 return;
634 }
635
636 ContentValues values = new ContentValues();
637 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700638 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
639 getContentResolver().update(groupUri, values, null, null);
640
641 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
642 callbackIntent.setData(groupUri);
643 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800644 }
645
646 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800647 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800648 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800649 public static Intent createGroupDeletionIntent(Context context, long groupId) {
650 Intent serviceIntent = new Intent(context, ContactSaveService.class);
651 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800652 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800653 return serviceIntent;
654 }
655
656 private void deleteGroup(Intent intent) {
657 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
658 if (groupId == -1) {
659 Log.e(TAG, "Invalid arguments for deleteGroup request");
660 return;
661 }
662
663 getContentResolver().delete(
664 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
665 }
666
667 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700668 * Creates an intent that can be sent to this service to rename a group as
669 * well as add and remove members from the group.
670 *
671 * @param context of the application
672 * @param groupId of the group that should be modified
673 * @param newLabel is the updated name of the group (can be null if the name
674 * should not be updated)
675 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
676 * should be added to the group
677 * @param rawContactsToRemove is an array of raw contact IDs for contacts
678 * that should be removed from the group
679 * @param callbackActivity is the activity to send the callback intent to
680 * @param callbackAction is the intent action for the callback intent
681 */
682 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
683 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700684 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700685 Intent serviceIntent = new Intent(context, ContactSaveService.class);
686 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
687 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
688 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
689 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
690 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
691 rawContactsToRemove);
692
693 // Callback intent will be invoked by the service once the group is updated
694 Intent callbackIntent = new Intent(context, callbackActivity);
695 callbackIntent.setAction(callbackAction);
696 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
697
698 return serviceIntent;
699 }
700
701 private void updateGroup(Intent intent) {
702 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
703 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
704 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
705 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
706
707 if (groupId == -1) {
708 Log.e(TAG, "Invalid arguments for updateGroup request");
709 return;
710 }
711
712 final ContentResolver resolver = getContentResolver();
713 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
714
715 // Update group name if necessary
716 if (label != null) {
717 ContentValues values = new ContentValues();
718 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700719 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700720 }
721
Katherine Kuan717e3432011-07-13 17:03:24 -0700722 // Add and remove members if necessary
723 addMembersToGroup(resolver, rawContactsToAdd, groupId);
724 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
725
726 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
727 callbackIntent.setData(groupUri);
728 deliverCallback(callbackIntent);
729 }
730
Daniel Lehmann18958a22012-02-28 17:45:25 -0800731 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700732 long groupId) {
733 if (rawContactsToAdd == null) {
734 return;
735 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700736 for (long rawContactId : rawContactsToAdd) {
737 try {
738 final ArrayList<ContentProviderOperation> rawContactOperations =
739 new ArrayList<ContentProviderOperation>();
740
741 // Build an assert operation to ensure the contact is not already in the group
742 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
743 .newAssertQuery(Data.CONTENT_URI);
744 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
745 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
746 new String[] { String.valueOf(rawContactId),
747 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
748 assertBuilder.withExpectedCount(0);
749 rawContactOperations.add(assertBuilder.build());
750
751 // Build an insert operation to add the contact to the group
752 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
753 .newInsert(Data.CONTENT_URI);
754 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
755 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
756 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
757 rawContactOperations.add(insertBuilder.build());
758
759 if (DEBUG) {
760 for (ContentProviderOperation operation : rawContactOperations) {
761 Log.v(TAG, operation.toString());
762 }
763 }
764
765 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700766 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800767 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700768 }
769 } catch (RemoteException e) {
770 // Something went wrong, bail without success
771 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
772 String.valueOf(rawContactId), e);
773 } catch (OperationApplicationException e) {
774 // The assert could have failed because the contact is already in the group,
775 // just continue to the next contact
776 Log.w(TAG, "Assert failed in adding raw contact ID " +
777 String.valueOf(rawContactId) + ". Already exists in group " +
778 String.valueOf(groupId), e);
779 }
780 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700781 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700782
Daniel Lehmann18958a22012-02-28 17:45:25 -0800783 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700784 long groupId) {
785 if (rawContactsToRemove == null) {
786 return;
787 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700788 for (long rawContactId : rawContactsToRemove) {
789 // Apply the delete operation on the data row for the given raw contact's
790 // membership in the given group. If no contact matches the provided selection, then
791 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800792 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700793 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
794 new String[] { String.valueOf(rawContactId),
795 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
796 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700797 }
798
799 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800800 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800801 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800802 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
803 Intent serviceIntent = new Intent(context, ContactSaveService.class);
804 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
805 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
806 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
807
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800808 return serviceIntent;
809 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800810
811 private void setStarred(Intent intent) {
812 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
813 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
814 if (contactUri == null) {
815 Log.e(TAG, "Invalid arguments for setStarred request");
816 return;
817 }
818
819 final ContentValues values = new ContentValues(1);
820 values.put(Contacts.STARRED, value);
821 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700822
823 // Undemote the contact if necessary
824 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
825 null, null, null);
826 try {
827 if (c.moveToFirst()) {
828 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700829
830 // Don't bother undemoting if this contact is the user's profile.
831 if (id < Profile.MIN_ID) {
832 values.clear();
833 values.put(String.valueOf(id), PinnedPositions.UNDEMOTE);
834 getContentResolver().update(PinnedPositions.UPDATE_URI, values, null, null);
835 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700836 }
837 } finally {
838 c.close();
839 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800840 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800841
842 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700843 * Creates an intent that can be sent to this service to set the redirect to voicemail.
844 */
845 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
846 boolean value) {
847 Intent serviceIntent = new Intent(context, ContactSaveService.class);
848 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
849 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
850 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
851
852 return serviceIntent;
853 }
854
855 private void setSendToVoicemail(Intent intent) {
856 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
857 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
858 if (contactUri == null) {
859 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
860 return;
861 }
862
863 final ContentValues values = new ContentValues(1);
864 values.put(Contacts.SEND_TO_VOICEMAIL, value);
865 getContentResolver().update(contactUri, values, null, null);
866 }
867
868 /**
869 * Creates an intent that can be sent to this service to save the contact's ringtone.
870 */
871 public static Intent createSetRingtone(Context context, Uri contactUri,
872 String value) {
873 Intent serviceIntent = new Intent(context, ContactSaveService.class);
874 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
875 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
876 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
877
878 return serviceIntent;
879 }
880
881 private void setRingtone(Intent intent) {
882 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
883 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
884 if (contactUri == null) {
885 Log.e(TAG, "Invalid arguments for setRingtone");
886 return;
887 }
888 ContentValues values = new ContentValues(1);
889 values.put(Contacts.CUSTOM_RINGTONE, value);
890 getContentResolver().update(contactUri, values, null, null);
891 }
892
893 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800894 * Creates an intent that sets the selected data item as super primary (default)
895 */
896 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
897 Intent serviceIntent = new Intent(context, ContactSaveService.class);
898 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
899 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
900 return serviceIntent;
901 }
902
903 private void setSuperPrimary(Intent intent) {
904 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
905 if (dataId == -1) {
906 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
907 return;
908 }
909
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700910 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800911 }
912
913 /**
914 * Creates an intent that clears the primary flag of all data items that belong to the same
915 * raw_contact as the given data item. Will only clear, if the data item was primary before
916 * this call
917 */
918 public static Intent createClearPrimaryIntent(Context context, long dataId) {
919 Intent serviceIntent = new Intent(context, ContactSaveService.class);
920 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
921 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
922 return serviceIntent;
923 }
924
925 private void clearPrimary(Intent intent) {
926 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
927 if (dataId == -1) {
928 Log.e(TAG, "Invalid arguments for clearPrimary request");
929 return;
930 }
931
932 // Update the primary values in the data record.
933 ContentValues values = new ContentValues(1);
934 values.put(Data.IS_SUPER_PRIMARY, 0);
935 values.put(Data.IS_PRIMARY, 0);
936
937 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
938 values, null, null);
939 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800940
941 /**
942 * Creates an intent that can be sent to this service to delete a contact.
943 */
944 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
945 Intent serviceIntent = new Intent(context, ContactSaveService.class);
946 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
947 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
948 return serviceIntent;
949 }
950
951 private void deleteContact(Intent intent) {
952 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
953 if (contactUri == null) {
954 Log.e(TAG, "Invalid arguments for deleteContact request");
955 return;
956 }
957
958 getContentResolver().delete(contactUri, null, null);
959 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800960
961 /**
962 * Creates an intent that can be sent to this service to join two contacts.
963 */
964 public static Intent createJoinContactsIntent(Context context, long contactId1,
965 long contactId2, boolean contactWritable,
Josh Garguse5d3f892012-04-11 11:56:15 -0700966 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800967 Intent serviceIntent = new Intent(context, ContactSaveService.class);
968 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
969 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
970 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
971 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
972
973 // Callback intent will be invoked by the service once the contacts are joined.
974 Intent callbackIntent = new Intent(context, callbackActivity);
975 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800976 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
977
978 return serviceIntent;
979 }
980
981
982 private interface JoinContactQuery {
983 String[] PROJECTION = {
984 RawContacts._ID,
985 RawContacts.CONTACT_ID,
986 RawContacts.NAME_VERIFIED,
987 RawContacts.DISPLAY_NAME_SOURCE,
988 };
989
990 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
991
992 int _ID = 0;
993 int CONTACT_ID = 1;
994 int NAME_VERIFIED = 2;
995 int DISPLAY_NAME_SOURCE = 3;
996 }
997
998 private void joinContacts(Intent intent) {
999 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1000 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
1001 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
1002 if (contactId1 == -1 || contactId2 == -1) {
1003 Log.e(TAG, "Invalid arguments for joinContacts request");
1004 return;
1005 }
1006
1007 final ContentResolver resolver = getContentResolver();
1008
1009 // Load raw contact IDs for all raw contacts involved - currently edited and selected
1010 // in the join UIs
1011 Cursor c = resolver.query(RawContacts.CONTENT_URI,
1012 JoinContactQuery.PROJECTION,
1013 JoinContactQuery.SELECTION,
1014 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1015
1016 long rawContactIds[];
1017 long verifiedNameRawContactId = -1;
1018 try {
Jay Shrauner7f42b902013-01-09 14:43:09 -08001019 if (c.getCount() == 0) {
1020 return;
1021 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001022 int maxDisplayNameSource = -1;
1023 rawContactIds = new long[c.getCount()];
1024 for (int i = 0; i < rawContactIds.length; i++) {
1025 c.moveToPosition(i);
1026 long rawContactId = c.getLong(JoinContactQuery._ID);
1027 rawContactIds[i] = rawContactId;
1028 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1029 if (nameSource > maxDisplayNameSource) {
1030 maxDisplayNameSource = nameSource;
1031 }
1032 }
1033
1034 // Find an appropriate display name for the joined contact:
1035 // if should have a higher DisplayNameSource or be the name
1036 // of the original contact that we are joining with another.
1037 if (writable) {
1038 for (int i = 0; i < rawContactIds.length; i++) {
1039 c.moveToPosition(i);
1040 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1041 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1042 if (nameSource == maxDisplayNameSource
1043 && (verifiedNameRawContactId == -1
1044 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1045 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1046 }
1047 }
1048 }
1049 }
1050 } finally {
1051 c.close();
1052 }
1053
1054 // For each pair of raw contacts, insert an aggregation exception
1055 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1056 for (int i = 0; i < rawContactIds.length; i++) {
1057 for (int j = 0; j < rawContactIds.length; j++) {
1058 if (i != j) {
1059 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1060 }
1061 }
1062 }
1063
1064 // Mark the original contact as "name verified" to make sure that the contact
1065 // display name does not change as a result of the join
1066 if (verifiedNameRawContactId != -1) {
1067 Builder builder = ContentProviderOperation.newUpdate(
1068 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1069 builder.withValue(RawContacts.NAME_VERIFIED, 1);
1070 operations.add(builder.build());
1071 }
1072
1073 boolean success = false;
1074 // Apply all aggregation exceptions as one batch
1075 try {
1076 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001077 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001078 success = true;
1079 } catch (RemoteException e) {
1080 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001081 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001082 } catch (OperationApplicationException e) {
1083 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001084 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001085 }
1086
1087 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1088 if (success) {
1089 Uri uri = RawContacts.getContactLookupUri(resolver,
1090 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1091 callbackIntent.setData(uri);
1092 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001093 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001094 }
1095
1096 /**
1097 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1098 */
1099 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1100 long rawContactId1, long rawContactId2) {
1101 Builder builder =
1102 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1103 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1104 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1105 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1106 operations.add(builder.build());
1107 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001108
1109 /**
1110 * Shows a toast on the UI thread.
1111 */
1112 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001113 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001114
1115 @Override
1116 public void run() {
1117 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1118 }
1119 });
1120 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001121
1122 private void deliverCallback(final Intent callbackIntent) {
1123 mMainHandler.post(new Runnable() {
1124
1125 @Override
1126 public void run() {
1127 deliverCallbackOnUiThread(callbackIntent);
1128 }
1129 });
1130 }
1131
1132 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1133 // TODO: this assumes that if there are multiple instances of the same
1134 // activity registered, the last one registered is the one waiting for
1135 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001136 for (Listener listener : sListeners) {
1137 if (callbackIntent.getComponent().equals(
1138 ((Activity) listener).getIntent().getComponent())) {
1139 listener.onServiceCompleted(callbackIntent);
1140 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001141 }
1142 }
1143 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001144}