blob: e6861006652381c3b577fd7098ee54042dfcddbf [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);
benny.lin3a4e7a22014-01-08 10:58:08 +0800323 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
324
Josh Garguse692e012012-01-18 14:53:11 -0800325 if (updatedPhotos != null) {
326 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
327 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800328
Josh Garguse5d3f892012-04-11 11:56:15 -0700329 if (callbackActivity != null) {
330 // Callback intent will be invoked by the service once the contact is
331 // saved. The service will put the URI of the new contact as "data" on
332 // the callback intent.
333 Intent callbackIntent = new Intent(context, callbackActivity);
334 callbackIntent.putExtra(saveModeExtraKey, saveMode);
335 callbackIntent.setAction(callbackAction);
336 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
337 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800338 return serviceIntent;
339 }
340
341 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700342 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700343 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800344 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
benny.lin3a4e7a22014-01-08 10:58:08 +0800345 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800346 // Trim any empty fields, and RawContacts, before persisting
347 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700348 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800349
350 Uri lookupUri = null;
351
352 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800353 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800354
Josh Gargusef15c8e2012-01-30 16:42:02 -0800355 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
356 long insertedRawContactId = -1;
357
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800358 // Attempt to persist changes
359 int tries = 0;
360 while (tries++ < PERSIST_TRIES) {
361 try {
362 // Build operations and try applying
363 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700364 if (DEBUG) {
365 Log.v(TAG, "Content Provider Operations:");
366 for (ContentProviderOperation operation : diff) {
367 Log.v(TAG, operation.toString());
368 }
369 }
370
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800371 ContentProviderResult[] results = null;
372 if (!diff.isEmpty()) {
373 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
374 }
375
376 final long rawContactId = getRawContactId(state, diff, results);
377 if (rawContactId == -1) {
378 throw new IllegalStateException("Could not determine RawContact ID after save");
379 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800380 // We don't have to check to see if the value is still -1. If we reach here,
381 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
382 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700383 if (isProfile) {
384 // Since the profile supports local raw contacts, which may have been completely
385 // removed if all information was removed, we need to do a special query to
386 // get the lookup URI for the profile contact (if it still exists).
387 Cursor c = resolver.query(Profile.CONTENT_URI,
388 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
389 null, null, null);
390 try {
Erik162b7e32011-09-20 15:23:55 -0700391 if (c.moveToFirst()) {
392 final long contactId = c.getLong(0);
393 final String lookupKey = c.getString(1);
394 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
395 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700396 } finally {
397 c.close();
398 }
399 } else {
400 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
401 rawContactId);
402 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
403 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800404 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
Josh Garguse692e012012-01-18 14:53:11 -0800405
406 // We can change this back to false later, if we fail to save the contact photo.
407 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800408 break;
409
410 } catch (RemoteException e) {
411 // Something went wrong, bail without success
412 Log.e(TAG, "Problem persisting user edits", e);
413 break;
414
415 } catch (OperationApplicationException e) {
416 // Version consistency failed, re-parent change and try again
417 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
418 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
419 boolean first = true;
420 final int count = state.size();
421 for (int i = 0; i < count; i++) {
422 Long rawContactId = state.getRawContactId(i);
423 if (rawContactId != null && rawContactId != -1) {
424 if (!first) {
425 sb.append(',');
426 }
427 sb.append(rawContactId);
428 first = false;
429 }
430 }
431 sb.append(")");
432
433 if (first) {
434 throw new IllegalStateException("Version consistency failed for a new contact");
435 }
436
Maurice Chu851222a2012-06-21 11:43:08 -0700437 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700438 isProfile
439 ? RawContactsEntity.PROFILE_CONTENT_URI
440 : RawContactsEntity.CONTENT_URI,
441 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700442 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700443
444 // Update the new state to use profile URIs if appropriate.
445 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700446 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700447 delta.setProfileQueryUri();
448 }
449 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800450 }
451 }
452
Josh Garguse692e012012-01-18 14:53:11 -0800453 // Now save any updated photos. We do this at the end to ensure that
454 // the ContactProvider already knows about newly-created contacts.
455 if (updatedPhotos != null) {
456 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700457 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800458 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800459
460 // If the raw-contact ID is negative, we are saving a new raw-contact;
461 // replace the bogus ID with the new one that we actually saved the contact at.
462 if (rawContactId < 0) {
463 rawContactId = insertedRawContactId;
464 if (rawContactId == -1) {
465 throw new IllegalStateException(
466 "Could not determine RawContact ID for image insertion");
467 }
468 }
469
benny.lin3a4e7a22014-01-08 10:58:08 +0800470 if (!saveUpdatedPhoto(rawContactId, photoUri, saveMode)) succeeded = false;
Josh Garguse692e012012-01-18 14:53:11 -0800471 }
472 }
473
Josh Garguse5d3f892012-04-11 11:56:15 -0700474 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
475 if (callbackIntent != null) {
476 if (succeeded) {
477 // Mark the intent to indicate that the save was successful (even if the lookup URI
478 // is now null). For local contacts or the local profile, it's possible that the
479 // save triggered removal of the contact, so no lookup URI would exist..
480 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
481 }
482 callbackIntent.setData(lookupUri);
483 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800484 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800485 }
486
Josh Garguse692e012012-01-18 14:53:11 -0800487 /**
488 * Save updated photo for the specified raw-contact.
489 * @return true for success, false for failure
490 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800491 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800492 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800493 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
494 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
495
benny.lin3a4e7a22014-01-08 10:58:08 +0800496 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800497 }
498
Josh Gargusef15c8e2012-01-30 16:42:02 -0800499 /**
500 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
501 */
Maurice Chu851222a2012-06-21 11:43:08 -0700502 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800503 final ArrayList<ContentProviderOperation> diff,
504 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800505 long existingRawContactId = state.findRawContactId();
506 if (existingRawContactId != -1) {
507 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800508 }
509
Josh Gargusef15c8e2012-01-30 16:42:02 -0800510 return getInsertedRawContactId(diff, results);
511 }
512
513 /**
514 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
515 */
516 private long getInsertedRawContactId(
517 final ArrayList<ContentProviderOperation> diff,
518 final ContentProviderResult[] results) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800519 final int diffSize = diff.size();
520 for (int i = 0; i < diffSize; i++) {
521 ContentProviderOperation operation = diff.get(i);
522 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
523 && operation.getUri().getEncodedPath().contains(
524 RawContacts.CONTENT_URI.getEncodedPath())) {
525 return ContentUris.parseId(results[i].uri);
526 }
527 }
528 return -1;
529 }
530
531 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700532 * Creates an intent that can be sent to this service to create a new group as
533 * well as add new members at the same time.
534 *
535 * @param context of the application
536 * @param account in which the group should be created
537 * @param label is the name of the group (cannot be null)
538 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
539 * should be added to the group
540 * @param callbackActivity is the activity to send the callback intent to
541 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700542 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700543 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700544 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700545 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800546 Intent serviceIntent = new Intent(context, ContactSaveService.class);
547 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
548 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
549 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700550 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800551 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700552 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700553
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800554 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700555 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800556 Intent callbackIntent = new Intent(context, callbackActivity);
557 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700558 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800559
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700560 return serviceIntent;
561 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800562
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800563 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700564 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
565 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
566 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
567 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700568 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800569
570 ContentValues values = new ContentValues();
571 values.put(Groups.ACCOUNT_TYPE, accountType);
572 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700573 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800574 values.put(Groups.TITLE, label);
575
Katherine Kuan717e3432011-07-13 17:03:24 -0700576 final ContentResolver resolver = getContentResolver();
577
578 // Create the new group
579 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
580
581 // If there's no URI, then the insertion failed. Abort early because group members can't be
582 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800583 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700584 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800585 return;
586 }
587
Katherine Kuan717e3432011-07-13 17:03:24 -0700588 // Add new group members
589 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
590
591 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
592 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800593 values.clear();
594 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
595 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
596
597 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700598 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700599 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800600 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800601 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800602 }
603
604 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800605 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800606 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700607 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700608 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800609 Intent serviceIntent = new Intent(context, ContactSaveService.class);
610 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
611 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
612 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700613
614 // Callback intent will be invoked by the service once the group is renamed.
615 Intent callbackIntent = new Intent(context, callbackActivity);
616 callbackIntent.setAction(callbackAction);
617 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
618
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800619 return serviceIntent;
620 }
621
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800622 private void renameGroup(Intent intent) {
623 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
624 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
625
626 if (groupId == -1) {
627 Log.e(TAG, "Invalid arguments for renameGroup request");
628 return;
629 }
630
631 ContentValues values = new ContentValues();
632 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700633 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
634 getContentResolver().update(groupUri, values, null, null);
635
636 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
637 callbackIntent.setData(groupUri);
638 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800639 }
640
641 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800642 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800643 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800644 public static Intent createGroupDeletionIntent(Context context, long groupId) {
645 Intent serviceIntent = new Intent(context, ContactSaveService.class);
646 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800647 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800648 return serviceIntent;
649 }
650
651 private void deleteGroup(Intent intent) {
652 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
653 if (groupId == -1) {
654 Log.e(TAG, "Invalid arguments for deleteGroup request");
655 return;
656 }
657
658 getContentResolver().delete(
659 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
660 }
661
662 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700663 * Creates an intent that can be sent to this service to rename a group as
664 * well as add and remove members from the group.
665 *
666 * @param context of the application
667 * @param groupId of the group that should be modified
668 * @param newLabel is the updated name of the group (can be null if the name
669 * should not be updated)
670 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
671 * should be added to the group
672 * @param rawContactsToRemove is an array of raw contact IDs for contacts
673 * that should be removed from the group
674 * @param callbackActivity is the activity to send the callback intent to
675 * @param callbackAction is the intent action for the callback intent
676 */
677 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
678 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700679 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700680 Intent serviceIntent = new Intent(context, ContactSaveService.class);
681 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
682 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
683 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
684 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
685 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
686 rawContactsToRemove);
687
688 // Callback intent will be invoked by the service once the group is updated
689 Intent callbackIntent = new Intent(context, callbackActivity);
690 callbackIntent.setAction(callbackAction);
691 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
692
693 return serviceIntent;
694 }
695
696 private void updateGroup(Intent intent) {
697 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
698 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
699 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
700 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
701
702 if (groupId == -1) {
703 Log.e(TAG, "Invalid arguments for updateGroup request");
704 return;
705 }
706
707 final ContentResolver resolver = getContentResolver();
708 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
709
710 // Update group name if necessary
711 if (label != null) {
712 ContentValues values = new ContentValues();
713 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700714 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700715 }
716
Katherine Kuan717e3432011-07-13 17:03:24 -0700717 // Add and remove members if necessary
718 addMembersToGroup(resolver, rawContactsToAdd, groupId);
719 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
720
721 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
722 callbackIntent.setData(groupUri);
723 deliverCallback(callbackIntent);
724 }
725
Daniel Lehmann18958a22012-02-28 17:45:25 -0800726 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700727 long groupId) {
728 if (rawContactsToAdd == null) {
729 return;
730 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700731 for (long rawContactId : rawContactsToAdd) {
732 try {
733 final ArrayList<ContentProviderOperation> rawContactOperations =
734 new ArrayList<ContentProviderOperation>();
735
736 // Build an assert operation to ensure the contact is not already in the group
737 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
738 .newAssertQuery(Data.CONTENT_URI);
739 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
740 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
741 new String[] { String.valueOf(rawContactId),
742 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
743 assertBuilder.withExpectedCount(0);
744 rawContactOperations.add(assertBuilder.build());
745
746 // Build an insert operation to add the contact to the group
747 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
748 .newInsert(Data.CONTENT_URI);
749 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
750 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
751 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
752 rawContactOperations.add(insertBuilder.build());
753
754 if (DEBUG) {
755 for (ContentProviderOperation operation : rawContactOperations) {
756 Log.v(TAG, operation.toString());
757 }
758 }
759
760 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700761 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800762 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700763 }
764 } catch (RemoteException e) {
765 // Something went wrong, bail without success
766 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
767 String.valueOf(rawContactId), e);
768 } catch (OperationApplicationException e) {
769 // The assert could have failed because the contact is already in the group,
770 // just continue to the next contact
771 Log.w(TAG, "Assert failed in adding raw contact ID " +
772 String.valueOf(rawContactId) + ". Already exists in group " +
773 String.valueOf(groupId), e);
774 }
775 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700776 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700777
Daniel Lehmann18958a22012-02-28 17:45:25 -0800778 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700779 long groupId) {
780 if (rawContactsToRemove == null) {
781 return;
782 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700783 for (long rawContactId : rawContactsToRemove) {
784 // Apply the delete operation on the data row for the given raw contact's
785 // membership in the given group. If no contact matches the provided selection, then
786 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800787 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700788 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
789 new String[] { String.valueOf(rawContactId),
790 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
791 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700792 }
793
794 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800795 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800796 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800797 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
798 Intent serviceIntent = new Intent(context, ContactSaveService.class);
799 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
800 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
801 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
802
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800803 return serviceIntent;
804 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800805
806 private void setStarred(Intent intent) {
807 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
808 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
809 if (contactUri == null) {
810 Log.e(TAG, "Invalid arguments for setStarred request");
811 return;
812 }
813
814 final ContentValues values = new ContentValues(1);
815 values.put(Contacts.STARRED, value);
816 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700817
818 // Undemote the contact if necessary
819 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
820 null, null, null);
821 try {
822 if (c.moveToFirst()) {
823 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700824
825 // Don't bother undemoting if this contact is the user's profile.
826 if (id < Profile.MIN_ID) {
827 values.clear();
828 values.put(String.valueOf(id), PinnedPositions.UNDEMOTE);
829 getContentResolver().update(PinnedPositions.UPDATE_URI, values, null, null);
830 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700831 }
832 } finally {
833 c.close();
834 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800835 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800836
837 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700838 * Creates an intent that can be sent to this service to set the redirect to voicemail.
839 */
840 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
841 boolean value) {
842 Intent serviceIntent = new Intent(context, ContactSaveService.class);
843 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
844 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
845 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
846
847 return serviceIntent;
848 }
849
850 private void setSendToVoicemail(Intent intent) {
851 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
852 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
853 if (contactUri == null) {
854 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
855 return;
856 }
857
858 final ContentValues values = new ContentValues(1);
859 values.put(Contacts.SEND_TO_VOICEMAIL, value);
860 getContentResolver().update(contactUri, values, null, null);
861 }
862
863 /**
864 * Creates an intent that can be sent to this service to save the contact's ringtone.
865 */
866 public static Intent createSetRingtone(Context context, Uri contactUri,
867 String value) {
868 Intent serviceIntent = new Intent(context, ContactSaveService.class);
869 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
870 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
871 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
872
873 return serviceIntent;
874 }
875
876 private void setRingtone(Intent intent) {
877 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
878 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
879 if (contactUri == null) {
880 Log.e(TAG, "Invalid arguments for setRingtone");
881 return;
882 }
883 ContentValues values = new ContentValues(1);
884 values.put(Contacts.CUSTOM_RINGTONE, value);
885 getContentResolver().update(contactUri, values, null, null);
886 }
887
888 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800889 * Creates an intent that sets the selected data item as super primary (default)
890 */
891 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
892 Intent serviceIntent = new Intent(context, ContactSaveService.class);
893 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
894 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
895 return serviceIntent;
896 }
897
898 private void setSuperPrimary(Intent intent) {
899 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
900 if (dataId == -1) {
901 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
902 return;
903 }
904
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700905 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800906 }
907
908 /**
909 * Creates an intent that clears the primary flag of all data items that belong to the same
910 * raw_contact as the given data item. Will only clear, if the data item was primary before
911 * this call
912 */
913 public static Intent createClearPrimaryIntent(Context context, long dataId) {
914 Intent serviceIntent = new Intent(context, ContactSaveService.class);
915 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
916 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
917 return serviceIntent;
918 }
919
920 private void clearPrimary(Intent intent) {
921 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
922 if (dataId == -1) {
923 Log.e(TAG, "Invalid arguments for clearPrimary request");
924 return;
925 }
926
927 // Update the primary values in the data record.
928 ContentValues values = new ContentValues(1);
929 values.put(Data.IS_SUPER_PRIMARY, 0);
930 values.put(Data.IS_PRIMARY, 0);
931
932 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
933 values, null, null);
934 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800935
936 /**
937 * Creates an intent that can be sent to this service to delete a contact.
938 */
939 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
940 Intent serviceIntent = new Intent(context, ContactSaveService.class);
941 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
942 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
943 return serviceIntent;
944 }
945
946 private void deleteContact(Intent intent) {
947 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
948 if (contactUri == null) {
949 Log.e(TAG, "Invalid arguments for deleteContact request");
950 return;
951 }
952
953 getContentResolver().delete(contactUri, null, null);
954 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800955
956 /**
957 * Creates an intent that can be sent to this service to join two contacts.
958 */
959 public static Intent createJoinContactsIntent(Context context, long contactId1,
960 long contactId2, boolean contactWritable,
Josh Garguse5d3f892012-04-11 11:56:15 -0700961 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800962 Intent serviceIntent = new Intent(context, ContactSaveService.class);
963 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
964 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
965 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
966 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
967
968 // Callback intent will be invoked by the service once the contacts are joined.
969 Intent callbackIntent = new Intent(context, callbackActivity);
970 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800971 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
972
973 return serviceIntent;
974 }
975
976
977 private interface JoinContactQuery {
978 String[] PROJECTION = {
979 RawContacts._ID,
980 RawContacts.CONTACT_ID,
981 RawContacts.NAME_VERIFIED,
982 RawContacts.DISPLAY_NAME_SOURCE,
983 };
984
985 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
986
987 int _ID = 0;
988 int CONTACT_ID = 1;
989 int NAME_VERIFIED = 2;
990 int DISPLAY_NAME_SOURCE = 3;
991 }
992
993 private void joinContacts(Intent intent) {
994 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
995 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
996 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
997 if (contactId1 == -1 || contactId2 == -1) {
998 Log.e(TAG, "Invalid arguments for joinContacts request");
999 return;
1000 }
1001
1002 final ContentResolver resolver = getContentResolver();
1003
1004 // Load raw contact IDs for all raw contacts involved - currently edited and selected
1005 // in the join UIs
1006 Cursor c = resolver.query(RawContacts.CONTENT_URI,
1007 JoinContactQuery.PROJECTION,
1008 JoinContactQuery.SELECTION,
1009 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1010
1011 long rawContactIds[];
1012 long verifiedNameRawContactId = -1;
1013 try {
Jay Shrauner7f42b902013-01-09 14:43:09 -08001014 if (c.getCount() == 0) {
1015 return;
1016 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001017 int maxDisplayNameSource = -1;
1018 rawContactIds = new long[c.getCount()];
1019 for (int i = 0; i < rawContactIds.length; i++) {
1020 c.moveToPosition(i);
1021 long rawContactId = c.getLong(JoinContactQuery._ID);
1022 rawContactIds[i] = rawContactId;
1023 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1024 if (nameSource > maxDisplayNameSource) {
1025 maxDisplayNameSource = nameSource;
1026 }
1027 }
1028
1029 // Find an appropriate display name for the joined contact:
1030 // if should have a higher DisplayNameSource or be the name
1031 // of the original contact that we are joining with another.
1032 if (writable) {
1033 for (int i = 0; i < rawContactIds.length; i++) {
1034 c.moveToPosition(i);
1035 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1036 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1037 if (nameSource == maxDisplayNameSource
1038 && (verifiedNameRawContactId == -1
1039 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1040 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1041 }
1042 }
1043 }
1044 }
1045 } finally {
1046 c.close();
1047 }
1048
1049 // For each pair of raw contacts, insert an aggregation exception
1050 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1051 for (int i = 0; i < rawContactIds.length; i++) {
1052 for (int j = 0; j < rawContactIds.length; j++) {
1053 if (i != j) {
1054 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1055 }
1056 }
1057 }
1058
1059 // Mark the original contact as "name verified" to make sure that the contact
1060 // display name does not change as a result of the join
1061 if (verifiedNameRawContactId != -1) {
1062 Builder builder = ContentProviderOperation.newUpdate(
1063 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1064 builder.withValue(RawContacts.NAME_VERIFIED, 1);
1065 operations.add(builder.build());
1066 }
1067
1068 boolean success = false;
1069 // Apply all aggregation exceptions as one batch
1070 try {
1071 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001072 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001073 success = true;
1074 } catch (RemoteException e) {
1075 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001076 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001077 } catch (OperationApplicationException e) {
1078 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001079 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001080 }
1081
1082 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1083 if (success) {
1084 Uri uri = RawContacts.getContactLookupUri(resolver,
1085 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1086 callbackIntent.setData(uri);
1087 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001088 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001089 }
1090
1091 /**
1092 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1093 */
1094 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1095 long rawContactId1, long rawContactId2) {
1096 Builder builder =
1097 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1098 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1099 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1100 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1101 operations.add(builder.build());
1102 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001103
1104 /**
1105 * Shows a toast on the UI thread.
1106 */
1107 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001108 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001109
1110 @Override
1111 public void run() {
1112 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1113 }
1114 });
1115 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001116
1117 private void deliverCallback(final Intent callbackIntent) {
1118 mMainHandler.post(new Runnable() {
1119
1120 @Override
1121 public void run() {
1122 deliverCallbackOnUiThread(callbackIntent);
1123 }
1124 });
1125 }
1126
1127 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1128 // TODO: this assumes that if there are multiple instances of the same
1129 // activity registered, the last one registered is the one waiting for
1130 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001131 for (Listener listener : sListeners) {
1132 if (callbackIntent.getComponent().equals(
1133 ((Activity) listener).getIntent().getComponent())) {
1134 listener.onServiceCompleted(callbackIntent);
1135 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001136 }
1137 }
1138 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001139}