blob: ff76844eb17b8bc8cf6c27e0918355eb77d00d35 [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) {
438 throw new IllegalStateException("Version consistency failed for a new contact");
439 }
440
Maurice Chu851222a2012-06-21 11:43:08 -0700441 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700442 isProfile
443 ? RawContactsEntity.PROFILE_CONTENT_URI
444 : RawContactsEntity.CONTENT_URI,
445 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700446 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700447
448 // Update the new state to use profile URIs if appropriate.
449 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700450 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700451 delta.setProfileQueryUri();
452 }
453 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800454 }
455 }
456
Josh Garguse692e012012-01-18 14:53:11 -0800457 // Now save any updated photos. We do this at the end to ensure that
458 // the ContactProvider already knows about newly-created contacts.
459 if (updatedPhotos != null) {
460 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700461 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800462 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800463
464 // If the raw-contact ID is negative, we are saving a new raw-contact;
465 // replace the bogus ID with the new one that we actually saved the contact at.
466 if (rawContactId < 0) {
467 rawContactId = insertedRawContactId;
468 if (rawContactId == -1) {
469 throw new IllegalStateException(
470 "Could not determine RawContact ID for image insertion");
471 }
472 }
473
Yorke Lee637a38e2013-09-14 08:36:33 -0700474 if (!saveUpdatedPhoto(rawContactId, photoUri)) succeeded = false;
Josh Garguse692e012012-01-18 14:53:11 -0800475 }
476 }
477
Josh Garguse5d3f892012-04-11 11:56:15 -0700478 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
479 if (callbackIntent != null) {
480 if (succeeded) {
481 // Mark the intent to indicate that the save was successful (even if the lookup URI
482 // is now null). For local contacts or the local profile, it's possible that the
483 // save triggered removal of the contact, so no lookup URI would exist..
484 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
485 }
486 callbackIntent.setData(lookupUri);
487 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800488 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800489 }
490
Josh Garguse692e012012-01-18 14:53:11 -0800491 /**
492 * Save updated photo for the specified raw-contact.
493 * @return true for success, false for failure
494 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700495 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800496 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800497 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
498 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
499
Yorke Lee637a38e2013-09-14 08:36:33 -0700500 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
Josh Garguse692e012012-01-18 14:53:11 -0800501 }
502
Josh Gargusef15c8e2012-01-30 16:42:02 -0800503 /**
504 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
505 */
Maurice Chu851222a2012-06-21 11:43:08 -0700506 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800507 final ArrayList<ContentProviderOperation> diff,
508 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800509 long existingRawContactId = state.findRawContactId();
510 if (existingRawContactId != -1) {
511 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800512 }
513
Josh Gargusef15c8e2012-01-30 16:42:02 -0800514 return getInsertedRawContactId(diff, results);
515 }
516
517 /**
518 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
519 */
520 private long getInsertedRawContactId(
521 final ArrayList<ContentProviderOperation> diff,
522 final ContentProviderResult[] results) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800523 final int diffSize = diff.size();
524 for (int i = 0; i < diffSize; i++) {
525 ContentProviderOperation operation = diff.get(i);
526 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
527 && operation.getUri().getEncodedPath().contains(
528 RawContacts.CONTENT_URI.getEncodedPath())) {
529 return ContentUris.parseId(results[i].uri);
530 }
531 }
532 return -1;
533 }
534
535 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700536 * Creates an intent that can be sent to this service to create a new group as
537 * well as add new members at the same time.
538 *
539 * @param context of the application
540 * @param account in which the group should be created
541 * @param label is the name of the group (cannot be null)
542 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
543 * should be added to the group
544 * @param callbackActivity is the activity to send the callback intent to
545 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700546 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700547 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700548 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700549 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800550 Intent serviceIntent = new Intent(context, ContactSaveService.class);
551 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
552 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
553 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700554 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800555 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700556 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700557
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800558 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700559 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800560 Intent callbackIntent = new Intent(context, callbackActivity);
561 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700562 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800563
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700564 return serviceIntent;
565 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800566
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800567 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700568 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
569 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
570 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
571 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700572 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800573
574 ContentValues values = new ContentValues();
575 values.put(Groups.ACCOUNT_TYPE, accountType);
576 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700577 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800578 values.put(Groups.TITLE, label);
579
Katherine Kuan717e3432011-07-13 17:03:24 -0700580 final ContentResolver resolver = getContentResolver();
581
582 // Create the new group
583 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
584
585 // If there's no URI, then the insertion failed. Abort early because group members can't be
586 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800587 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700588 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800589 return;
590 }
591
Katherine Kuan717e3432011-07-13 17:03:24 -0700592 // Add new group members
593 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
594
595 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
596 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800597 values.clear();
598 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
599 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
600
601 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700602 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700603 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800604 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800605 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800606 }
607
608 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800609 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800610 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700611 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700612 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800613 Intent serviceIntent = new Intent(context, ContactSaveService.class);
614 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
615 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
616 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700617
618 // Callback intent will be invoked by the service once the group is renamed.
619 Intent callbackIntent = new Intent(context, callbackActivity);
620 callbackIntent.setAction(callbackAction);
621 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
622
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800623 return serviceIntent;
624 }
625
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800626 private void renameGroup(Intent intent) {
627 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
628 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
629
630 if (groupId == -1) {
631 Log.e(TAG, "Invalid arguments for renameGroup request");
632 return;
633 }
634
635 ContentValues values = new ContentValues();
636 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700637 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
638 getContentResolver().update(groupUri, values, null, null);
639
640 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
641 callbackIntent.setData(groupUri);
642 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800643 }
644
645 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800646 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800647 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800648 public static Intent createGroupDeletionIntent(Context context, long groupId) {
649 Intent serviceIntent = new Intent(context, ContactSaveService.class);
650 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800651 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800652 return serviceIntent;
653 }
654
655 private void deleteGroup(Intent intent) {
656 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
657 if (groupId == -1) {
658 Log.e(TAG, "Invalid arguments for deleteGroup request");
659 return;
660 }
661
662 getContentResolver().delete(
663 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
664 }
665
666 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700667 * Creates an intent that can be sent to this service to rename a group as
668 * well as add and remove members from the group.
669 *
670 * @param context of the application
671 * @param groupId of the group that should be modified
672 * @param newLabel is the updated name of the group (can be null if the name
673 * should not be updated)
674 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
675 * should be added to the group
676 * @param rawContactsToRemove is an array of raw contact IDs for contacts
677 * that should be removed from the group
678 * @param callbackActivity is the activity to send the callback intent to
679 * @param callbackAction is the intent action for the callback intent
680 */
681 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
682 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700683 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700684 Intent serviceIntent = new Intent(context, ContactSaveService.class);
685 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
686 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
687 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
688 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
689 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
690 rawContactsToRemove);
691
692 // Callback intent will be invoked by the service once the group is updated
693 Intent callbackIntent = new Intent(context, callbackActivity);
694 callbackIntent.setAction(callbackAction);
695 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
696
697 return serviceIntent;
698 }
699
700 private void updateGroup(Intent intent) {
701 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
702 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
703 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
704 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
705
706 if (groupId == -1) {
707 Log.e(TAG, "Invalid arguments for updateGroup request");
708 return;
709 }
710
711 final ContentResolver resolver = getContentResolver();
712 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
713
714 // Update group name if necessary
715 if (label != null) {
716 ContentValues values = new ContentValues();
717 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700718 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700719 }
720
Katherine Kuan717e3432011-07-13 17:03:24 -0700721 // Add and remove members if necessary
722 addMembersToGroup(resolver, rawContactsToAdd, groupId);
723 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
724
725 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
726 callbackIntent.setData(groupUri);
727 deliverCallback(callbackIntent);
728 }
729
Daniel Lehmann18958a22012-02-28 17:45:25 -0800730 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700731 long groupId) {
732 if (rawContactsToAdd == null) {
733 return;
734 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700735 for (long rawContactId : rawContactsToAdd) {
736 try {
737 final ArrayList<ContentProviderOperation> rawContactOperations =
738 new ArrayList<ContentProviderOperation>();
739
740 // Build an assert operation to ensure the contact is not already in the group
741 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
742 .newAssertQuery(Data.CONTENT_URI);
743 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
744 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
745 new String[] { String.valueOf(rawContactId),
746 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
747 assertBuilder.withExpectedCount(0);
748 rawContactOperations.add(assertBuilder.build());
749
750 // Build an insert operation to add the contact to the group
751 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
752 .newInsert(Data.CONTENT_URI);
753 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
754 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
755 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
756 rawContactOperations.add(insertBuilder.build());
757
758 if (DEBUG) {
759 for (ContentProviderOperation operation : rawContactOperations) {
760 Log.v(TAG, operation.toString());
761 }
762 }
763
764 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700765 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800766 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700767 }
768 } catch (RemoteException e) {
769 // Something went wrong, bail without success
770 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
771 String.valueOf(rawContactId), e);
772 } catch (OperationApplicationException e) {
773 // The assert could have failed because the contact is already in the group,
774 // just continue to the next contact
775 Log.w(TAG, "Assert failed in adding raw contact ID " +
776 String.valueOf(rawContactId) + ". Already exists in group " +
777 String.valueOf(groupId), e);
778 }
779 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700780 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700781
Daniel Lehmann18958a22012-02-28 17:45:25 -0800782 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700783 long groupId) {
784 if (rawContactsToRemove == null) {
785 return;
786 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700787 for (long rawContactId : rawContactsToRemove) {
788 // Apply the delete operation on the data row for the given raw contact's
789 // membership in the given group. If no contact matches the provided selection, then
790 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800791 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700792 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
793 new String[] { String.valueOf(rawContactId),
794 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
795 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700796 }
797
798 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800799 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800800 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800801 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
802 Intent serviceIntent = new Intent(context, ContactSaveService.class);
803 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
804 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
805 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
806
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800807 return serviceIntent;
808 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800809
810 private void setStarred(Intent intent) {
811 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
812 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
813 if (contactUri == null) {
814 Log.e(TAG, "Invalid arguments for setStarred request");
815 return;
816 }
817
818 final ContentValues values = new ContentValues(1);
819 values.put(Contacts.STARRED, value);
820 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700821
822 // Undemote the contact if necessary
823 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
824 null, null, null);
825 try {
826 if (c.moveToFirst()) {
827 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700828
829 // Don't bother undemoting if this contact is the user's profile.
830 if (id < Profile.MIN_ID) {
831 values.clear();
832 values.put(String.valueOf(id), PinnedPositions.UNDEMOTE);
833 getContentResolver().update(PinnedPositions.UPDATE_URI, values, null, null);
834 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700835 }
836 } finally {
837 c.close();
838 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800839 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800840
841 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700842 * Creates an intent that can be sent to this service to set the redirect to voicemail.
843 */
844 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
845 boolean value) {
846 Intent serviceIntent = new Intent(context, ContactSaveService.class);
847 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
848 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
849 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
850
851 return serviceIntent;
852 }
853
854 private void setSendToVoicemail(Intent intent) {
855 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
856 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
857 if (contactUri == null) {
858 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
859 return;
860 }
861
862 final ContentValues values = new ContentValues(1);
863 values.put(Contacts.SEND_TO_VOICEMAIL, value);
864 getContentResolver().update(contactUri, values, null, null);
865 }
866
867 /**
868 * Creates an intent that can be sent to this service to save the contact's ringtone.
869 */
870 public static Intent createSetRingtone(Context context, Uri contactUri,
871 String value) {
872 Intent serviceIntent = new Intent(context, ContactSaveService.class);
873 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
874 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
875 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
876
877 return serviceIntent;
878 }
879
880 private void setRingtone(Intent intent) {
881 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
882 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
883 if (contactUri == null) {
884 Log.e(TAG, "Invalid arguments for setRingtone");
885 return;
886 }
887 ContentValues values = new ContentValues(1);
888 values.put(Contacts.CUSTOM_RINGTONE, value);
889 getContentResolver().update(contactUri, values, null, null);
890 }
891
892 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800893 * Creates an intent that sets the selected data item as super primary (default)
894 */
895 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
896 Intent serviceIntent = new Intent(context, ContactSaveService.class);
897 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
898 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
899 return serviceIntent;
900 }
901
902 private void setSuperPrimary(Intent intent) {
903 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
904 if (dataId == -1) {
905 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
906 return;
907 }
908
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700909 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800910 }
911
912 /**
913 * Creates an intent that clears the primary flag of all data items that belong to the same
914 * raw_contact as the given data item. Will only clear, if the data item was primary before
915 * this call
916 */
917 public static Intent createClearPrimaryIntent(Context context, long dataId) {
918 Intent serviceIntent = new Intent(context, ContactSaveService.class);
919 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
920 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
921 return serviceIntent;
922 }
923
924 private void clearPrimary(Intent intent) {
925 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
926 if (dataId == -1) {
927 Log.e(TAG, "Invalid arguments for clearPrimary request");
928 return;
929 }
930
931 // Update the primary values in the data record.
932 ContentValues values = new ContentValues(1);
933 values.put(Data.IS_SUPER_PRIMARY, 0);
934 values.put(Data.IS_PRIMARY, 0);
935
936 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
937 values, null, null);
938 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800939
940 /**
941 * Creates an intent that can be sent to this service to delete a contact.
942 */
943 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
944 Intent serviceIntent = new Intent(context, ContactSaveService.class);
945 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
946 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
947 return serviceIntent;
948 }
949
950 private void deleteContact(Intent intent) {
951 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
952 if (contactUri == null) {
953 Log.e(TAG, "Invalid arguments for deleteContact request");
954 return;
955 }
956
957 getContentResolver().delete(contactUri, null, null);
958 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800959
960 /**
961 * Creates an intent that can be sent to this service to join two contacts.
962 */
963 public static Intent createJoinContactsIntent(Context context, long contactId1,
964 long contactId2, boolean contactWritable,
Josh Garguse5d3f892012-04-11 11:56:15 -0700965 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800966 Intent serviceIntent = new Intent(context, ContactSaveService.class);
967 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
968 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
969 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
970 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
971
972 // Callback intent will be invoked by the service once the contacts are joined.
973 Intent callbackIntent = new Intent(context, callbackActivity);
974 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800975 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
976
977 return serviceIntent;
978 }
979
980
981 private interface JoinContactQuery {
982 String[] PROJECTION = {
983 RawContacts._ID,
984 RawContacts.CONTACT_ID,
985 RawContacts.NAME_VERIFIED,
986 RawContacts.DISPLAY_NAME_SOURCE,
987 };
988
989 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
990
991 int _ID = 0;
992 int CONTACT_ID = 1;
993 int NAME_VERIFIED = 2;
994 int DISPLAY_NAME_SOURCE = 3;
995 }
996
997 private void joinContacts(Intent intent) {
998 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
999 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
1000 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
1001 if (contactId1 == -1 || contactId2 == -1) {
1002 Log.e(TAG, "Invalid arguments for joinContacts request");
1003 return;
1004 }
1005
1006 final ContentResolver resolver = getContentResolver();
1007
1008 // Load raw contact IDs for all raw contacts involved - currently edited and selected
1009 // in the join UIs
1010 Cursor c = resolver.query(RawContacts.CONTENT_URI,
1011 JoinContactQuery.PROJECTION,
1012 JoinContactQuery.SELECTION,
1013 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1014
1015 long rawContactIds[];
1016 long verifiedNameRawContactId = -1;
1017 try {
Jay Shrauner7f42b902013-01-09 14:43:09 -08001018 if (c.getCount() == 0) {
1019 return;
1020 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001021 int maxDisplayNameSource = -1;
1022 rawContactIds = new long[c.getCount()];
1023 for (int i = 0; i < rawContactIds.length; i++) {
1024 c.moveToPosition(i);
1025 long rawContactId = c.getLong(JoinContactQuery._ID);
1026 rawContactIds[i] = rawContactId;
1027 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1028 if (nameSource > maxDisplayNameSource) {
1029 maxDisplayNameSource = nameSource;
1030 }
1031 }
1032
1033 // Find an appropriate display name for the joined contact:
1034 // if should have a higher DisplayNameSource or be the name
1035 // of the original contact that we are joining with another.
1036 if (writable) {
1037 for (int i = 0; i < rawContactIds.length; i++) {
1038 c.moveToPosition(i);
1039 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1040 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1041 if (nameSource == maxDisplayNameSource
1042 && (verifiedNameRawContactId == -1
1043 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1044 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1045 }
1046 }
1047 }
1048 }
1049 } finally {
1050 c.close();
1051 }
1052
1053 // For each pair of raw contacts, insert an aggregation exception
1054 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1055 for (int i = 0; i < rawContactIds.length; i++) {
1056 for (int j = 0; j < rawContactIds.length; j++) {
1057 if (i != j) {
1058 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1059 }
1060 }
1061 }
1062
1063 // Mark the original contact as "name verified" to make sure that the contact
1064 // display name does not change as a result of the join
1065 if (verifiedNameRawContactId != -1) {
1066 Builder builder = ContentProviderOperation.newUpdate(
1067 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1068 builder.withValue(RawContacts.NAME_VERIFIED, 1);
1069 operations.add(builder.build());
1070 }
1071
1072 boolean success = false;
1073 // Apply all aggregation exceptions as one batch
1074 try {
1075 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001076 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001077 success = true;
1078 } catch (RemoteException e) {
1079 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001080 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001081 } catch (OperationApplicationException e) {
1082 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001083 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001084 }
1085
1086 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1087 if (success) {
1088 Uri uri = RawContacts.getContactLookupUri(resolver,
1089 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1090 callbackIntent.setData(uri);
1091 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001092 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001093 }
1094
1095 /**
1096 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1097 */
1098 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1099 long rawContactId1, long rawContactId2) {
1100 Builder builder =
1101 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1102 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1103 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1104 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1105 operations.add(builder.build());
1106 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001107
1108 /**
1109 * Shows a toast on the UI thread.
1110 */
1111 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001112 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001113
1114 @Override
1115 public void run() {
1116 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1117 }
1118 });
1119 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001120
1121 private void deliverCallback(final Intent callbackIntent) {
1122 mMainHandler.post(new Runnable() {
1123
1124 @Override
1125 public void run() {
1126 deliverCallbackOnUiThread(callbackIntent);
1127 }
1128 });
1129 }
1130
1131 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1132 // TODO: this assumes that if there are multiple instances of the same
1133 // activity registered, the last one registered is the one waiting for
1134 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001135 for (Listener listener : sListeners) {
1136 if (callbackIntent.getComponent().equals(
1137 ((Activity) listener).getIntent().getComponent())) {
1138 listener.onServiceCompleted(callbackIntent);
1139 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001140 }
1141 }
1142 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001143}