blob: 12ae1501263980c2f739267e871d766454fe286d [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
Jay Shrauner615ed9c2015-07-29 11:27:56 -070019import static android.Manifest.permission.WRITE_CONTACTS;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080020import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070021import android.app.IntentService;
22import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080023import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070024import android.content.ContentProviderResult;
25import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080026import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070027import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080028import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070029import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080030import android.content.OperationApplicationException;
31import android.database.Cursor;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070032import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080033import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080034import android.os.Handler;
35import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080036import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080037import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070038import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080039import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080040import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080041import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080042import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070043import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080044import android.provider.ContactsContract.Groups;
Yorke Leee8e3fb82013-09-12 17:53:31 -070045import android.provider.ContactsContract.PinnedPositions;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070046import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070047import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070048import android.provider.ContactsContract.RawContactsEntity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070049import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080050import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070051
Chiao Chengd7ca03e2012-10-24 15:14:08 -070052import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080053import com.android.contacts.common.model.AccountTypeManager;
Yorke Leecd321f62013-10-28 15:20:15 -070054import com.android.contacts.common.model.RawContactDelta;
55import com.android.contacts.common.model.RawContactDeltaList;
56import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080057import com.android.contacts.common.model.account.AccountWithDataSet;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070058import com.android.contacts.common.util.PermissionsUtil;
Walter Jang3e764082015-05-22 09:38:42 -070059import com.android.contacts.editor.ContactEditorFragment;
Yorke Lee637a38e2013-09-14 08:36:33 -070060import com.android.contacts.util.ContactPhotoUtils;
61
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070062import com.google.common.collect.Lists;
63import com.google.common.collect.Sets;
64
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080065import java.util.ArrayList;
66import java.util.HashSet;
67import java.util.List;
68import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070069
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080070/**
71 * A service responsible for saving changes to the content provider.
72 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070073public class ContactSaveService extends IntentService {
74 private static final String TAG = "ContactSaveService";
75
Katherine Kuana007e442011-07-07 09:25:34 -070076 /** Set to true in order to view logs on content provider operations */
77 private static final boolean DEBUG = false;
78
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070079 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
80
81 public static final String EXTRA_ACCOUNT_NAME = "accountName";
82 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070083 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070084 public static final String EXTRA_CONTENT_VALUES = "contentValues";
85 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
86
Dmitri Plotnikova0114142011-02-15 13:53:21 -080087 public static final String ACTION_SAVE_CONTACT = "saveContact";
88 public static final String EXTRA_CONTACT_STATE = "state";
89 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070090 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070091 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080092 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070093
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080094 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080095 public static final String ACTION_RENAME_GROUP = "renameGroup";
96 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070097 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080098 public static final String EXTRA_GROUP_ID = "groupId";
99 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700100 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
101 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800102
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800103 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800104 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800105 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800106 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800107 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800108 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";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800115 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800116 public static final String EXTRA_CONTACT_ID1 = "contactId1";
117 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800118
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
Walter Jang0653de32015-07-24 12:12:40 -0700147 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
148
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800149 public interface Listener {
150 public void onServiceCompleted(Intent callbackIntent);
151 }
152
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100153 private static final CopyOnWriteArrayList<Listener> sListeners =
154 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800155
156 private Handler mMainHandler;
157
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700158 public ContactSaveService() {
159 super(TAG);
160 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800161 mMainHandler = new Handler(Looper.getMainLooper());
162 }
163
164 public static void registerListener(Listener listener) {
165 if (!(listener instanceof Activity)) {
166 throw new ClassCastException("Only activities can be registered to"
167 + " receive callback from " + ContactSaveService.class.getName());
168 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100169 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800170 }
171
172 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100173 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700174 }
175
176 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800177 public Object getSystemService(String name) {
178 Object service = super.getSystemService(name);
179 if (service != null) {
180 return service;
181 }
182
183 return getApplicationContext().getSystemService(name);
184 }
185
186 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700187 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800188 if (intent == null) {
189 Log.d(TAG, "onHandleIntent: could not handle null intent");
190 return;
191 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700192 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
193 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
194 // TODO: add more specific error string such as "Turn on Contacts
195 // permission to update your contacts"
196 showToast(R.string.contactSavedErrorToast);
197 return;
198 }
199
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700200 // Call an appropriate method. If we're sure it affects how incoming phone calls are
201 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700202 String action = intent.getAction();
203 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
204 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800205 } else if (ACTION_SAVE_CONTACT.equals(action)) {
206 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800207 } else if (ACTION_CREATE_GROUP.equals(action)) {
208 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800209 } else if (ACTION_RENAME_GROUP.equals(action)) {
210 renameGroup(intent);
211 } else if (ACTION_DELETE_GROUP.equals(action)) {
212 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700213 } else if (ACTION_UPDATE_GROUP.equals(action)) {
214 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800215 } else if (ACTION_SET_STARRED.equals(action)) {
216 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800217 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
218 setSuperPrimary(intent);
219 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
220 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800221 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
222 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800223 } else if (ACTION_DELETE_CONTACT.equals(action)) {
224 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800225 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
226 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800227 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
228 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700229 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
230 setSendToVoicemail(intent);
231 } else if (ACTION_SET_RINGTONE.equals(action)) {
232 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700233 }
234 }
235
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800236 /**
237 * Creates an intent that can be sent to this service to create a new raw contact
238 * using data presented as a set of ContentValues.
239 */
240 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700241 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700242 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800243 Intent serviceIntent = new Intent(
244 context, ContactSaveService.class);
245 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
246 if (account != null) {
247 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
248 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700249 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800250 }
251 serviceIntent.putParcelableArrayListExtra(
252 ContactSaveService.EXTRA_CONTENT_VALUES, values);
253
254 // Callback intent will be invoked by the service once the new contact is
255 // created. The service will put the URI of the new contact as "data" on
256 // the callback intent.
257 Intent callbackIntent = new Intent(context, callbackActivity);
258 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800259 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
260 return serviceIntent;
261 }
262
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700263 private void createRawContact(Intent intent) {
264 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
265 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700266 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700267 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
268 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
269
270 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
271 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
272 .withValue(RawContacts.ACCOUNT_NAME, accountName)
273 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700274 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700275 .build());
276
277 int size = valueList.size();
278 for (int i = 0; i < size; i++) {
279 ContentValues values = valueList.get(i);
280 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
281 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
282 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
283 .withValues(values)
284 .build());
285 }
286
287 ContentResolver resolver = getContentResolver();
288 ContentProviderResult[] results;
289 try {
290 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
291 } catch (Exception e) {
292 throw new RuntimeException("Failed to store new contact", e);
293 }
294
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700295 Uri rawContactUri = results[0].uri;
296 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
297
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800298 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700299 }
300
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700301 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800302 * Creates an intent that can be sent to this service to create a new raw contact
303 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800304 * This variant is more convenient to use when there is only one photo that can
305 * possibly be updated, as in the Contact Details screen.
306 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
307 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800308 */
Maurice Chu851222a2012-06-21 11:43:08 -0700309 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700310 String saveModeExtraKey, int saveMode, boolean isProfile,
311 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700312 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800313 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700314 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800315 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700316 callbackActivity, callbackAction, bundle,
317 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800318 }
319
320 /**
321 * Creates an intent that can be sent to this service to create a new raw contact
322 * using data presented as a set of ContentValues.
323 * This variant is used when multiple contacts' photos may be updated, as in the
324 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700325 *
Josh Garguse692e012012-01-18 14:53:11 -0800326 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700327 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
328 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800329 */
Maurice Chu851222a2012-06-21 11:43:08 -0700330 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700331 String saveModeExtraKey, int saveMode, boolean isProfile,
332 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700333 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800334 Intent serviceIntent = new Intent(
335 context, ContactSaveService.class);
336 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
337 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700338 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800339 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
340
Josh Garguse692e012012-01-18 14:53:11 -0800341 if (updatedPhotos != null) {
342 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
343 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800344
Josh Garguse5d3f892012-04-11 11:56:15 -0700345 if (callbackActivity != null) {
346 // Callback intent will be invoked by the service once the contact is
347 // saved. The service will put the URI of the new contact as "data" on
348 // the callback intent.
349 Intent callbackIntent = new Intent(context, callbackActivity);
350 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700351 if (joinContactIdExtraKey != null && joinContactId != null) {
352 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
353 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700354 callbackIntent.setAction(callbackAction);
355 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
356 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800357 return serviceIntent;
358 }
359
360 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700361 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700362 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800363 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800364
Jay Shrauner08099782015-03-25 14:17:11 -0700365 if (state == null) {
366 Log.e(TAG, "Invalid arguments for saveContact request");
367 return;
368 }
369
benny.lin3a4e7a22014-01-08 10:58:08 +0800370 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800371 // Trim any empty fields, and RawContacts, before persisting
372 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700373 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800374
375 Uri lookupUri = null;
376
377 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800378 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800379
Josh Gargusef15c8e2012-01-30 16:42:02 -0800380 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
381 long insertedRawContactId = -1;
382
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800383 // Attempt to persist changes
384 int tries = 0;
385 while (tries++ < PERSIST_TRIES) {
386 try {
387 // Build operations and try applying
388 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700389 if (DEBUG) {
390 Log.v(TAG, "Content Provider Operations:");
391 for (ContentProviderOperation operation : diff) {
392 Log.v(TAG, operation.toString());
393 }
394 }
395
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800396 ContentProviderResult[] results = null;
397 if (!diff.isEmpty()) {
398 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
Jay Shrauner511561d2015-04-02 10:35:33 -0700399 if (results == null) {
400 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
401 // Retry save
402 continue;
403 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800404 }
405
406 final long rawContactId = getRawContactId(state, diff, results);
407 if (rawContactId == -1) {
408 throw new IllegalStateException("Could not determine RawContact ID after save");
409 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800410 // We don't have to check to see if the value is still -1. If we reach here,
411 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
412 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700413 if (isProfile) {
414 // Since the profile supports local raw contacts, which may have been completely
415 // removed if all information was removed, we need to do a special query to
416 // get the lookup URI for the profile contact (if it still exists).
417 Cursor c = resolver.query(Profile.CONTENT_URI,
418 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
419 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800420 if (c == null) {
421 continue;
422 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700423 try {
Erik162b7e32011-09-20 15:23:55 -0700424 if (c.moveToFirst()) {
425 final long contactId = c.getLong(0);
426 final String lookupKey = c.getString(1);
427 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
428 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700429 } finally {
430 c.close();
431 }
432 } else {
433 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
434 rawContactId);
435 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
436 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800437 if (lookupUri != null) {
438 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
439 }
Josh Garguse692e012012-01-18 14:53:11 -0800440
441 // We can change this back to false later, if we fail to save the contact photo.
442 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800443 break;
444
445 } catch (RemoteException e) {
446 // Something went wrong, bail without success
447 Log.e(TAG, "Problem persisting user edits", e);
448 break;
449
Jay Shrauner57fca182014-01-17 14:20:50 -0800450 } catch (IllegalArgumentException e) {
451 // This is thrown by applyBatch on malformed requests
452 Log.e(TAG, "Problem persisting user edits", e);
453 showToast(R.string.contactSavedErrorToast);
454 break;
455
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800456 } catch (OperationApplicationException e) {
457 // Version consistency failed, re-parent change and try again
458 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
459 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
460 boolean first = true;
461 final int count = state.size();
462 for (int i = 0; i < count; i++) {
463 Long rawContactId = state.getRawContactId(i);
464 if (rawContactId != null && rawContactId != -1) {
465 if (!first) {
466 sb.append(',');
467 }
468 sb.append(rawContactId);
469 first = false;
470 }
471 }
472 sb.append(")");
473
474 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800475 throw new IllegalStateException(
476 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800477 }
478
Maurice Chu851222a2012-06-21 11:43:08 -0700479 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700480 isProfile
481 ? RawContactsEntity.PROFILE_CONTENT_URI
482 : RawContactsEntity.CONTENT_URI,
483 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700484 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700485
486 // Update the new state to use profile URIs if appropriate.
487 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700488 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700489 delta.setProfileQueryUri();
490 }
491 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800492 }
493 }
494
Josh Garguse692e012012-01-18 14:53:11 -0800495 // Now save any updated photos. We do this at the end to ensure that
496 // the ContactProvider already knows about newly-created contacts.
497 if (updatedPhotos != null) {
498 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700499 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800500 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800501
502 // If the raw-contact ID is negative, we are saving a new raw-contact;
503 // replace the bogus ID with the new one that we actually saved the contact at.
504 if (rawContactId < 0) {
505 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800506 }
507
Jay Shrauner511561d2015-04-02 10:35:33 -0700508 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700509 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700510 succeeded = false;
511 }
Josh Garguse692e012012-01-18 14:53:11 -0800512 }
513 }
514
Josh Garguse5d3f892012-04-11 11:56:15 -0700515 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
516 if (callbackIntent != null) {
517 if (succeeded) {
518 // Mark the intent to indicate that the save was successful (even if the lookup URI
519 // is now null). For local contacts or the local profile, it's possible that the
520 // save triggered removal of the contact, so no lookup URI would exist..
521 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
522 }
523 callbackIntent.setData(lookupUri);
524 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800525 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800526 }
527
Josh Garguse692e012012-01-18 14:53:11 -0800528 /**
529 * Save updated photo for the specified raw-contact.
530 * @return true for success, false for failure
531 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800532 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800533 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800534 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
535 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
536
benny.lin3a4e7a22014-01-08 10:58:08 +0800537 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800538 }
539
Josh Gargusef15c8e2012-01-30 16:42:02 -0800540 /**
541 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
542 */
Maurice Chu851222a2012-06-21 11:43:08 -0700543 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800544 final ArrayList<ContentProviderOperation> diff,
545 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800546 long existingRawContactId = state.findRawContactId();
547 if (existingRawContactId != -1) {
548 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800549 }
550
Josh Gargusef15c8e2012-01-30 16:42:02 -0800551 return getInsertedRawContactId(diff, results);
552 }
553
554 /**
555 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
556 */
557 private long getInsertedRawContactId(
558 final ArrayList<ContentProviderOperation> diff,
559 final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800560 if (results == null) {
561 return -1;
562 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800563 final int diffSize = diff.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800564 final int numResults = results.length;
565 for (int i = 0; i < diffSize && i < numResults; i++) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800566 ContentProviderOperation operation = diff.get(i);
Brian Attwell13f94e12015-01-22 16:27:48 -0800567 if (operation.isInsert() && operation.getUri().getEncodedPath().contains(
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800568 RawContacts.CONTENT_URI.getEncodedPath())) {
569 return ContentUris.parseId(results[i].uri);
570 }
571 }
572 return -1;
573 }
574
575 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700576 * Creates an intent that can be sent to this service to create a new group as
577 * well as add new members at the same time.
578 *
579 * @param context of the application
580 * @param account in which the group should be created
581 * @param label is the name of the group (cannot be null)
582 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
583 * should be added to the group
584 * @param callbackActivity is the activity to send the callback intent to
585 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700586 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700587 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700588 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700589 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800590 Intent serviceIntent = new Intent(context, ContactSaveService.class);
591 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
592 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
593 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700594 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800595 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700596 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700597
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800598 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700599 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800600 Intent callbackIntent = new Intent(context, callbackActivity);
601 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700602 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800603
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700604 return serviceIntent;
605 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800606
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800607 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700608 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
609 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
610 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
611 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700612 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800613
614 ContentValues values = new ContentValues();
615 values.put(Groups.ACCOUNT_TYPE, accountType);
616 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700617 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800618 values.put(Groups.TITLE, label);
619
Katherine Kuan717e3432011-07-13 17:03:24 -0700620 final ContentResolver resolver = getContentResolver();
621
622 // Create the new group
623 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
624
625 // If there's no URI, then the insertion failed. Abort early because group members can't be
626 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800627 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700628 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800629 return;
630 }
631
Katherine Kuan717e3432011-07-13 17:03:24 -0700632 // Add new group members
633 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
634
635 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
636 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800637 values.clear();
638 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
639 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
640
641 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700642 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700643 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800644 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800645 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800646 }
647
648 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800649 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800650 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700651 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700652 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800653 Intent serviceIntent = new Intent(context, ContactSaveService.class);
654 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
655 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
656 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700657
658 // Callback intent will be invoked by the service once the group is renamed.
659 Intent callbackIntent = new Intent(context, callbackActivity);
660 callbackIntent.setAction(callbackAction);
661 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
662
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800663 return serviceIntent;
664 }
665
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800666 private void renameGroup(Intent intent) {
667 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
668 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
669
670 if (groupId == -1) {
671 Log.e(TAG, "Invalid arguments for renameGroup request");
672 return;
673 }
674
675 ContentValues values = new ContentValues();
676 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700677 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
678 getContentResolver().update(groupUri, values, null, null);
679
680 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
681 callbackIntent.setData(groupUri);
682 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800683 }
684
685 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800686 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800687 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800688 public static Intent createGroupDeletionIntent(Context context, long groupId) {
689 Intent serviceIntent = new Intent(context, ContactSaveService.class);
690 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800691 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800692 return serviceIntent;
693 }
694
695 private void deleteGroup(Intent intent) {
696 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
697 if (groupId == -1) {
698 Log.e(TAG, "Invalid arguments for deleteGroup request");
699 return;
700 }
701
702 getContentResolver().delete(
703 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
704 }
705
706 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700707 * Creates an intent that can be sent to this service to rename a group as
708 * well as add and remove members from the group.
709 *
710 * @param context of the application
711 * @param groupId of the group that should be modified
712 * @param newLabel is the updated name of the group (can be null if the name
713 * should not be updated)
714 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
715 * should be added to the group
716 * @param rawContactsToRemove is an array of raw contact IDs for contacts
717 * that should be removed from the group
718 * @param callbackActivity is the activity to send the callback intent to
719 * @param callbackAction is the intent action for the callback intent
720 */
721 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
722 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700723 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700724 Intent serviceIntent = new Intent(context, ContactSaveService.class);
725 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
726 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
727 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
728 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
729 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
730 rawContactsToRemove);
731
732 // Callback intent will be invoked by the service once the group is updated
733 Intent callbackIntent = new Intent(context, callbackActivity);
734 callbackIntent.setAction(callbackAction);
735 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
736
737 return serviceIntent;
738 }
739
740 private void updateGroup(Intent intent) {
741 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
742 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
743 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
744 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
745
746 if (groupId == -1) {
747 Log.e(TAG, "Invalid arguments for updateGroup request");
748 return;
749 }
750
751 final ContentResolver resolver = getContentResolver();
752 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
753
754 // Update group name if necessary
755 if (label != null) {
756 ContentValues values = new ContentValues();
757 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700758 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700759 }
760
Katherine Kuan717e3432011-07-13 17:03:24 -0700761 // Add and remove members if necessary
762 addMembersToGroup(resolver, rawContactsToAdd, groupId);
763 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
764
765 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
766 callbackIntent.setData(groupUri);
767 deliverCallback(callbackIntent);
768 }
769
Daniel Lehmann18958a22012-02-28 17:45:25 -0800770 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700771 long groupId) {
772 if (rawContactsToAdd == null) {
773 return;
774 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700775 for (long rawContactId : rawContactsToAdd) {
776 try {
777 final ArrayList<ContentProviderOperation> rawContactOperations =
778 new ArrayList<ContentProviderOperation>();
779
780 // Build an assert operation to ensure the contact is not already in the group
781 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
782 .newAssertQuery(Data.CONTENT_URI);
783 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
784 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
785 new String[] { String.valueOf(rawContactId),
786 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
787 assertBuilder.withExpectedCount(0);
788 rawContactOperations.add(assertBuilder.build());
789
790 // Build an insert operation to add the contact to the group
791 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
792 .newInsert(Data.CONTENT_URI);
793 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
794 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
795 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
796 rawContactOperations.add(insertBuilder.build());
797
798 if (DEBUG) {
799 for (ContentProviderOperation operation : rawContactOperations) {
800 Log.v(TAG, operation.toString());
801 }
802 }
803
804 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700805 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800806 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700807 }
808 } catch (RemoteException e) {
809 // Something went wrong, bail without success
810 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
811 String.valueOf(rawContactId), e);
812 } catch (OperationApplicationException e) {
813 // The assert could have failed because the contact is already in the group,
814 // just continue to the next contact
815 Log.w(TAG, "Assert failed in adding raw contact ID " +
816 String.valueOf(rawContactId) + ". Already exists in group " +
817 String.valueOf(groupId), e);
818 }
819 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700820 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700821
Daniel Lehmann18958a22012-02-28 17:45:25 -0800822 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700823 long groupId) {
824 if (rawContactsToRemove == null) {
825 return;
826 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700827 for (long rawContactId : rawContactsToRemove) {
828 // Apply the delete operation on the data row for the given raw contact's
829 // membership in the given group. If no contact matches the provided selection, then
830 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800831 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700832 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
833 new String[] { String.valueOf(rawContactId),
834 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
835 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700836 }
837
838 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800839 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800840 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800841 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
842 Intent serviceIntent = new Intent(context, ContactSaveService.class);
843 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
844 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
845 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
846
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800847 return serviceIntent;
848 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800849
850 private void setStarred(Intent intent) {
851 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
852 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
853 if (contactUri == null) {
854 Log.e(TAG, "Invalid arguments for setStarred request");
855 return;
856 }
857
858 final ContentValues values = new ContentValues(1);
859 values.put(Contacts.STARRED, value);
860 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700861
862 // Undemote the contact if necessary
863 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
864 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800865 if (c == null) {
866 return;
867 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700868 try {
869 if (c.moveToFirst()) {
870 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700871
872 // Don't bother undemoting if this contact is the user's profile.
873 if (id < Profile.MIN_ID) {
Brian Attwell2d88efa2014-12-17 21:49:56 -0800874 PinnedPositions.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700875 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700876 }
877 } finally {
878 c.close();
879 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800880 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800881
882 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700883 * Creates an intent that can be sent to this service to set the redirect to voicemail.
884 */
885 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
886 boolean value) {
887 Intent serviceIntent = new Intent(context, ContactSaveService.class);
888 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
889 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
890 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
891
892 return serviceIntent;
893 }
894
895 private void setSendToVoicemail(Intent intent) {
896 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
897 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
898 if (contactUri == null) {
899 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
900 return;
901 }
902
903 final ContentValues values = new ContentValues(1);
904 values.put(Contacts.SEND_TO_VOICEMAIL, value);
905 getContentResolver().update(contactUri, values, null, null);
906 }
907
908 /**
909 * Creates an intent that can be sent to this service to save the contact's ringtone.
910 */
911 public static Intent createSetRingtone(Context context, Uri contactUri,
912 String value) {
913 Intent serviceIntent = new Intent(context, ContactSaveService.class);
914 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
915 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
916 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
917
918 return serviceIntent;
919 }
920
921 private void setRingtone(Intent intent) {
922 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
923 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
924 if (contactUri == null) {
925 Log.e(TAG, "Invalid arguments for setRingtone");
926 return;
927 }
928 ContentValues values = new ContentValues(1);
929 values.put(Contacts.CUSTOM_RINGTONE, value);
930 getContentResolver().update(contactUri, values, null, null);
931 }
932
933 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800934 * Creates an intent that sets the selected data item as super primary (default)
935 */
936 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
937 Intent serviceIntent = new Intent(context, ContactSaveService.class);
938 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
939 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
940 return serviceIntent;
941 }
942
943 private void setSuperPrimary(Intent intent) {
944 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
945 if (dataId == -1) {
946 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
947 return;
948 }
949
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700950 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800951 }
952
953 /**
954 * Creates an intent that clears the primary flag of all data items that belong to the same
955 * raw_contact as the given data item. Will only clear, if the data item was primary before
956 * this call
957 */
958 public static Intent createClearPrimaryIntent(Context context, long dataId) {
959 Intent serviceIntent = new Intent(context, ContactSaveService.class);
960 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
961 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
962 return serviceIntent;
963 }
964
965 private void clearPrimary(Intent intent) {
966 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
967 if (dataId == -1) {
968 Log.e(TAG, "Invalid arguments for clearPrimary request");
969 return;
970 }
971
972 // Update the primary values in the data record.
973 ContentValues values = new ContentValues(1);
974 values.put(Data.IS_SUPER_PRIMARY, 0);
975 values.put(Data.IS_PRIMARY, 0);
976
977 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
978 values, null, null);
979 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800980
981 /**
982 * Creates an intent that can be sent to this service to delete a contact.
983 */
984 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
985 Intent serviceIntent = new Intent(context, ContactSaveService.class);
986 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
987 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
988 return serviceIntent;
989 }
990
Brian Attwelld2962a32015-03-02 14:48:50 -0800991 /**
992 * Creates an intent that can be sent to this service to delete multiple contacts.
993 */
994 public static Intent createDeleteMultipleContactsIntent(Context context,
995 long[] contactIds) {
996 Intent serviceIntent = new Intent(context, ContactSaveService.class);
997 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
998 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
999 return serviceIntent;
1000 }
1001
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001002 private void deleteContact(Intent intent) {
1003 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1004 if (contactUri == null) {
1005 Log.e(TAG, "Invalid arguments for deleteContact request");
1006 return;
1007 }
1008
1009 getContentResolver().delete(contactUri, null, null);
1010 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001011
Brian Attwelld2962a32015-03-02 14:48:50 -08001012 private void deleteMultipleContacts(Intent intent) {
1013 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1014 if (contactIds == null) {
1015 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1016 return;
1017 }
1018 for (long contactId : contactIds) {
1019 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1020 getContentResolver().delete(contactUri, null, null);
1021 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001022 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1023 .contacts_deleted_toast, contactIds.length);
1024 mMainHandler.post(new Runnable() {
1025 @Override
1026 public void run() {
1027 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1028 .show();
1029 }
1030 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001031 }
1032
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001033 /**
1034 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001035 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001036 */
1037 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001038 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001039 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1040 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1041 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1042 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001043
1044 // Callback intent will be invoked by the service once the contacts are joined.
1045 Intent callbackIntent = new Intent(context, callbackActivity);
1046 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001047 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1048
1049 return serviceIntent;
1050 }
1051
Brian Attwelld3946ca2015-03-03 11:13:49 -08001052 /**
1053 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1054 * No special attention is paid to where the resulting contact's name is taken from.
1055 */
1056 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1057 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1058 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1059 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1060 return serviceIntent;
1061 }
1062
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001063
1064 private interface JoinContactQuery {
1065 String[] PROJECTION = {
1066 RawContacts._ID,
1067 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001068 RawContacts.DISPLAY_NAME_SOURCE,
1069 };
1070
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001071 int _ID = 0;
1072 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001073 int DISPLAY_NAME_SOURCE = 2;
1074 }
1075
1076 private interface ContactEntityQuery {
1077 String[] PROJECTION = {
1078 Contacts.Entity.DATA_ID,
1079 Contacts.Entity.CONTACT_ID,
1080 Contacts.Entity.IS_SUPER_PRIMARY,
1081 };
1082 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1083 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1084 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1085 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1086
1087 int DATA_ID = 0;
1088 int CONTACT_ID = 1;
1089 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001090 }
1091
Brian Attwelld3946ca2015-03-03 11:13:49 -08001092 private void joinSeveralContacts(Intent intent) {
1093 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001094
Brian Attwelld3946ca2015-03-03 11:13:49 -08001095 // Load raw contact IDs for all contacts involved.
1096 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1097 if (rawContactIds == null) {
1098 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001099 return;
1100 }
1101
Brian Attwelld3946ca2015-03-03 11:13:49 -08001102 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001103 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001104 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1105 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1106 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001107 for (int i = 0; i < rawContactIds.length; i++) {
1108 for (int j = 0; j < rawContactIds.length; j++) {
1109 if (i != j) {
1110 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1111 }
Walter Jang0653de32015-07-24 12:12:40 -07001112 // Before we get to 500 we need to flush the operations list
1113 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1114 if (!applyJoinOperations(resolver, operations)) {
1115 return;
1116 }
1117 operations.clear();
1118 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001119 }
1120 }
Walter Jang0653de32015-07-24 12:12:40 -07001121 if (operations.size() > 0 && !applyJoinOperations(resolver, operations)) {
1122 return;
1123 }
1124 showToast(R.string.contactsJoinedMessage);
1125 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001126
Walter Jang0653de32015-07-24 12:12:40 -07001127 /** Returns true if the batch was successfully applied and false otherwise. */
1128 private boolean applyJoinOperations(ContentResolver resolver,
1129 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001130 try {
1131 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Walter Jang0653de32015-07-24 12:12:40 -07001132 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001133 } catch (RemoteException | OperationApplicationException e) {
1134 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1135 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001136 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001137 }
1138 }
1139
1140
1141 private void joinContacts(Intent intent) {
1142 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1143 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001144
1145 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001146 // in the join UIs.
1147 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1148 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001149 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001150 return;
1151 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001152
Brian Attwell548f5c62015-01-27 17:46:46 -08001153 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001154
1155 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001156 for (int i = 0; i < rawContactIds.length; i++) {
1157 for (int j = 0; j < rawContactIds.length; j++) {
1158 if (i != j) {
1159 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1160 }
1161 }
1162 }
1163
Brian Attwelld3946ca2015-03-03 11:13:49 -08001164 final ContentResolver resolver = getContentResolver();
1165
Brian Attwell548f5c62015-01-27 17:46:46 -08001166 // Use the name for contactId1 as the name for the newly aggregated contact.
1167 final Uri contactId1Uri = ContentUris.withAppendedId(
1168 Contacts.CONTENT_URI, contactId1);
1169 final Uri entityUri = Uri.withAppendedPath(
1170 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1171 Cursor c = resolver.query(entityUri,
1172 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1173 if (c == null) {
1174 Log.e(TAG, "Unable to open Contacts DB cursor");
1175 showToast(R.string.contactSavedErrorToast);
1176 return;
1177 }
1178 long dataIdToAddSuperPrimary = -1;
1179 try {
1180 if (c.moveToFirst()) {
1181 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1182 }
1183 } finally {
1184 c.close();
1185 }
1186
1187 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1188 // display name does not change as a result of the join.
1189 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001190 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001191 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1192 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1193 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001194 operations.add(builder.build());
1195 }
1196
1197 boolean success = false;
1198 // Apply all aggregation exceptions as one batch
1199 try {
1200 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001201 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001202 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001203 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001204 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001205 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001206 }
1207
1208 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1209 if (success) {
1210 Uri uri = RawContacts.getContactLookupUri(resolver,
1211 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1212 callbackIntent.setData(uri);
1213 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001214 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001215 }
1216
Brian Attwelld3946ca2015-03-03 11:13:49 -08001217 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1218 if (contactIds == null) {
1219 return null;
1220 }
1221
Brian Attwell548f5c62015-01-27 17:46:46 -08001222 final ContentResolver resolver = getContentResolver();
1223 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001224
1225 final StringBuilder queryBuilder = new StringBuilder();
1226 final String stringContactIds[] = new String[contactIds.length];
1227 for (int i = 0; i < contactIds.length; i++) {
1228 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1229 stringContactIds[i] = String.valueOf(contactIds[i]);
1230 if (contactIds[i] == -1) {
1231 return null;
1232 }
1233 if (i == contactIds.length -1) {
1234 break;
1235 }
1236 queryBuilder.append(" OR ");
1237 }
1238
Brian Attwell548f5c62015-01-27 17:46:46 -08001239 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1240 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001241 queryBuilder.toString(),
1242 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001243 if (c == null) {
1244 Log.e(TAG, "Unable to open Contacts DB cursor");
1245 showToast(R.string.contactSavedErrorToast);
1246 return null;
1247 }
1248 try {
1249 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001250 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001251 return null;
1252 }
1253 rawContactIds = new long[c.getCount()];
1254 for (int i = 0; i < rawContactIds.length; i++) {
1255 c.moveToPosition(i);
1256 long rawContactId = c.getLong(JoinContactQuery._ID);
1257 rawContactIds[i] = rawContactId;
1258 }
1259 } finally {
1260 c.close();
1261 }
1262 return rawContactIds;
1263 }
1264
Brian Attwelld3946ca2015-03-03 11:13:49 -08001265 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1266 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1267 }
1268
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001269 /**
1270 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1271 */
1272 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1273 long rawContactId1, long rawContactId2) {
1274 Builder builder =
1275 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1276 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1277 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1278 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1279 operations.add(builder.build());
1280 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001281
1282 /**
1283 * Shows a toast on the UI thread.
1284 */
1285 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001286 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001287
1288 @Override
1289 public void run() {
1290 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1291 }
1292 });
1293 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001294
1295 private void deliverCallback(final Intent callbackIntent) {
1296 mMainHandler.post(new Runnable() {
1297
1298 @Override
1299 public void run() {
1300 deliverCallbackOnUiThread(callbackIntent);
1301 }
1302 });
1303 }
1304
1305 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1306 // TODO: this assumes that if there are multiple instances of the same
1307 // activity registered, the last one registered is the one waiting for
1308 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001309 for (Listener listener : sListeners) {
1310 if (callbackIntent.getComponent().equals(
1311 ((Activity) listener).getIntent().getComponent())) {
1312 listener.onServiceCompleted(callbackIntent);
1313 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001314 }
1315 }
1316 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001317}