blob: aed0f347f2a8e81a17b3fef98a49c463a569c3a6 [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;
Yorke Lee637a38e2013-09-14 08:36:33 -070056import com.android.contacts.util.ContactPhotoUtils;
57
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070058import com.google.common.collect.Lists;
59import com.google.common.collect.Sets;
60
Josh Garguse692e012012-01-18 14:53:11 -080061import java.io.File;
62import java.io.FileInputStream;
63import java.io.FileOutputStream;
64import java.io.IOException;
Yorke Lee637a38e2013-09-14 08:36:33 -070065import java.io.InputStream;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080066import java.util.ArrayList;
67import java.util.HashSet;
68import java.util.List;
69import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070070
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080071/**
72 * A service responsible for saving changes to the content provider.
73 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070074public class ContactSaveService extends IntentService {
75 private static final String TAG = "ContactSaveService";
76
Katherine Kuana007e442011-07-07 09:25:34 -070077 /** Set to true in order to view logs on content provider operations */
78 private static final boolean DEBUG = false;
79
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070080 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
81
82 public static final String EXTRA_ACCOUNT_NAME = "accountName";
83 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070084 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070085 public static final String EXTRA_CONTENT_VALUES = "contentValues";
86 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
87
Dmitri Plotnikova0114142011-02-15 13:53:21 -080088 public static final String ACTION_SAVE_CONTACT = "saveContact";
89 public static final String EXTRA_CONTACT_STATE = "state";
90 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070091 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070092 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080093 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070094
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080095 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080096 public static final String ACTION_RENAME_GROUP = "renameGroup";
97 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070098 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080099 public static final String EXTRA_GROUP_ID = "groupId";
100 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700101 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
102 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800103
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800104 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800105 public static final String ACTION_DELETE_CONTACT = "delete";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800106 public static final String EXTRA_CONTACT_URI = "contactUri";
107 public static final String EXTRA_STARRED_FLAG = "starred";
108
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800109 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
110 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
111 public static final String EXTRA_DATA_ID = "dataId";
112
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800113 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
114 public static final String EXTRA_CONTACT_ID1 = "contactId1";
115 public static final String EXTRA_CONTACT_ID2 = "contactId2";
116 public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
117
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700118 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
119 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
120
121 public static final String ACTION_SET_RINGTONE = "setRingtone";
122 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
123
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700124 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
125 Data.MIMETYPE,
126 Data.IS_PRIMARY,
127 Data.DATA1,
128 Data.DATA2,
129 Data.DATA3,
130 Data.DATA4,
131 Data.DATA5,
132 Data.DATA6,
133 Data.DATA7,
134 Data.DATA8,
135 Data.DATA9,
136 Data.DATA10,
137 Data.DATA11,
138 Data.DATA12,
139 Data.DATA13,
140 Data.DATA14,
141 Data.DATA15
142 );
143
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800144 private static final int PERSIST_TRIES = 3;
145
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800146 public interface Listener {
147 public void onServiceCompleted(Intent callbackIntent);
148 }
149
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100150 private static final CopyOnWriteArrayList<Listener> sListeners =
151 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800152
153 private Handler mMainHandler;
154
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700155 public ContactSaveService() {
156 super(TAG);
157 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800158 mMainHandler = new Handler(Looper.getMainLooper());
159 }
160
161 public static void registerListener(Listener listener) {
162 if (!(listener instanceof Activity)) {
163 throw new ClassCastException("Only activities can be registered to"
164 + " receive callback from " + ContactSaveService.class.getName());
165 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100166 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800167 }
168
169 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100170 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700171 }
172
173 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800174 public Object getSystemService(String name) {
175 Object service = super.getSystemService(name);
176 if (service != null) {
177 return service;
178 }
179
180 return getApplicationContext().getSystemService(name);
181 }
182
183 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700184 protected void onHandleIntent(Intent intent) {
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700185 // Call an appropriate method. If we're sure it affects how incoming phone calls are
186 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700187 String action = intent.getAction();
188 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
189 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800190 } else if (ACTION_SAVE_CONTACT.equals(action)) {
191 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800192 } else if (ACTION_CREATE_GROUP.equals(action)) {
193 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800194 } else if (ACTION_RENAME_GROUP.equals(action)) {
195 renameGroup(intent);
196 } else if (ACTION_DELETE_GROUP.equals(action)) {
197 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700198 } else if (ACTION_UPDATE_GROUP.equals(action)) {
199 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800200 } else if (ACTION_SET_STARRED.equals(action)) {
201 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800202 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
203 setSuperPrimary(intent);
204 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
205 clearPrimary(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800206 } else if (ACTION_DELETE_CONTACT.equals(action)) {
207 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800208 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
209 joinContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700210 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
211 setSendToVoicemail(intent);
212 } else if (ACTION_SET_RINGTONE.equals(action)) {
213 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700214 }
215 }
216
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800217 /**
218 * Creates an intent that can be sent to this service to create a new raw contact
219 * using data presented as a set of ContentValues.
220 */
221 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700222 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700223 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800224 Intent serviceIntent = new Intent(
225 context, ContactSaveService.class);
226 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
227 if (account != null) {
228 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
229 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700230 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800231 }
232 serviceIntent.putParcelableArrayListExtra(
233 ContactSaveService.EXTRA_CONTENT_VALUES, values);
234
235 // Callback intent will be invoked by the service once the new contact is
236 // created. The service will put the URI of the new contact as "data" on
237 // the callback intent.
238 Intent callbackIntent = new Intent(context, callbackActivity);
239 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800240 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
241 return serviceIntent;
242 }
243
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700244 private void createRawContact(Intent intent) {
245 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
246 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700247 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700248 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
249 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
250
251 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
252 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
253 .withValue(RawContacts.ACCOUNT_NAME, accountName)
254 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700255 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700256 .build());
257
258 int size = valueList.size();
259 for (int i = 0; i < size; i++) {
260 ContentValues values = valueList.get(i);
261 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
262 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
263 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
264 .withValues(values)
265 .build());
266 }
267
268 ContentResolver resolver = getContentResolver();
269 ContentProviderResult[] results;
270 try {
271 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
272 } catch (Exception e) {
273 throw new RuntimeException("Failed to store new contact", e);
274 }
275
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700276 Uri rawContactUri = results[0].uri;
277 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
278
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800279 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700280 }
281
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700282 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800283 * Creates an intent that can be sent to this service to create a new raw contact
284 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800285 * This variant is more convenient to use when there is only one photo that can
286 * possibly be updated, as in the Contact Details screen.
287 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
288 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800289 */
Maurice Chu851222a2012-06-21 11:43:08 -0700290 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700291 String saveModeExtraKey, int saveMode, boolean isProfile,
292 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700293 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800294 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700295 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800296 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
297 callbackActivity, callbackAction, bundle);
298 }
299
300 /**
301 * Creates an intent that can be sent to this service to create a new raw contact
302 * using data presented as a set of ContentValues.
303 * This variant is used when multiple contacts' photos may be updated, as in the
304 * Contact Editor.
305 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
306 */
Maurice Chu851222a2012-06-21 11:43:08 -0700307 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700308 String saveModeExtraKey, int saveMode, boolean isProfile,
309 Class<? extends Activity> callbackActivity, String callbackAction,
310 Bundle updatedPhotos) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800311 Intent serviceIntent = new Intent(
312 context, ContactSaveService.class);
313 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
314 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700315 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Josh Garguse692e012012-01-18 14:53:11 -0800316 if (updatedPhotos != null) {
317 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
318 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800319
Josh Garguse5d3f892012-04-11 11:56:15 -0700320 if (callbackActivity != null) {
321 // Callback intent will be invoked by the service once the contact is
322 // saved. The service will put the URI of the new contact as "data" on
323 // the callback intent.
324 Intent callbackIntent = new Intent(context, callbackActivity);
325 callbackIntent.putExtra(saveModeExtraKey, saveMode);
326 callbackIntent.setAction(callbackAction);
327 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
328 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800329 return serviceIntent;
330 }
331
332 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700333 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700334 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800335 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800336
337 // Trim any empty fields, and RawContacts, before persisting
338 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700339 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800340
341 Uri lookupUri = null;
342
343 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800344 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800345
Josh Gargusef15c8e2012-01-30 16:42:02 -0800346 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
347 long insertedRawContactId = -1;
348
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800349 // Attempt to persist changes
350 int tries = 0;
351 while (tries++ < PERSIST_TRIES) {
352 try {
353 // Build operations and try applying
354 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700355 if (DEBUG) {
356 Log.v(TAG, "Content Provider Operations:");
357 for (ContentProviderOperation operation : diff) {
358 Log.v(TAG, operation.toString());
359 }
360 }
361
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800362 ContentProviderResult[] results = null;
363 if (!diff.isEmpty()) {
364 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
365 }
366
367 final long rawContactId = getRawContactId(state, diff, results);
368 if (rawContactId == -1) {
369 throw new IllegalStateException("Could not determine RawContact ID after save");
370 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800371 // We don't have to check to see if the value is still -1. If we reach here,
372 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
373 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700374 if (isProfile) {
375 // Since the profile supports local raw contacts, which may have been completely
376 // removed if all information was removed, we need to do a special query to
377 // get the lookup URI for the profile contact (if it still exists).
378 Cursor c = resolver.query(Profile.CONTENT_URI,
379 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
380 null, null, null);
381 try {
Erik162b7e32011-09-20 15:23:55 -0700382 if (c.moveToFirst()) {
383 final long contactId = c.getLong(0);
384 final String lookupKey = c.getString(1);
385 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
386 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700387 } finally {
388 c.close();
389 }
390 } else {
391 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
392 rawContactId);
393 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
394 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800395 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
Josh Garguse692e012012-01-18 14:53:11 -0800396
397 // We can change this back to false later, if we fail to save the contact photo.
398 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800399 break;
400
401 } catch (RemoteException e) {
402 // Something went wrong, bail without success
403 Log.e(TAG, "Problem persisting user edits", e);
404 break;
405
Jay Shrauner57fca182014-01-17 14:20:50 -0800406 } catch (IllegalArgumentException e) {
407 // This is thrown by applyBatch on malformed requests
408 Log.e(TAG, "Problem persisting user edits", e);
409 showToast(R.string.contactSavedErrorToast);
410 break;
411
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800412 } catch (OperationApplicationException e) {
413 // Version consistency failed, re-parent change and try again
414 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
415 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
416 boolean first = true;
417 final int count = state.size();
418 for (int i = 0; i < count; i++) {
419 Long rawContactId = state.getRawContactId(i);
420 if (rawContactId != null && rawContactId != -1) {
421 if (!first) {
422 sb.append(',');
423 }
424 sb.append(rawContactId);
425 first = false;
426 }
427 }
428 sb.append(")");
429
430 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800431 throw new IllegalStateException(
432 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800433 }
434
Maurice Chu851222a2012-06-21 11:43:08 -0700435 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700436 isProfile
437 ? RawContactsEntity.PROFILE_CONTENT_URI
438 : RawContactsEntity.CONTENT_URI,
439 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700440 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700441
442 // Update the new state to use profile URIs if appropriate.
443 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700444 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700445 delta.setProfileQueryUri();
446 }
447 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800448 }
449 }
450
Josh Garguse692e012012-01-18 14:53:11 -0800451 // Now save any updated photos. We do this at the end to ensure that
452 // the ContactProvider already knows about newly-created contacts.
453 if (updatedPhotos != null) {
454 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700455 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800456 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800457
458 // If the raw-contact ID is negative, we are saving a new raw-contact;
459 // replace the bogus ID with the new one that we actually saved the contact at.
460 if (rawContactId < 0) {
461 rawContactId = insertedRawContactId;
462 if (rawContactId == -1) {
463 throw new IllegalStateException(
464 "Could not determine RawContact ID for image insertion");
465 }
466 }
467
Yorke Lee637a38e2013-09-14 08:36:33 -0700468 if (!saveUpdatedPhoto(rawContactId, photoUri)) succeeded = false;
Josh Garguse692e012012-01-18 14:53:11 -0800469 }
470 }
471
Josh Garguse5d3f892012-04-11 11:56:15 -0700472 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
473 if (callbackIntent != null) {
474 if (succeeded) {
475 // Mark the intent to indicate that the save was successful (even if the lookup URI
476 // is now null). For local contacts or the local profile, it's possible that the
477 // save triggered removal of the contact, so no lookup URI would exist..
478 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
479 }
480 callbackIntent.setData(lookupUri);
481 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800482 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800483 }
484
Josh Garguse692e012012-01-18 14:53:11 -0800485 /**
486 * Save updated photo for the specified raw-contact.
487 * @return true for success, false for failure
488 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700489 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800490 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800491 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
492 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
493
Yorke Lee637a38e2013-09-14 08:36:33 -0700494 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
Josh Garguse692e012012-01-18 14:53:11 -0800495 }
496
Josh Gargusef15c8e2012-01-30 16:42:02 -0800497 /**
498 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
499 */
Maurice Chu851222a2012-06-21 11:43:08 -0700500 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800501 final ArrayList<ContentProviderOperation> diff,
502 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800503 long existingRawContactId = state.findRawContactId();
504 if (existingRawContactId != -1) {
505 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800506 }
507
Josh Gargusef15c8e2012-01-30 16:42:02 -0800508 return getInsertedRawContactId(diff, results);
509 }
510
511 /**
512 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
513 */
514 private long getInsertedRawContactId(
515 final ArrayList<ContentProviderOperation> diff,
516 final ContentProviderResult[] results) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800517 final int diffSize = diff.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800518 final int numResults = results.length;
519 for (int i = 0; i < diffSize && i < numResults; i++) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800520 ContentProviderOperation operation = diff.get(i);
521 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
522 && operation.getUri().getEncodedPath().contains(
523 RawContacts.CONTENT_URI.getEncodedPath())) {
524 return ContentUris.parseId(results[i].uri);
525 }
526 }
527 return -1;
528 }
529
530 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700531 * Creates an intent that can be sent to this service to create a new group as
532 * well as add new members at the same time.
533 *
534 * @param context of the application
535 * @param account in which the group should be created
536 * @param label is the name of the group (cannot be null)
537 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
538 * should be added to the group
539 * @param callbackActivity is the activity to send the callback intent to
540 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700541 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700542 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700543 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700544 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800545 Intent serviceIntent = new Intent(context, ContactSaveService.class);
546 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
547 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
548 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700549 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800550 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700551 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700552
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800553 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700554 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800555 Intent callbackIntent = new Intent(context, callbackActivity);
556 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700557 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800558
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700559 return serviceIntent;
560 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800561
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800562 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700563 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
564 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
565 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
566 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700567 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800568
569 ContentValues values = new ContentValues();
570 values.put(Groups.ACCOUNT_TYPE, accountType);
571 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700572 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800573 values.put(Groups.TITLE, label);
574
Katherine Kuan717e3432011-07-13 17:03:24 -0700575 final ContentResolver resolver = getContentResolver();
576
577 // Create the new group
578 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
579
580 // If there's no URI, then the insertion failed. Abort early because group members can't be
581 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800582 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700583 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800584 return;
585 }
586
Katherine Kuan717e3432011-07-13 17:03:24 -0700587 // Add new group members
588 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
589
590 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
591 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800592 values.clear();
593 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
594 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
595
596 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700597 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700598 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800599 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800600 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800601 }
602
603 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800604 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800605 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700606 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700607 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800608 Intent serviceIntent = new Intent(context, ContactSaveService.class);
609 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
610 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
611 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700612
613 // Callback intent will be invoked by the service once the group is renamed.
614 Intent callbackIntent = new Intent(context, callbackActivity);
615 callbackIntent.setAction(callbackAction);
616 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
617
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800618 return serviceIntent;
619 }
620
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800621 private void renameGroup(Intent intent) {
622 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
623 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
624
625 if (groupId == -1) {
626 Log.e(TAG, "Invalid arguments for renameGroup request");
627 return;
628 }
629
630 ContentValues values = new ContentValues();
631 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700632 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
633 getContentResolver().update(groupUri, values, null, null);
634
635 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
636 callbackIntent.setData(groupUri);
637 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800638 }
639
640 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800641 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800642 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800643 public static Intent createGroupDeletionIntent(Context context, long groupId) {
644 Intent serviceIntent = new Intent(context, ContactSaveService.class);
645 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800646 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800647 return serviceIntent;
648 }
649
650 private void deleteGroup(Intent intent) {
651 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
652 if (groupId == -1) {
653 Log.e(TAG, "Invalid arguments for deleteGroup request");
654 return;
655 }
656
657 getContentResolver().delete(
658 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
659 }
660
661 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700662 * Creates an intent that can be sent to this service to rename a group as
663 * well as add and remove members from the group.
664 *
665 * @param context of the application
666 * @param groupId of the group that should be modified
667 * @param newLabel is the updated name of the group (can be null if the name
668 * should not be updated)
669 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
670 * should be added to the group
671 * @param rawContactsToRemove is an array of raw contact IDs for contacts
672 * that should be removed from the group
673 * @param callbackActivity is the activity to send the callback intent to
674 * @param callbackAction is the intent action for the callback intent
675 */
676 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
677 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700678 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700679 Intent serviceIntent = new Intent(context, ContactSaveService.class);
680 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
681 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
682 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
683 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
684 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
685 rawContactsToRemove);
686
687 // Callback intent will be invoked by the service once the group is updated
688 Intent callbackIntent = new Intent(context, callbackActivity);
689 callbackIntent.setAction(callbackAction);
690 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
691
692 return serviceIntent;
693 }
694
695 private void updateGroup(Intent intent) {
696 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
697 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
698 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
699 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
700
701 if (groupId == -1) {
702 Log.e(TAG, "Invalid arguments for updateGroup request");
703 return;
704 }
705
706 final ContentResolver resolver = getContentResolver();
707 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
708
709 // Update group name if necessary
710 if (label != null) {
711 ContentValues values = new ContentValues();
712 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700713 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700714 }
715
Katherine Kuan717e3432011-07-13 17:03:24 -0700716 // Add and remove members if necessary
717 addMembersToGroup(resolver, rawContactsToAdd, groupId);
718 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
719
720 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
721 callbackIntent.setData(groupUri);
722 deliverCallback(callbackIntent);
723 }
724
Daniel Lehmann18958a22012-02-28 17:45:25 -0800725 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700726 long groupId) {
727 if (rawContactsToAdd == null) {
728 return;
729 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700730 for (long rawContactId : rawContactsToAdd) {
731 try {
732 final ArrayList<ContentProviderOperation> rawContactOperations =
733 new ArrayList<ContentProviderOperation>();
734
735 // Build an assert operation to ensure the contact is not already in the group
736 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
737 .newAssertQuery(Data.CONTENT_URI);
738 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
739 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
740 new String[] { String.valueOf(rawContactId),
741 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
742 assertBuilder.withExpectedCount(0);
743 rawContactOperations.add(assertBuilder.build());
744
745 // Build an insert operation to add the contact to the group
746 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
747 .newInsert(Data.CONTENT_URI);
748 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
749 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
750 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
751 rawContactOperations.add(insertBuilder.build());
752
753 if (DEBUG) {
754 for (ContentProviderOperation operation : rawContactOperations) {
755 Log.v(TAG, operation.toString());
756 }
757 }
758
759 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700760 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800761 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700762 }
763 } catch (RemoteException e) {
764 // Something went wrong, bail without success
765 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
766 String.valueOf(rawContactId), e);
767 } catch (OperationApplicationException e) {
768 // The assert could have failed because the contact is already in the group,
769 // just continue to the next contact
770 Log.w(TAG, "Assert failed in adding raw contact ID " +
771 String.valueOf(rawContactId) + ". Already exists in group " +
772 String.valueOf(groupId), e);
773 }
774 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700775 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700776
Daniel Lehmann18958a22012-02-28 17:45:25 -0800777 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700778 long groupId) {
779 if (rawContactsToRemove == null) {
780 return;
781 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700782 for (long rawContactId : rawContactsToRemove) {
783 // Apply the delete operation on the data row for the given raw contact's
784 // membership in the given group. If no contact matches the provided selection, then
785 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800786 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700787 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
788 new String[] { String.valueOf(rawContactId),
789 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
790 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700791 }
792
793 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800794 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800795 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800796 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
797 Intent serviceIntent = new Intent(context, ContactSaveService.class);
798 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
799 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
800 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
801
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800802 return serviceIntent;
803 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800804
805 private void setStarred(Intent intent) {
806 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
807 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
808 if (contactUri == null) {
809 Log.e(TAG, "Invalid arguments for setStarred request");
810 return;
811 }
812
813 final ContentValues values = new ContentValues(1);
814 values.put(Contacts.STARRED, value);
815 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700816
817 // Undemote the contact if necessary
818 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
819 null, null, null);
820 try {
821 if (c.moveToFirst()) {
822 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700823
824 // Don't bother undemoting if this contact is the user's profile.
825 if (id < Profile.MIN_ID) {
Yorke Leede1c78c2014-06-19 11:30:55 -0700826 getContentResolver().call(ContactsContract.AUTHORITY_URI,
827 PinnedPositions.UNDEMOTE_METHOD, String.valueOf(id), null);
Yorke Leebbb8c992013-09-23 16:20:53 -0700828 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700829 }
830 } finally {
831 c.close();
832 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800833 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800834
835 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700836 * Creates an intent that can be sent to this service to set the redirect to voicemail.
837 */
838 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
839 boolean value) {
840 Intent serviceIntent = new Intent(context, ContactSaveService.class);
841 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
842 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
843 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
844
845 return serviceIntent;
846 }
847
848 private void setSendToVoicemail(Intent intent) {
849 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
850 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
851 if (contactUri == null) {
852 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
853 return;
854 }
855
856 final ContentValues values = new ContentValues(1);
857 values.put(Contacts.SEND_TO_VOICEMAIL, value);
858 getContentResolver().update(contactUri, values, null, null);
859 }
860
861 /**
862 * Creates an intent that can be sent to this service to save the contact's ringtone.
863 */
864 public static Intent createSetRingtone(Context context, Uri contactUri,
865 String value) {
866 Intent serviceIntent = new Intent(context, ContactSaveService.class);
867 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
868 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
869 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
870
871 return serviceIntent;
872 }
873
874 private void setRingtone(Intent intent) {
875 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
876 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
877 if (contactUri == null) {
878 Log.e(TAG, "Invalid arguments for setRingtone");
879 return;
880 }
881 ContentValues values = new ContentValues(1);
882 values.put(Contacts.CUSTOM_RINGTONE, value);
883 getContentResolver().update(contactUri, values, null, null);
884 }
885
886 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800887 * Creates an intent that sets the selected data item as super primary (default)
888 */
889 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
890 Intent serviceIntent = new Intent(context, ContactSaveService.class);
891 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
892 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
893 return serviceIntent;
894 }
895
896 private void setSuperPrimary(Intent intent) {
897 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
898 if (dataId == -1) {
899 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
900 return;
901 }
902
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700903 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800904 }
905
906 /**
907 * Creates an intent that clears the primary flag of all data items that belong to the same
908 * raw_contact as the given data item. Will only clear, if the data item was primary before
909 * this call
910 */
911 public static Intent createClearPrimaryIntent(Context context, long dataId) {
912 Intent serviceIntent = new Intent(context, ContactSaveService.class);
913 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
914 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
915 return serviceIntent;
916 }
917
918 private void clearPrimary(Intent intent) {
919 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
920 if (dataId == -1) {
921 Log.e(TAG, "Invalid arguments for clearPrimary request");
922 return;
923 }
924
925 // Update the primary values in the data record.
926 ContentValues values = new ContentValues(1);
927 values.put(Data.IS_SUPER_PRIMARY, 0);
928 values.put(Data.IS_PRIMARY, 0);
929
930 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
931 values, null, null);
932 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800933
934 /**
935 * Creates an intent that can be sent to this service to delete a contact.
936 */
937 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
938 Intent serviceIntent = new Intent(context, ContactSaveService.class);
939 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
940 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
941 return serviceIntent;
942 }
943
944 private void deleteContact(Intent intent) {
945 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
946 if (contactUri == null) {
947 Log.e(TAG, "Invalid arguments for deleteContact request");
948 return;
949 }
950
951 getContentResolver().delete(contactUri, null, null);
952 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800953
954 /**
955 * Creates an intent that can be sent to this service to join two contacts.
956 */
957 public static Intent createJoinContactsIntent(Context context, long contactId1,
958 long contactId2, boolean contactWritable,
Josh Garguse5d3f892012-04-11 11:56:15 -0700959 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800960 Intent serviceIntent = new Intent(context, ContactSaveService.class);
961 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
962 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
963 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
964 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
965
966 // Callback intent will be invoked by the service once the contacts are joined.
967 Intent callbackIntent = new Intent(context, callbackActivity);
968 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800969 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
970
971 return serviceIntent;
972 }
973
974
975 private interface JoinContactQuery {
976 String[] PROJECTION = {
977 RawContacts._ID,
978 RawContacts.CONTACT_ID,
979 RawContacts.NAME_VERIFIED,
980 RawContacts.DISPLAY_NAME_SOURCE,
981 };
982
983 String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
984
985 int _ID = 0;
986 int CONTACT_ID = 1;
987 int NAME_VERIFIED = 2;
988 int DISPLAY_NAME_SOURCE = 3;
989 }
990
991 private void joinContacts(Intent intent) {
992 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
993 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
994 boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
995 if (contactId1 == -1 || contactId2 == -1) {
996 Log.e(TAG, "Invalid arguments for joinContacts request");
997 return;
998 }
999
1000 final ContentResolver resolver = getContentResolver();
1001
1002 // Load raw contact IDs for all raw contacts involved - currently edited and selected
1003 // in the join UIs
1004 Cursor c = resolver.query(RawContacts.CONTENT_URI,
1005 JoinContactQuery.PROJECTION,
1006 JoinContactQuery.SELECTION,
1007 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1008
1009 long rawContactIds[];
1010 long verifiedNameRawContactId = -1;
1011 try {
Jay Shrauner7f42b902013-01-09 14:43:09 -08001012 if (c.getCount() == 0) {
1013 return;
1014 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001015 int maxDisplayNameSource = -1;
1016 rawContactIds = new long[c.getCount()];
1017 for (int i = 0; i < rawContactIds.length; i++) {
1018 c.moveToPosition(i);
1019 long rawContactId = c.getLong(JoinContactQuery._ID);
1020 rawContactIds[i] = rawContactId;
1021 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1022 if (nameSource > maxDisplayNameSource) {
1023 maxDisplayNameSource = nameSource;
1024 }
1025 }
1026
1027 // Find an appropriate display name for the joined contact:
1028 // if should have a higher DisplayNameSource or be the name
1029 // of the original contact that we are joining with another.
1030 if (writable) {
1031 for (int i = 0; i < rawContactIds.length; i++) {
1032 c.moveToPosition(i);
1033 if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1034 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1035 if (nameSource == maxDisplayNameSource
1036 && (verifiedNameRawContactId == -1
1037 || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1038 verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1039 }
1040 }
1041 }
1042 }
1043 } finally {
1044 c.close();
1045 }
1046
1047 // For each pair of raw contacts, insert an aggregation exception
1048 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1049 for (int i = 0; i < rawContactIds.length; i++) {
1050 for (int j = 0; j < rawContactIds.length; j++) {
1051 if (i != j) {
1052 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1053 }
1054 }
1055 }
1056
1057 // Mark the original contact as "name verified" to make sure that the contact
1058 // display name does not change as a result of the join
1059 if (verifiedNameRawContactId != -1) {
1060 Builder builder = ContentProviderOperation.newUpdate(
1061 ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1062 builder.withValue(RawContacts.NAME_VERIFIED, 1);
1063 operations.add(builder.build());
1064 }
1065
1066 boolean success = false;
1067 // Apply all aggregation exceptions as one batch
1068 try {
1069 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001070 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001071 success = true;
1072 } catch (RemoteException e) {
1073 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001074 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001075 } catch (OperationApplicationException e) {
1076 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001077 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001078 }
1079
1080 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1081 if (success) {
1082 Uri uri = RawContacts.getContactLookupUri(resolver,
1083 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1084 callbackIntent.setData(uri);
1085 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001086 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001087 }
1088
1089 /**
1090 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1091 */
1092 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1093 long rawContactId1, long rawContactId2) {
1094 Builder builder =
1095 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1096 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1097 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1098 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1099 operations.add(builder.build());
1100 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001101
1102 /**
1103 * Shows a toast on the UI thread.
1104 */
1105 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001106 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001107
1108 @Override
1109 public void run() {
1110 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1111 }
1112 });
1113 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001114
1115 private void deliverCallback(final Intent callbackIntent) {
1116 mMainHandler.post(new Runnable() {
1117
1118 @Override
1119 public void run() {
1120 deliverCallbackOnUiThread(callbackIntent);
1121 }
1122 });
1123 }
1124
1125 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1126 // TODO: this assumes that if there are multiple instances of the same
1127 // activity registered, the last one registered is the one waiting for
1128 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001129 for (Listener listener : sListeners) {
1130 if (callbackIntent.getComponent().equals(
1131 ((Activity) listener).getIntent().getComponent())) {
1132 listener.onServiceCompleted(callbackIntent);
1133 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001134 }
1135 }
1136 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001137}