blob: 9944c77c67006908262e19cde7de24dd9641ecb1 [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;
Brian Attwell548f5c62015-01-27 17:46:46 -080040import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080041import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070042import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080043import android.provider.ContactsContract.Groups;
Yorke Leee8e3fb82013-09-12 17:53:31 -070044import android.provider.ContactsContract.PinnedPositions;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070045import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070046import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070047import android.provider.ContactsContract.RawContactsEntity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070048import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080049import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070050
Chiao Chengd7ca03e2012-10-24 15:14:08 -070051import com.android.contacts.common.database.ContactUpdateUtils;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080052import com.android.contacts.common.model.AccountTypeManager;
Yorke Leecd321f62013-10-28 15:20:15 -070053import com.android.contacts.common.model.RawContactDelta;
54import com.android.contacts.common.model.RawContactDeltaList;
55import com.android.contacts.common.model.RawContactModifier;
Chiao Cheng428f0082012-11-13 18:38:56 -080056import com.android.contacts.common.model.account.AccountWithDataSet;
Walter Jang3e764082015-05-22 09:38:42 -070057import com.android.contacts.editor.ContactEditorFragment;
Yorke Lee637a38e2013-09-14 08:36:33 -070058import com.android.contacts.util.ContactPhotoUtils;
59
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070060import com.google.common.collect.Lists;
61import com.google.common.collect.Sets;
62
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080063import java.util.ArrayList;
64import java.util.HashSet;
65import java.util.List;
66import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070067
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080068/**
69 * A service responsible for saving changes to the content provider.
70 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070071public class ContactSaveService extends IntentService {
72 private static final String TAG = "ContactSaveService";
73
Katherine Kuana007e442011-07-07 09:25:34 -070074 /** Set to true in order to view logs on content provider operations */
75 private static final boolean DEBUG = false;
76
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070077 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
78
79 public static final String EXTRA_ACCOUNT_NAME = "accountName";
80 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070081 public static final String EXTRA_DATA_SET = "dataSet";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070082 public static final String EXTRA_CONTENT_VALUES = "contentValues";
83 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
84
Dmitri Plotnikova0114142011-02-15 13:53:21 -080085 public static final String ACTION_SAVE_CONTACT = "saveContact";
86 public static final String EXTRA_CONTACT_STATE = "state";
87 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070088 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -070089 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -080090 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -070091
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080092 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080093 public static final String ACTION_RENAME_GROUP = "renameGroup";
94 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070095 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080096 public static final String EXTRA_GROUP_ID = "groupId";
97 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -070098 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
99 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800100
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800101 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800102 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800103 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800104 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800105 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800106 public static final String EXTRA_STARRED_FLAG = "starred";
107
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800108 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
109 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
110 public static final String EXTRA_DATA_ID = "dataId";
111
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800112 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800113 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800114 public static final String EXTRA_CONTACT_ID1 = "contactId1";
115 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800116
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700117 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
118 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
119
120 public static final String ACTION_SET_RINGTONE = "setRingtone";
121 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
122
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700123 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
124 Data.MIMETYPE,
125 Data.IS_PRIMARY,
126 Data.DATA1,
127 Data.DATA2,
128 Data.DATA3,
129 Data.DATA4,
130 Data.DATA5,
131 Data.DATA6,
132 Data.DATA7,
133 Data.DATA8,
134 Data.DATA9,
135 Data.DATA10,
136 Data.DATA11,
137 Data.DATA12,
138 Data.DATA13,
139 Data.DATA14,
140 Data.DATA15
141 );
142
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800143 private static final int PERSIST_TRIES = 3;
144
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800145 public interface Listener {
146 public void onServiceCompleted(Intent callbackIntent);
147 }
148
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100149 private static final CopyOnWriteArrayList<Listener> sListeners =
150 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800151
152 private Handler mMainHandler;
153
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700154 public ContactSaveService() {
155 super(TAG);
156 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800157 mMainHandler = new Handler(Looper.getMainLooper());
158 }
159
160 public static void registerListener(Listener listener) {
161 if (!(listener instanceof Activity)) {
162 throw new ClassCastException("Only activities can be registered to"
163 + " receive callback from " + ContactSaveService.class.getName());
164 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100165 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800166 }
167
168 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100169 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700170 }
171
172 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800173 public Object getSystemService(String name) {
174 Object service = super.getSystemService(name);
175 if (service != null) {
176 return service;
177 }
178
179 return getApplicationContext().getSystemService(name);
180 }
181
182 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700183 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800184 if (intent == null) {
185 Log.d(TAG, "onHandleIntent: could not handle null intent");
186 return;
187 }
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700188 // Call an appropriate method. If we're sure it affects how incoming phone calls are
189 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700190 String action = intent.getAction();
191 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
192 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800193 } else if (ACTION_SAVE_CONTACT.equals(action)) {
194 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800195 } else if (ACTION_CREATE_GROUP.equals(action)) {
196 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800197 } else if (ACTION_RENAME_GROUP.equals(action)) {
198 renameGroup(intent);
199 } else if (ACTION_DELETE_GROUP.equals(action)) {
200 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700201 } else if (ACTION_UPDATE_GROUP.equals(action)) {
202 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800203 } else if (ACTION_SET_STARRED.equals(action)) {
204 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800205 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
206 setSuperPrimary(intent);
207 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
208 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800209 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
210 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800211 } else if (ACTION_DELETE_CONTACT.equals(action)) {
212 deleteContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800213 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
214 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800215 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
216 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700217 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
218 setSendToVoicemail(intent);
219 } else if (ACTION_SET_RINGTONE.equals(action)) {
220 setRingtone(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700221 }
222 }
223
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800224 /**
225 * Creates an intent that can be sent to this service to create a new raw contact
226 * using data presented as a set of ContentValues.
227 */
228 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700229 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700230 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800231 Intent serviceIntent = new Intent(
232 context, ContactSaveService.class);
233 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
234 if (account != null) {
235 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
236 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700237 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800238 }
239 serviceIntent.putParcelableArrayListExtra(
240 ContactSaveService.EXTRA_CONTENT_VALUES, values);
241
242 // Callback intent will be invoked by the service once the new contact is
243 // created. The service will put the URI of the new contact as "data" on
244 // the callback intent.
245 Intent callbackIntent = new Intent(context, callbackActivity);
246 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800247 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
248 return serviceIntent;
249 }
250
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700251 private void createRawContact(Intent intent) {
252 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
253 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700254 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700255 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
256 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
257
258 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
259 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
260 .withValue(RawContacts.ACCOUNT_NAME, accountName)
261 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700262 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700263 .build());
264
265 int size = valueList.size();
266 for (int i = 0; i < size; i++) {
267 ContentValues values = valueList.get(i);
268 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
269 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
270 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
271 .withValues(values)
272 .build());
273 }
274
275 ContentResolver resolver = getContentResolver();
276 ContentProviderResult[] results;
277 try {
278 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
279 } catch (Exception e) {
280 throw new RuntimeException("Failed to store new contact", e);
281 }
282
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700283 Uri rawContactUri = results[0].uri;
284 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
285
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800286 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700287 }
288
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700289 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800290 * Creates an intent that can be sent to this service to create a new raw contact
291 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800292 * This variant is more convenient to use when there is only one photo that can
293 * possibly be updated, as in the Contact Details screen.
294 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
295 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800296 */
Maurice Chu851222a2012-06-21 11:43:08 -0700297 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700298 String saveModeExtraKey, int saveMode, boolean isProfile,
299 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700300 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800301 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700302 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800303 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jang3e764082015-05-22 09:38:42 -0700304 callbackActivity, callbackAction, bundle, /* backPressed =*/ false);
Josh Garguse692e012012-01-18 14:53:11 -0800305 }
306
307 /**
308 * Creates an intent that can be sent to this service to create a new raw contact
309 * using data presented as a set of ContentValues.
310 * This variant is used when multiple contacts' photos may be updated, as in the
311 * Contact Editor.
312 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jang3e764082015-05-22 09:38:42 -0700313 * @param backPressed whether the save was initiated as a result of a back button press
314 * or because the framework stopped the editor Activity
Josh Garguse692e012012-01-18 14:53:11 -0800315 */
Maurice Chu851222a2012-06-21 11:43:08 -0700316 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700317 String saveModeExtraKey, int saveMode, boolean isProfile,
318 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jang3e764082015-05-22 09:38:42 -0700319 Bundle updatedPhotos, boolean backPressed) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800320 Intent serviceIntent = new Intent(
321 context, ContactSaveService.class);
322 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
323 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700324 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
Josh Garguse692e012012-01-18 14:53:11 -0800325 if (updatedPhotos != null) {
326 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
327 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800328
Josh Garguse5d3f892012-04-11 11:56:15 -0700329 if (callbackActivity != null) {
330 // Callback intent will be invoked by the service once the contact is
331 // saved. The service will put the URI of the new contact as "data" on
332 // the callback intent.
333 Intent callbackIntent = new Intent(context, callbackActivity);
334 callbackIntent.putExtra(saveModeExtraKey, saveMode);
335 callbackIntent.setAction(callbackAction);
Walter Jang1e8801b2015-03-10 15:57:05 -0700336 if (updatedPhotos != null) {
337 callbackIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
338 }
Walter Jang3e764082015-05-22 09:38:42 -0700339 callbackIntent.putExtra(ContactEditorFragment.INTENT_EXTRA_SAVE_BACK_PRESSED,
340 backPressed);
Josh Garguse5d3f892012-04-11 11:56:15 -0700341 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
342 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800343 return serviceIntent;
344 }
345
346 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700347 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700348 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800349 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800350
Jay Shrauner08099782015-03-25 14:17:11 -0700351 if (state == null) {
352 Log.e(TAG, "Invalid arguments for saveContact request");
353 return;
354 }
355
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800356 // Trim any empty fields, and RawContacts, before persisting
357 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700358 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800359
360 Uri lookupUri = null;
361
362 final ContentResolver resolver = getContentResolver();
Josh Garguse692e012012-01-18 14:53:11 -0800363 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800364
Josh Gargusef15c8e2012-01-30 16:42:02 -0800365 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
366 long insertedRawContactId = -1;
367
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800368 // Attempt to persist changes
369 int tries = 0;
370 while (tries++ < PERSIST_TRIES) {
371 try {
372 // Build operations and try applying
373 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
Katherine Kuana007e442011-07-07 09:25:34 -0700374 if (DEBUG) {
375 Log.v(TAG, "Content Provider Operations:");
376 for (ContentProviderOperation operation : diff) {
377 Log.v(TAG, operation.toString());
378 }
379 }
380
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800381 ContentProviderResult[] results = null;
382 if (!diff.isEmpty()) {
383 results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
Jay Shrauner511561d2015-04-02 10:35:33 -0700384 if (results == null) {
385 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
386 // Retry save
387 continue;
388 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800389 }
390
391 final long rawContactId = getRawContactId(state, diff, results);
392 if (rawContactId == -1) {
393 throw new IllegalStateException("Could not determine RawContact ID after save");
394 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800395 // We don't have to check to see if the value is still -1. If we reach here,
396 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
397 insertedRawContactId = getInsertedRawContactId(diff, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700398 if (isProfile) {
399 // Since the profile supports local raw contacts, which may have been completely
400 // removed if all information was removed, we need to do a special query to
401 // get the lookup URI for the profile contact (if it still exists).
402 Cursor c = resolver.query(Profile.CONTENT_URI,
403 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
404 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800405 if (c == null) {
406 continue;
407 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700408 try {
Erik162b7e32011-09-20 15:23:55 -0700409 if (c.moveToFirst()) {
410 final long contactId = c.getLong(0);
411 final String lookupKey = c.getString(1);
412 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
413 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700414 } finally {
415 c.close();
416 }
417 } else {
418 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
419 rawContactId);
420 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
421 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800422 if (lookupUri != null) {
423 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
424 }
Josh Garguse692e012012-01-18 14:53:11 -0800425
426 // We can change this back to false later, if we fail to save the contact photo.
427 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800428 break;
429
430 } catch (RemoteException e) {
431 // Something went wrong, bail without success
432 Log.e(TAG, "Problem persisting user edits", e);
433 break;
434
Jay Shrauner57fca182014-01-17 14:20:50 -0800435 } catch (IllegalArgumentException e) {
436 // This is thrown by applyBatch on malformed requests
437 Log.e(TAG, "Problem persisting user edits", e);
438 showToast(R.string.contactSavedErrorToast);
439 break;
440
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800441 } catch (OperationApplicationException e) {
442 // Version consistency failed, re-parent change and try again
443 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
444 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
445 boolean first = true;
446 final int count = state.size();
447 for (int i = 0; i < count; i++) {
448 Long rawContactId = state.getRawContactId(i);
449 if (rawContactId != null && rawContactId != -1) {
450 if (!first) {
451 sb.append(',');
452 }
453 sb.append(rawContactId);
454 first = false;
455 }
456 }
457 sb.append(")");
458
459 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800460 throw new IllegalStateException(
461 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800462 }
463
Maurice Chu851222a2012-06-21 11:43:08 -0700464 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700465 isProfile
466 ? RawContactsEntity.PROFILE_CONTENT_URI
467 : RawContactsEntity.CONTENT_URI,
468 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700469 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700470
471 // Update the new state to use profile URIs if appropriate.
472 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700473 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700474 delta.setProfileQueryUri();
475 }
476 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800477 }
478 }
479
Josh Garguse692e012012-01-18 14:53:11 -0800480 // Now save any updated photos. We do this at the end to ensure that
481 // the ContactProvider already knows about newly-created contacts.
482 if (updatedPhotos != null) {
483 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700484 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800485 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800486
487 // If the raw-contact ID is negative, we are saving a new raw-contact;
488 // replace the bogus ID with the new one that we actually saved the contact at.
489 if (rawContactId < 0) {
490 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800491 }
492
Jay Shrauner511561d2015-04-02 10:35:33 -0700493 // If the save failed, insertedRawContactId will be -1
494 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri)) {
495 succeeded = false;
496 }
Josh Garguse692e012012-01-18 14:53:11 -0800497 }
498 }
499
Josh Garguse5d3f892012-04-11 11:56:15 -0700500 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
501 if (callbackIntent != null) {
502 if (succeeded) {
503 // Mark the intent to indicate that the save was successful (even if the lookup URI
504 // is now null). For local contacts or the local profile, it's possible that the
505 // save triggered removal of the contact, so no lookup URI would exist..
506 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
507 }
508 callbackIntent.setData(lookupUri);
509 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800510 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800511 }
512
Josh Garguse692e012012-01-18 14:53:11 -0800513 /**
514 * Save updated photo for the specified raw-contact.
515 * @return true for success, false for failure
516 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700517 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800518 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800519 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
520 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
521
Yorke Lee637a38e2013-09-14 08:36:33 -0700522 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
Josh Garguse692e012012-01-18 14:53:11 -0800523 }
524
Josh Gargusef15c8e2012-01-30 16:42:02 -0800525 /**
526 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
527 */
Maurice Chu851222a2012-06-21 11:43:08 -0700528 private long getRawContactId(RawContactDeltaList state,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800529 final ArrayList<ContentProviderOperation> diff,
530 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800531 long existingRawContactId = state.findRawContactId();
532 if (existingRawContactId != -1) {
533 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800534 }
535
Josh Gargusef15c8e2012-01-30 16:42:02 -0800536 return getInsertedRawContactId(diff, results);
537 }
538
539 /**
540 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
541 */
542 private long getInsertedRawContactId(
543 final ArrayList<ContentProviderOperation> diff,
544 final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800545 if (results == null) {
546 return -1;
547 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800548 final int diffSize = diff.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800549 final int numResults = results.length;
550 for (int i = 0; i < diffSize && i < numResults; i++) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800551 ContentProviderOperation operation = diff.get(i);
Brian Attwell13f94e12015-01-22 16:27:48 -0800552 if (operation.isInsert() && operation.getUri().getEncodedPath().contains(
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800553 RawContacts.CONTENT_URI.getEncodedPath())) {
554 return ContentUris.parseId(results[i].uri);
555 }
556 }
557 return -1;
558 }
559
560 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700561 * Creates an intent that can be sent to this service to create a new group as
562 * well as add new members at the same time.
563 *
564 * @param context of the application
565 * @param account in which the group should be created
566 * @param label is the name of the group (cannot be null)
567 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
568 * should be added to the group
569 * @param callbackActivity is the activity to send the callback intent to
570 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700571 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700572 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700573 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700574 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800575 Intent serviceIntent = new Intent(context, ContactSaveService.class);
576 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
577 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
578 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700579 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800580 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700581 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700582
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800583 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700584 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800585 Intent callbackIntent = new Intent(context, callbackActivity);
586 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700587 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800588
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700589 return serviceIntent;
590 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800591
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800592 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700593 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
594 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
595 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
596 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700597 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800598
599 ContentValues values = new ContentValues();
600 values.put(Groups.ACCOUNT_TYPE, accountType);
601 values.put(Groups.ACCOUNT_NAME, accountName);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700602 values.put(Groups.DATA_SET, dataSet);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800603 values.put(Groups.TITLE, label);
604
Katherine Kuan717e3432011-07-13 17:03:24 -0700605 final ContentResolver resolver = getContentResolver();
606
607 // Create the new group
608 final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
609
610 // If there's no URI, then the insertion failed. Abort early because group members can't be
611 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800612 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700613 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800614 return;
615 }
616
Katherine Kuan717e3432011-07-13 17:03:24 -0700617 // Add new group members
618 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
619
620 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
621 // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800622 values.clear();
623 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
624 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
625
626 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700627 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700628 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800629 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800630 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800631 }
632
633 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800634 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800635 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700636 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700637 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800638 Intent serviceIntent = new Intent(context, ContactSaveService.class);
639 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
640 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
641 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700642
643 // Callback intent will be invoked by the service once the group is renamed.
644 Intent callbackIntent = new Intent(context, callbackActivity);
645 callbackIntent.setAction(callbackAction);
646 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
647
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800648 return serviceIntent;
649 }
650
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800651 private void renameGroup(Intent intent) {
652 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
653 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
654
655 if (groupId == -1) {
656 Log.e(TAG, "Invalid arguments for renameGroup request");
657 return;
658 }
659
660 ContentValues values = new ContentValues();
661 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700662 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
663 getContentResolver().update(groupUri, values, null, null);
664
665 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
666 callbackIntent.setData(groupUri);
667 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800668 }
669
670 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800671 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800672 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800673 public static Intent createGroupDeletionIntent(Context context, long groupId) {
674 Intent serviceIntent = new Intent(context, ContactSaveService.class);
675 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800676 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800677 return serviceIntent;
678 }
679
680 private void deleteGroup(Intent intent) {
681 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
682 if (groupId == -1) {
683 Log.e(TAG, "Invalid arguments for deleteGroup request");
684 return;
685 }
686
687 getContentResolver().delete(
688 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
689 }
690
691 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700692 * Creates an intent that can be sent to this service to rename a group as
693 * well as add and remove members from the group.
694 *
695 * @param context of the application
696 * @param groupId of the group that should be modified
697 * @param newLabel is the updated name of the group (can be null if the name
698 * should not be updated)
699 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
700 * should be added to the group
701 * @param rawContactsToRemove is an array of raw contact IDs for contacts
702 * that should be removed from the group
703 * @param callbackActivity is the activity to send the callback intent to
704 * @param callbackAction is the intent action for the callback intent
705 */
706 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
707 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700708 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700709 Intent serviceIntent = new Intent(context, ContactSaveService.class);
710 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
711 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
712 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
713 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
714 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
715 rawContactsToRemove);
716
717 // Callback intent will be invoked by the service once the group is updated
718 Intent callbackIntent = new Intent(context, callbackActivity);
719 callbackIntent.setAction(callbackAction);
720 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
721
722 return serviceIntent;
723 }
724
725 private void updateGroup(Intent intent) {
726 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
727 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
728 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
729 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
730
731 if (groupId == -1) {
732 Log.e(TAG, "Invalid arguments for updateGroup request");
733 return;
734 }
735
736 final ContentResolver resolver = getContentResolver();
737 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
738
739 // Update group name if necessary
740 if (label != null) {
741 ContentValues values = new ContentValues();
742 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700743 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700744 }
745
Katherine Kuan717e3432011-07-13 17:03:24 -0700746 // Add and remove members if necessary
747 addMembersToGroup(resolver, rawContactsToAdd, groupId);
748 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
749
750 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
751 callbackIntent.setData(groupUri);
752 deliverCallback(callbackIntent);
753 }
754
Daniel Lehmann18958a22012-02-28 17:45:25 -0800755 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700756 long groupId) {
757 if (rawContactsToAdd == null) {
758 return;
759 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700760 for (long rawContactId : rawContactsToAdd) {
761 try {
762 final ArrayList<ContentProviderOperation> rawContactOperations =
763 new ArrayList<ContentProviderOperation>();
764
765 // Build an assert operation to ensure the contact is not already in the group
766 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
767 .newAssertQuery(Data.CONTENT_URI);
768 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
769 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
770 new String[] { String.valueOf(rawContactId),
771 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
772 assertBuilder.withExpectedCount(0);
773 rawContactOperations.add(assertBuilder.build());
774
775 // Build an insert operation to add the contact to the group
776 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
777 .newInsert(Data.CONTENT_URI);
778 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
779 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
780 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
781 rawContactOperations.add(insertBuilder.build());
782
783 if (DEBUG) {
784 for (ContentProviderOperation operation : rawContactOperations) {
785 Log.v(TAG, operation.toString());
786 }
787 }
788
789 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700790 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800791 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700792 }
793 } catch (RemoteException e) {
794 // Something went wrong, bail without success
795 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
796 String.valueOf(rawContactId), e);
797 } catch (OperationApplicationException e) {
798 // The assert could have failed because the contact is already in the group,
799 // just continue to the next contact
800 Log.w(TAG, "Assert failed in adding raw contact ID " +
801 String.valueOf(rawContactId) + ". Already exists in group " +
802 String.valueOf(groupId), e);
803 }
804 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700805 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700806
Daniel Lehmann18958a22012-02-28 17:45:25 -0800807 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700808 long groupId) {
809 if (rawContactsToRemove == null) {
810 return;
811 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700812 for (long rawContactId : rawContactsToRemove) {
813 // Apply the delete operation on the data row for the given raw contact's
814 // membership in the given group. If no contact matches the provided selection, then
815 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800816 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700817 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
818 new String[] { String.valueOf(rawContactId),
819 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
820 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700821 }
822
823 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800824 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800825 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800826 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
827 Intent serviceIntent = new Intent(context, ContactSaveService.class);
828 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
829 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
830 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
831
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800832 return serviceIntent;
833 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800834
835 private void setStarred(Intent intent) {
836 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
837 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
838 if (contactUri == null) {
839 Log.e(TAG, "Invalid arguments for setStarred request");
840 return;
841 }
842
843 final ContentValues values = new ContentValues(1);
844 values.put(Contacts.STARRED, value);
845 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -0700846
847 // Undemote the contact if necessary
848 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
849 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -0800850 if (c == null) {
851 return;
852 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700853 try {
854 if (c.moveToFirst()) {
855 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -0700856
857 // Don't bother undemoting if this contact is the user's profile.
858 if (id < Profile.MIN_ID) {
Brian Attwell2d88efa2014-12-17 21:49:56 -0800859 PinnedPositions.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -0700860 }
Yorke Leee8e3fb82013-09-12 17:53:31 -0700861 }
862 } finally {
863 c.close();
864 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800865 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800866
867 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700868 * Creates an intent that can be sent to this service to set the redirect to voicemail.
869 */
870 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
871 boolean value) {
872 Intent serviceIntent = new Intent(context, ContactSaveService.class);
873 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
874 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
875 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
876
877 return serviceIntent;
878 }
879
880 private void setSendToVoicemail(Intent intent) {
881 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
882 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
883 if (contactUri == null) {
884 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
885 return;
886 }
887
888 final ContentValues values = new ContentValues(1);
889 values.put(Contacts.SEND_TO_VOICEMAIL, value);
890 getContentResolver().update(contactUri, values, null, null);
891 }
892
893 /**
894 * Creates an intent that can be sent to this service to save the contact's ringtone.
895 */
896 public static Intent createSetRingtone(Context context, Uri contactUri,
897 String value) {
898 Intent serviceIntent = new Intent(context, ContactSaveService.class);
899 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
900 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
901 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
902
903 return serviceIntent;
904 }
905
906 private void setRingtone(Intent intent) {
907 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
908 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
909 if (contactUri == null) {
910 Log.e(TAG, "Invalid arguments for setRingtone");
911 return;
912 }
913 ContentValues values = new ContentValues(1);
914 values.put(Contacts.CUSTOM_RINGTONE, value);
915 getContentResolver().update(contactUri, values, null, null);
916 }
917
918 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800919 * Creates an intent that sets the selected data item as super primary (default)
920 */
921 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
922 Intent serviceIntent = new Intent(context, ContactSaveService.class);
923 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
924 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
925 return serviceIntent;
926 }
927
928 private void setSuperPrimary(Intent intent) {
929 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
930 if (dataId == -1) {
931 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
932 return;
933 }
934
Chiao Chengd7ca03e2012-10-24 15:14:08 -0700935 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800936 }
937
938 /**
939 * Creates an intent that clears the primary flag of all data items that belong to the same
940 * raw_contact as the given data item. Will only clear, if the data item was primary before
941 * this call
942 */
943 public static Intent createClearPrimaryIntent(Context context, long dataId) {
944 Intent serviceIntent = new Intent(context, ContactSaveService.class);
945 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
946 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
947 return serviceIntent;
948 }
949
950 private void clearPrimary(Intent intent) {
951 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
952 if (dataId == -1) {
953 Log.e(TAG, "Invalid arguments for clearPrimary request");
954 return;
955 }
956
957 // Update the primary values in the data record.
958 ContentValues values = new ContentValues(1);
959 values.put(Data.IS_SUPER_PRIMARY, 0);
960 values.put(Data.IS_PRIMARY, 0);
961
962 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
963 values, null, null);
964 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800965
966 /**
967 * Creates an intent that can be sent to this service to delete a contact.
968 */
969 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
970 Intent serviceIntent = new Intent(context, ContactSaveService.class);
971 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
972 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
973 return serviceIntent;
974 }
975
Brian Attwelld2962a32015-03-02 14:48:50 -0800976 /**
977 * Creates an intent that can be sent to this service to delete multiple contacts.
978 */
979 public static Intent createDeleteMultipleContactsIntent(Context context,
980 long[] contactIds) {
981 Intent serviceIntent = new Intent(context, ContactSaveService.class);
982 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
983 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
984 return serviceIntent;
985 }
986
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800987 private void deleteContact(Intent intent) {
988 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
989 if (contactUri == null) {
990 Log.e(TAG, "Invalid arguments for deleteContact request");
991 return;
992 }
993
994 getContentResolver().delete(contactUri, null, null);
995 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800996
Brian Attwelld2962a32015-03-02 14:48:50 -0800997 private void deleteMultipleContacts(Intent intent) {
998 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
999 if (contactIds == null) {
1000 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1001 return;
1002 }
1003 for (long contactId : contactIds) {
1004 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1005 getContentResolver().delete(contactUri, null, null);
1006 }
Brian Attwelle986c6b2015-03-05 19:47:30 -08001007 showToast(R.string.contacts_deleted_toast);
Brian Attwelld2962a32015-03-02 14:48:50 -08001008 }
1009
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001010 /**
1011 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001012 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001013 */
1014 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001015 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001016 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1017 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1018 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1019 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001020
1021 // Callback intent will be invoked by the service once the contacts are joined.
1022 Intent callbackIntent = new Intent(context, callbackActivity);
1023 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001024 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1025
1026 return serviceIntent;
1027 }
1028
Brian Attwelld3946ca2015-03-03 11:13:49 -08001029 /**
1030 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1031 * No special attention is paid to where the resulting contact's name is taken from.
1032 */
1033 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1034 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1035 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1036 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1037 return serviceIntent;
1038 }
1039
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001040
1041 private interface JoinContactQuery {
1042 String[] PROJECTION = {
1043 RawContacts._ID,
1044 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001045 RawContacts.DISPLAY_NAME_SOURCE,
1046 };
1047
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001048 int _ID = 0;
1049 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001050 int DISPLAY_NAME_SOURCE = 2;
1051 }
1052
1053 private interface ContactEntityQuery {
1054 String[] PROJECTION = {
1055 Contacts.Entity.DATA_ID,
1056 Contacts.Entity.CONTACT_ID,
1057 Contacts.Entity.IS_SUPER_PRIMARY,
1058 };
1059 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1060 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1061 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1062 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1063
1064 int DATA_ID = 0;
1065 int CONTACT_ID = 1;
1066 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001067 }
1068
Brian Attwelld3946ca2015-03-03 11:13:49 -08001069 private void joinSeveralContacts(Intent intent) {
1070 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Brian Attwell548f5c62015-01-27 17:46:46 -08001071
Brian Attwelld3946ca2015-03-03 11:13:49 -08001072 // Load raw contact IDs for all contacts involved.
1073 long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1074 if (rawContactIds == null) {
1075 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001076 return;
1077 }
1078
Brian Attwelld3946ca2015-03-03 11:13:49 -08001079 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001080 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001081 final ArrayList<ContentProviderOperation> operations
1082 = new ArrayList<ContentProviderOperation>();
1083 for (int i = 0; i < rawContactIds.length; i++) {
1084 for (int j = 0; j < rawContactIds.length; j++) {
1085 if (i != j) {
1086 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1087 }
1088 }
1089 }
1090
1091 // Apply all aggregation exceptions as one batch
1092 try {
1093 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1094 showToast(R.string.contactsJoinedMessage);
1095 } catch (RemoteException | OperationApplicationException e) {
1096 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1097 showToast(R.string.contactSavedErrorToast);
1098 }
1099 }
1100
1101
1102 private void joinContacts(Intent intent) {
1103 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1104 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001105
1106 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001107 // in the join UIs.
1108 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1109 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001110 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001111 return;
1112 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001113
Brian Attwell548f5c62015-01-27 17:46:46 -08001114 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001115
1116 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001117 for (int i = 0; i < rawContactIds.length; i++) {
1118 for (int j = 0; j < rawContactIds.length; j++) {
1119 if (i != j) {
1120 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1121 }
1122 }
1123 }
1124
Brian Attwelld3946ca2015-03-03 11:13:49 -08001125 final ContentResolver resolver = getContentResolver();
1126
Brian Attwell548f5c62015-01-27 17:46:46 -08001127 // Use the name for contactId1 as the name for the newly aggregated contact.
1128 final Uri contactId1Uri = ContentUris.withAppendedId(
1129 Contacts.CONTENT_URI, contactId1);
1130 final Uri entityUri = Uri.withAppendedPath(
1131 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1132 Cursor c = resolver.query(entityUri,
1133 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1134 if (c == null) {
1135 Log.e(TAG, "Unable to open Contacts DB cursor");
1136 showToast(R.string.contactSavedErrorToast);
1137 return;
1138 }
1139 long dataIdToAddSuperPrimary = -1;
1140 try {
1141 if (c.moveToFirst()) {
1142 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1143 }
1144 } finally {
1145 c.close();
1146 }
1147
1148 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1149 // display name does not change as a result of the join.
1150 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001151 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001152 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1153 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1154 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001155 operations.add(builder.build());
1156 }
1157
1158 boolean success = false;
1159 // Apply all aggregation exceptions as one batch
1160 try {
1161 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001162 showToast(R.string.contactsJoinedMessage);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001163 success = true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001164 } catch (RemoteException | OperationApplicationException e) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001165 Log.e(TAG, "Failed to apply aggregation exception batch", e);
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001166 showToast(R.string.contactSavedErrorToast);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001167 }
1168
1169 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1170 if (success) {
1171 Uri uri = RawContacts.getContactLookupUri(resolver,
1172 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1173 callbackIntent.setData(uri);
1174 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001175 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001176 }
1177
Brian Attwelld3946ca2015-03-03 11:13:49 -08001178 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1179 if (contactIds == null) {
1180 return null;
1181 }
1182
Brian Attwell548f5c62015-01-27 17:46:46 -08001183 final ContentResolver resolver = getContentResolver();
1184 long rawContactIds[];
Brian Attwelld3946ca2015-03-03 11:13:49 -08001185
1186 final StringBuilder queryBuilder = new StringBuilder();
1187 final String stringContactIds[] = new String[contactIds.length];
1188 for (int i = 0; i < contactIds.length; i++) {
1189 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1190 stringContactIds[i] = String.valueOf(contactIds[i]);
1191 if (contactIds[i] == -1) {
1192 return null;
1193 }
1194 if (i == contactIds.length -1) {
1195 break;
1196 }
1197 queryBuilder.append(" OR ");
1198 }
1199
Brian Attwell548f5c62015-01-27 17:46:46 -08001200 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1201 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001202 queryBuilder.toString(),
1203 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001204 if (c == null) {
1205 Log.e(TAG, "Unable to open Contacts DB cursor");
1206 showToast(R.string.contactSavedErrorToast);
1207 return null;
1208 }
1209 try {
1210 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001211 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001212 return null;
1213 }
1214 rawContactIds = new long[c.getCount()];
1215 for (int i = 0; i < rawContactIds.length; i++) {
1216 c.moveToPosition(i);
1217 long rawContactId = c.getLong(JoinContactQuery._ID);
1218 rawContactIds[i] = rawContactId;
1219 }
1220 } finally {
1221 c.close();
1222 }
1223 return rawContactIds;
1224 }
1225
Brian Attwelld3946ca2015-03-03 11:13:49 -08001226 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1227 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1228 }
1229
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001230 /**
1231 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1232 */
1233 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1234 long rawContactId1, long rawContactId2) {
1235 Builder builder =
1236 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1237 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1238 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1239 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1240 operations.add(builder.build());
1241 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001242
1243 /**
1244 * Shows a toast on the UI thread.
1245 */
1246 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001247 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001248
1249 @Override
1250 public void run() {
1251 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1252 }
1253 });
1254 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001255
1256 private void deliverCallback(final Intent callbackIntent) {
1257 mMainHandler.post(new Runnable() {
1258
1259 @Override
1260 public void run() {
1261 deliverCallbackOnUiThread(callbackIntent);
1262 }
1263 });
1264 }
1265
1266 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1267 // TODO: this assumes that if there are multiple instances of the same
1268 // activity registered, the last one registered is the one waiting for
1269 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001270 for (Listener listener : sListeners) {
1271 if (callbackIntent.getComponent().equals(
1272 ((Activity) listener).getIntent().getComponent())) {
1273 listener.onServiceCompleted(callbackIntent);
1274 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001275 }
1276 }
1277 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001278}