blob: 1ed36b54ff3c637e9e4dfc34f35d81d6863383b6 [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;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070031import android.database.DatabaseUtils;
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;
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;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070048import android.support.v4.content.LocalBroadcastManager;
Gary Mai7efa9942016-05-12 11:26:49 -070049import android.support.v4.os.ResultReceiver;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070050import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080051import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070052
Gary Mai363af602016-09-28 10:01:23 -070053import com.android.contacts.activities.ContactEditorActivity;
Wenyi Wang67addcc2015-11-23 10:07:48 -080054import com.android.contacts.common.compat.CompatUtils;
Chiao Chengd7ca03e2012-10-24 15:14:08 -070055import com.android.contacts.common.database.ContactUpdateUtils;
Marcus Hagerott819214d2016-09-29 14:58:27 -070056import com.android.contacts.common.database.SimContactDao;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080057import com.android.contacts.common.model.AccountTypeManager;
Wenyi Wang67addcc2015-11-23 10:07:48 -080058import com.android.contacts.common.model.CPOWrapper;
Yorke Leecd321f62013-10-28 15:20:15 -070059import com.android.contacts.common.model.RawContactDelta;
60import com.android.contacts.common.model.RawContactDeltaList;
61import com.android.contacts.common.model.RawContactModifier;
Marcus Hagerott819214d2016-09-29 14:58:27 -070062import com.android.contacts.common.model.SimContact;
Chiao Cheng428f0082012-11-13 18:38:56 -080063import com.android.contacts.common.model.account.AccountWithDataSet;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070064import com.android.contacts.common.util.PermissionsUtil;
Wenyi Wangaac0e662015-12-18 17:17:33 -080065import com.android.contacts.compat.PinnedPositionsCompat;
Yorke Lee637a38e2013-09-14 08:36:33 -070066import com.android.contacts.util.ContactPhotoUtils;
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070067import com.google.common.collect.Lists;
68import com.google.common.collect.Sets;
69
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080070import java.util.ArrayList;
71import java.util.HashSet;
72import java.util.List;
73import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070074
Marcus Hagerott819214d2016-09-29 14:58:27 -070075import static android.Manifest.permission.WRITE_CONTACTS;
76
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080077/**
78 * A service responsible for saving changes to the content provider.
79 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070080public class ContactSaveService extends IntentService {
81 private static final String TAG = "ContactSaveService";
82
Katherine Kuana007e442011-07-07 09:25:34 -070083 /** Set to true in order to view logs on content provider operations */
84 private static final boolean DEBUG = false;
85
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070086 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
87
88 public static final String EXTRA_ACCOUNT_NAME = "accountName";
89 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070090 public static final String EXTRA_DATA_SET = "dataSet";
Marcus Hagerott819214d2016-09-29 14:58:27 -070091 public static final String EXTRA_ACCOUNT = "account";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070092 public static final String EXTRA_CONTENT_VALUES = "contentValues";
93 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
Gary Mai7efa9942016-05-12 11:26:49 -070094 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
95 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070096
Dmitri Plotnikova0114142011-02-15 13:53:21 -080097 public static final String ACTION_SAVE_CONTACT = "saveContact";
98 public static final String EXTRA_CONTACT_STATE = "state";
99 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700100 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -0700101 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -0800102 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700103
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800104 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800105 public static final String ACTION_RENAME_GROUP = "renameGroup";
106 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700107 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800108 public static final String EXTRA_GROUP_ID = "groupId";
109 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700110 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
111 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800112
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800113 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800114 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800115 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800116 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800117 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800118 public static final String EXTRA_STARRED_FLAG = "starred";
Marcus Hagerott3bb85142016-07-29 10:46:36 -0700119 public static final String EXTRA_DISPLAY_NAME = "extraDisplayName";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800120
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800121 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
122 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
123 public static final String EXTRA_DATA_ID = "dataId";
124
Gary Mai7efa9942016-05-12 11:26:49 -0700125 public static final String ACTION_SPLIT_CONTACT = "splitContact";
126
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800127 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800128 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800129 public static final String EXTRA_CONTACT_ID1 = "contactId1";
130 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800131
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700132 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
133 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
134
135 public static final String ACTION_SET_RINGTONE = "setRingtone";
136 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
137
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700138 public static final String ACTION_UNDO = "undo";
139 public static final String EXTRA_UNDO_ACTION = "undoAction";
140 public static final String EXTRA_UNDO_DATA = "undoData";
141
Marcus Hagerott819214d2016-09-29 14:58:27 -0700142 public static final String ACTION_IMPORT_FROM_SIM = "importFromSim";
143 public static final String EXTRA_SIM_CONTACTS = "simContacts";
144
145 public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
146 public static final String BROADCAST_SIM_IMPORT_COMPLETE = "simImportComplete";
147
148 public static final String EXTRA_RESULT_CODE = "resultCode";
149 public static final String EXTRA_RESULT_COUNT = "count";
150 public static final String EXTRA_OPERATION_REQUESTED_AT_TIME = "requestedTime";
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700151
Gary Mai7efa9942016-05-12 11:26:49 -0700152 public static final int CP2_ERROR = 0;
153 public static final int CONTACTS_LINKED = 1;
154 public static final int CONTACTS_SPLIT = 2;
Gary Mai31d572e2016-06-03 14:04:32 -0700155 public static final int BAD_ARGUMENTS = 3;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700156 public static final int RESULT_UNKNOWN = 0;
157 public static final int RESULT_SUCCESS = 1;
158 public static final int RESULT_FAILURE = 2;
Gary Mai7efa9942016-05-12 11:26:49 -0700159
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700160 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
161 Data.MIMETYPE,
162 Data.IS_PRIMARY,
163 Data.DATA1,
164 Data.DATA2,
165 Data.DATA3,
166 Data.DATA4,
167 Data.DATA5,
168 Data.DATA6,
169 Data.DATA7,
170 Data.DATA8,
171 Data.DATA9,
172 Data.DATA10,
173 Data.DATA11,
174 Data.DATA12,
175 Data.DATA13,
176 Data.DATA14,
177 Data.DATA15
178 );
179
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800180 private static final int PERSIST_TRIES = 3;
181
Walter Jang0653de32015-07-24 12:12:40 -0700182 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
183
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800184 public interface Listener {
185 public void onServiceCompleted(Intent callbackIntent);
186 }
187
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100188 private static final CopyOnWriteArrayList<Listener> sListeners =
189 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800190
191 private Handler mMainHandler;
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700192 private GroupsDao mGroupsDao;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700193 private SimContactDao mSimContactDao;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800194
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700195 public ContactSaveService() {
196 super(TAG);
197 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800198 mMainHandler = new Handler(Looper.getMainLooper());
199 }
200
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700201 @Override
202 public void onCreate() {
203 super.onCreate();
204 mGroupsDao = new GroupsDaoImpl(this);
Marcus Hagerott819214d2016-09-29 14:58:27 -0700205 mSimContactDao = new SimContactDao(this);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700206 }
207
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800208 public static void registerListener(Listener listener) {
209 if (!(listener instanceof Activity)) {
210 throw new ClassCastException("Only activities can be registered to"
211 + " receive callback from " + ContactSaveService.class.getName());
212 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100213 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800214 }
215
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700216 public static boolean canUndo(Intent resultIntent) {
217 return resultIntent.hasExtra(EXTRA_UNDO_DATA);
218 }
219
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800220 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100221 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700222 }
223
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800224 /**
225 * Returns true if the ContactSaveService was started successfully and false if an exception
226 * was thrown and a Toast error message was displayed.
227 */
228 public static boolean startService(Context context, Intent intent, int saveMode) {
229 try {
230 context.startService(intent);
231 } catch (Exception exception) {
232 final int resId;
233 switch (saveMode) {
Gary Mai363af602016-09-28 10:01:23 -0700234 case ContactEditorActivity.ContactEditor.SaveMode.SPLIT:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800235 resId = R.string.contactUnlinkErrorToast;
236 break;
Gary Mai363af602016-09-28 10:01:23 -0700237 case ContactEditorActivity.ContactEditor.SaveMode.RELOAD:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800238 resId = R.string.contactJoinErrorToast;
239 break;
Gary Mai363af602016-09-28 10:01:23 -0700240 case ContactEditorActivity.ContactEditor.SaveMode.CLOSE:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800241 resId = R.string.contactSavedErrorToast;
242 break;
243 default:
244 resId = R.string.contactGenericErrorToast;
245 }
246 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
247 return false;
248 }
249 return true;
250 }
251
252 /**
253 * Utility method that starts service and handles exception.
254 */
255 public static void startService(Context context, Intent intent) {
256 try {
257 context.startService(intent);
258 } catch (Exception exception) {
259 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
260 }
261 }
262
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700263 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800264 public Object getSystemService(String name) {
265 Object service = super.getSystemService(name);
266 if (service != null) {
267 return service;
268 }
269
270 return getApplicationContext().getSystemService(name);
271 }
272
273 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700274 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800275 if (intent == null) {
276 Log.d(TAG, "onHandleIntent: could not handle null intent");
277 return;
278 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700279 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
280 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
281 // TODO: add more specific error string such as "Turn on Contacts
282 // permission to update your contacts"
283 showToast(R.string.contactSavedErrorToast);
284 return;
285 }
286
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700287 // Call an appropriate method. If we're sure it affects how incoming phone calls are
288 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700289 String action = intent.getAction();
290 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
291 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800292 } else if (ACTION_SAVE_CONTACT.equals(action)) {
293 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800294 } else if (ACTION_CREATE_GROUP.equals(action)) {
295 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800296 } else if (ACTION_RENAME_GROUP.equals(action)) {
297 renameGroup(intent);
298 } else if (ACTION_DELETE_GROUP.equals(action)) {
299 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700300 } else if (ACTION_UPDATE_GROUP.equals(action)) {
301 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800302 } else if (ACTION_SET_STARRED.equals(action)) {
303 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800304 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
305 setSuperPrimary(intent);
306 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
307 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800308 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
309 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800310 } else if (ACTION_DELETE_CONTACT.equals(action)) {
311 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700312 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
313 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800314 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
315 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800316 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
317 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700318 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
319 setSendToVoicemail(intent);
320 } else if (ACTION_SET_RINGTONE.equals(action)) {
321 setRingtone(intent);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700322 } else if (ACTION_UNDO.equals(action)) {
323 undo(intent);
Marcus Hagerott819214d2016-09-29 14:58:27 -0700324 } else if (ACTION_IMPORT_FROM_SIM.equals(action)) {
325 importFromSim(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700326 }
327 }
328
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800329 /**
330 * Creates an intent that can be sent to this service to create a new raw contact
331 * using data presented as a set of ContentValues.
332 */
333 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700334 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700335 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800336 Intent serviceIntent = new Intent(
337 context, ContactSaveService.class);
338 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
339 if (account != null) {
340 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
341 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700342 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800343 }
344 serviceIntent.putParcelableArrayListExtra(
345 ContactSaveService.EXTRA_CONTENT_VALUES, values);
346
347 // Callback intent will be invoked by the service once the new contact is
348 // created. The service will put the URI of the new contact as "data" on
349 // the callback intent.
350 Intent callbackIntent = new Intent(context, callbackActivity);
351 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800352 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
353 return serviceIntent;
354 }
355
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700356 private void createRawContact(Intent intent) {
357 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
358 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700359 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700360 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
361 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
362
363 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
364 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
365 .withValue(RawContacts.ACCOUNT_NAME, accountName)
366 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700367 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700368 .build());
369
370 int size = valueList.size();
371 for (int i = 0; i < size; i++) {
372 ContentValues values = valueList.get(i);
373 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
374 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
375 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
376 .withValues(values)
377 .build());
378 }
379
380 ContentResolver resolver = getContentResolver();
381 ContentProviderResult[] results;
382 try {
383 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
384 } catch (Exception e) {
385 throw new RuntimeException("Failed to store new contact", e);
386 }
387
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700388 Uri rawContactUri = results[0].uri;
389 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
390
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800391 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700392 }
393
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700394 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800395 * Creates an intent that can be sent to this service to create a new raw contact
396 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800397 * This variant is more convenient to use when there is only one photo that can
398 * possibly be updated, as in the Contact Details screen.
399 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
400 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800401 */
Maurice Chu851222a2012-06-21 11:43:08 -0700402 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700403 String saveModeExtraKey, int saveMode, boolean isProfile,
404 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700405 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800406 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700407 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800408 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700409 callbackActivity, callbackAction, bundle,
410 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800411 }
412
413 /**
414 * Creates an intent that can be sent to this service to create a new raw contact
415 * using data presented as a set of ContentValues.
416 * This variant is used when multiple contacts' photos may be updated, as in the
417 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700418 *
Josh Garguse692e012012-01-18 14:53:11 -0800419 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700420 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
421 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800422 */
Maurice Chu851222a2012-06-21 11:43:08 -0700423 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700424 String saveModeExtraKey, int saveMode, boolean isProfile,
425 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700426 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000427 Intent serviceIntent = new Intent(
428 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800429 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
430 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700431 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800432 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
433
Josh Garguse692e012012-01-18 14:53:11 -0800434 if (updatedPhotos != null) {
435 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
436 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800437
Josh Garguse5d3f892012-04-11 11:56:15 -0700438 if (callbackActivity != null) {
439 // Callback intent will be invoked by the service once the contact is
440 // saved. The service will put the URI of the new contact as "data" on
441 // the callback intent.
442 Intent callbackIntent = new Intent(context, callbackActivity);
443 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700444 if (joinContactIdExtraKey != null && joinContactId != null) {
445 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
446 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700447 callbackIntent.setAction(callbackAction);
448 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
449 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800450 return serviceIntent;
451 }
452
453 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700454 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700455 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800456 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800457
Jay Shrauner08099782015-03-25 14:17:11 -0700458 if (state == null) {
459 Log.e(TAG, "Invalid arguments for saveContact request");
460 return;
461 }
462
benny.lin3a4e7a22014-01-08 10:58:08 +0800463 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800464 // Trim any empty fields, and RawContacts, before persisting
465 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700466 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800467
468 Uri lookupUri = null;
469
470 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700471
Josh Garguse692e012012-01-18 14:53:11 -0800472 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800473
Josh Gargusef15c8e2012-01-30 16:42:02 -0800474 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
475 long insertedRawContactId = -1;
476
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800477 // Attempt to persist changes
478 int tries = 0;
479 while (tries++ < PERSIST_TRIES) {
480 try {
481 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800482 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
483
484 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
485
486 for (CPOWrapper cpoWrapper : diffWrapper) {
487 diff.add(cpoWrapper.getOperation());
488 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700489
Katherine Kuana007e442011-07-07 09:25:34 -0700490 if (DEBUG) {
491 Log.v(TAG, "Content Provider Operations:");
492 for (ContentProviderOperation operation : diff) {
493 Log.v(TAG, operation.toString());
494 }
495 }
496
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700497 int numberProcessed = 0;
498 boolean batchFailed = false;
499 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
500 while (numberProcessed < diff.size()) {
501 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
502 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700503 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700504 batchFailed = true;
505 break;
506 } else {
507 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700508 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800509 }
510
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700511 if (batchFailed) {
512 // Retry save
513 continue;
514 }
515
Wenyi Wang67addcc2015-11-23 10:07:48 -0800516 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800517 if (rawContactId == -1) {
518 throw new IllegalStateException("Could not determine RawContact ID after save");
519 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800520 // We don't have to check to see if the value is still -1. If we reach here,
521 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800522 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700523 if (isProfile) {
524 // Since the profile supports local raw contacts, which may have been completely
525 // removed if all information was removed, we need to do a special query to
526 // get the lookup URI for the profile contact (if it still exists).
527 Cursor c = resolver.query(Profile.CONTENT_URI,
528 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
529 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800530 if (c == null) {
531 continue;
532 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700533 try {
Erik162b7e32011-09-20 15:23:55 -0700534 if (c.moveToFirst()) {
535 final long contactId = c.getLong(0);
536 final String lookupKey = c.getString(1);
537 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
538 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700539 } finally {
540 c.close();
541 }
542 } else {
543 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
544 rawContactId);
545 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
546 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800547 if (lookupUri != null) {
548 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
549 }
Josh Garguse692e012012-01-18 14:53:11 -0800550
551 // We can change this back to false later, if we fail to save the contact photo.
552 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800553 break;
554
555 } catch (RemoteException e) {
556 // Something went wrong, bail without success
557 Log.e(TAG, "Problem persisting user edits", e);
558 break;
559
Jay Shrauner57fca182014-01-17 14:20:50 -0800560 } catch (IllegalArgumentException e) {
561 // This is thrown by applyBatch on malformed requests
562 Log.e(TAG, "Problem persisting user edits", e);
563 showToast(R.string.contactSavedErrorToast);
564 break;
565
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800566 } catch (OperationApplicationException e) {
567 // Version consistency failed, re-parent change and try again
568 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
569 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
570 boolean first = true;
571 final int count = state.size();
572 for (int i = 0; i < count; i++) {
573 Long rawContactId = state.getRawContactId(i);
574 if (rawContactId != null && rawContactId != -1) {
575 if (!first) {
576 sb.append(',');
577 }
578 sb.append(rawContactId);
579 first = false;
580 }
581 }
582 sb.append(")");
583
584 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800585 throw new IllegalStateException(
586 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800587 }
588
Maurice Chu851222a2012-06-21 11:43:08 -0700589 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700590 isProfile
591 ? RawContactsEntity.PROFILE_CONTENT_URI
592 : RawContactsEntity.CONTENT_URI,
593 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700594 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700595
596 // Update the new state to use profile URIs if appropriate.
597 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700598 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700599 delta.setProfileQueryUri();
600 }
601 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800602 }
603 }
604
Josh Garguse692e012012-01-18 14:53:11 -0800605 // Now save any updated photos. We do this at the end to ensure that
606 // the ContactProvider already knows about newly-created contacts.
607 if (updatedPhotos != null) {
608 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700609 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800610 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800611
612 // If the raw-contact ID is negative, we are saving a new raw-contact;
613 // replace the bogus ID with the new one that we actually saved the contact at.
614 if (rawContactId < 0) {
615 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800616 }
617
Jay Shrauner511561d2015-04-02 10:35:33 -0700618 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700619 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700620 succeeded = false;
621 }
Josh Garguse692e012012-01-18 14:53:11 -0800622 }
623 }
624
Josh Garguse5d3f892012-04-11 11:56:15 -0700625 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
626 if (callbackIntent != null) {
627 if (succeeded) {
628 // Mark the intent to indicate that the save was successful (even if the lookup URI
629 // is now null). For local contacts or the local profile, it's possible that the
630 // save triggered removal of the contact, so no lookup URI would exist..
631 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
632 }
633 callbackIntent.setData(lookupUri);
634 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800635 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800636 }
637
Josh Garguse692e012012-01-18 14:53:11 -0800638 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700639 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
640 * subsets, adds the returned array to "results".
641 *
642 * @return the size of the array, if not null; -1 when the array is null.
643 */
644 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
645 ContentProviderResult[] results, ContentResolver resolver)
646 throws RemoteException, OperationApplicationException {
647 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
648 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
649 subset.addAll(diff.subList(offset, offset + subsetCount));
650 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
651 .AUTHORITY, subset);
652 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
653 return -1;
654 }
655 for (ContentProviderResult c : subsetResult) {
656 results[offset++] = c;
657 }
658 return subsetResult.length;
659 }
660
661 /**
Josh Garguse692e012012-01-18 14:53:11 -0800662 * Save updated photo for the specified raw-contact.
663 * @return true for success, false for failure
664 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800665 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800666 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800667 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
668 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
669
benny.lin3a4e7a22014-01-08 10:58:08 +0800670 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800671 }
672
Josh Gargusef15c8e2012-01-30 16:42:02 -0800673 /**
674 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
675 */
Maurice Chu851222a2012-06-21 11:43:08 -0700676 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800677 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800678 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800679 long existingRawContactId = state.findRawContactId();
680 if (existingRawContactId != -1) {
681 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800682 }
683
Wenyi Wang67addcc2015-11-23 10:07:48 -0800684 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800685 }
686
687 /**
688 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
689 */
690 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800691 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800692 if (results == null) {
693 return -1;
694 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800695 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800696 final int numResults = results.length;
697 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800698 final CPOWrapper cpoWrapper = diffWrapper.get(i);
699 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
700 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
701 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800702 return ContentUris.parseId(results[i].uri);
703 }
704 }
705 return -1;
706 }
707
708 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700709 * Creates an intent that can be sent to this service to create a new group as
710 * well as add new members at the same time.
711 *
712 * @param context of the application
713 * @param account in which the group should be created
714 * @param label is the name of the group (cannot be null)
715 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
716 * should be added to the group
717 * @param callbackActivity is the activity to send the callback intent to
718 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700719 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700720 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700721 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700722 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800723 Intent serviceIntent = new Intent(context, ContactSaveService.class);
724 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
725 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
726 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700727 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800728 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700729 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700730
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800731 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700732 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800733 Intent callbackIntent = new Intent(context, callbackActivity);
734 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700735 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800736
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700737 return serviceIntent;
738 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800739
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800740 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700741 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
742 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
743 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
744 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700745 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800746
Katherine Kuan717e3432011-07-13 17:03:24 -0700747 // Create the new group
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700748 final Uri groupUri = mGroupsDao.create(label,
749 new AccountWithDataSet(accountName, accountType, dataSet));
750 final ContentResolver resolver = getContentResolver();
Katherine Kuan717e3432011-07-13 17:03:24 -0700751
752 // If there's no URI, then the insertion failed. Abort early because group members can't be
753 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800754 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700755 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800756 return;
757 }
758
Katherine Kuan717e3432011-07-13 17:03:24 -0700759 // Add new group members
760 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
761
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700762 ContentValues values = new ContentValues();
Katherine Kuan717e3432011-07-13 17:03:24 -0700763 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
Walter Jang8bac28b2016-08-30 10:34:55 -0700764 // with the way other intent extras that are passed to the
Gary Mai363af602016-09-28 10:01:23 -0700765 // {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800766 values.clear();
767 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
768 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
769
770 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700771 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700772 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800773 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800774 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800775 }
776
777 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800778 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800779 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700780 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700781 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800782 Intent serviceIntent = new Intent(context, ContactSaveService.class);
783 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
784 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
785 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700786
787 // Callback intent will be invoked by the service once the group is renamed.
788 Intent callbackIntent = new Intent(context, callbackActivity);
789 callbackIntent.setAction(callbackAction);
790 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
791
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800792 return serviceIntent;
793 }
794
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800795 private void renameGroup(Intent intent) {
796 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
797 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
798
799 if (groupId == -1) {
800 Log.e(TAG, "Invalid arguments for renameGroup request");
801 return;
802 }
803
804 ContentValues values = new ContentValues();
805 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700806 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
807 getContentResolver().update(groupUri, values, null, null);
808
809 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
810 callbackIntent.setData(groupUri);
811 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800812 }
813
814 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800815 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800816 */
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700817 public static Intent createGroupDeletionIntent(Context context, long groupId) {
818 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800819 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800820 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700821
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800822 return serviceIntent;
823 }
824
825 private void deleteGroup(Intent intent) {
826 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
827 if (groupId == -1) {
828 Log.e(TAG, "Invalid arguments for deleteGroup request");
829 return;
830 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700831 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800832
Marcus Hagerott819214d2016-09-29 14:58:27 -0700833 final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700834 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
835 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
836 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
Walter Jang72f99882016-05-26 09:01:31 -0700837
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700838 mGroupsDao.delete(groupUri);
839
840 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
841 }
842
843 public static Intent createUndoIntent(Context context, Intent resultIntent) {
844 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
845 serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
846 serviceIntent.putExtras(resultIntent);
847 return serviceIntent;
848 }
849
850 private void undo(Intent intent) {
851 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
852 if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
853 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
Walter Jang72f99882016-05-26 09:01:31 -0700854 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800855 }
856
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700857
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800858 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700859 * Creates an intent that can be sent to this service to rename a group as
860 * well as add and remove members from the group.
861 *
862 * @param context of the application
863 * @param groupId of the group that should be modified
864 * @param newLabel is the updated name of the group (can be null if the name
865 * should not be updated)
866 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
867 * should be added to the group
868 * @param rawContactsToRemove is an array of raw contact IDs for contacts
869 * that should be removed from the group
870 * @param callbackActivity is the activity to send the callback intent to
871 * @param callbackAction is the intent action for the callback intent
872 */
873 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
874 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700875 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700876 Intent serviceIntent = new Intent(context, ContactSaveService.class);
877 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
878 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
879 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
880 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
881 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
882 rawContactsToRemove);
883
884 // Callback intent will be invoked by the service once the group is updated
885 Intent callbackIntent = new Intent(context, callbackActivity);
886 callbackIntent.setAction(callbackAction);
887 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
888
889 return serviceIntent;
890 }
891
892 private void updateGroup(Intent intent) {
893 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
894 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
895 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
896 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
897
898 if (groupId == -1) {
899 Log.e(TAG, "Invalid arguments for updateGroup request");
900 return;
901 }
902
903 final ContentResolver resolver = getContentResolver();
904 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
905
906 // Update group name if necessary
907 if (label != null) {
908 ContentValues values = new ContentValues();
909 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700910 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700911 }
912
Katherine Kuan717e3432011-07-13 17:03:24 -0700913 // Add and remove members if necessary
914 addMembersToGroup(resolver, rawContactsToAdd, groupId);
915 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
916
917 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
918 callbackIntent.setData(groupUri);
919 deliverCallback(callbackIntent);
920 }
921
Daniel Lehmann18958a22012-02-28 17:45:25 -0800922 private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700923 long groupId) {
924 if (rawContactsToAdd == null) {
925 return;
926 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700927 for (long rawContactId : rawContactsToAdd) {
928 try {
929 final ArrayList<ContentProviderOperation> rawContactOperations =
930 new ArrayList<ContentProviderOperation>();
931
932 // Build an assert operation to ensure the contact is not already in the group
933 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
934 .newAssertQuery(Data.CONTENT_URI);
935 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
936 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
937 new String[] { String.valueOf(rawContactId),
938 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
939 assertBuilder.withExpectedCount(0);
940 rawContactOperations.add(assertBuilder.build());
941
942 // Build an insert operation to add the contact to the group
943 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
944 .newInsert(Data.CONTENT_URI);
945 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
946 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
947 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
948 rawContactOperations.add(insertBuilder.build());
949
950 if (DEBUG) {
951 for (ContentProviderOperation operation : rawContactOperations) {
952 Log.v(TAG, operation.toString());
953 }
954 }
955
956 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700957 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800958 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700959 }
960 } catch (RemoteException e) {
961 // Something went wrong, bail without success
962 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
963 String.valueOf(rawContactId), e);
964 } catch (OperationApplicationException e) {
965 // The assert could have failed because the contact is already in the group,
966 // just continue to the next contact
967 Log.w(TAG, "Assert failed in adding raw contact ID " +
968 String.valueOf(rawContactId) + ". Already exists in group " +
969 String.valueOf(groupId), e);
970 }
971 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700972 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700973
Daniel Lehmann18958a22012-02-28 17:45:25 -0800974 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700975 long groupId) {
976 if (rawContactsToRemove == null) {
977 return;
978 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700979 for (long rawContactId : rawContactsToRemove) {
980 // Apply the delete operation on the data row for the given raw contact's
981 // membership in the given group. If no contact matches the provided selection, then
982 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800983 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700984 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
985 new String[] { String.valueOf(rawContactId),
986 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
987 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700988 }
989
990 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800991 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800992 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800993 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
994 Intent serviceIntent = new Intent(context, ContactSaveService.class);
995 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
996 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
997 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
998
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800999 return serviceIntent;
1000 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001001
1002 private void setStarred(Intent intent) {
1003 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1004 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
1005 if (contactUri == null) {
1006 Log.e(TAG, "Invalid arguments for setStarred request");
1007 return;
1008 }
1009
1010 final ContentValues values = new ContentValues(1);
1011 values.put(Contacts.STARRED, value);
1012 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -07001013
1014 // Undemote the contact if necessary
1015 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
1016 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -08001017 if (c == null) {
1018 return;
1019 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001020 try {
1021 if (c.moveToFirst()) {
1022 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -07001023
1024 // Don't bother undemoting if this contact is the user's profile.
1025 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -08001026 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -07001027 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001028 }
1029 } finally {
1030 c.close();
1031 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001032 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001033
1034 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -07001035 * Creates an intent that can be sent to this service to set the redirect to voicemail.
1036 */
1037 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
1038 boolean value) {
1039 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1040 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1041 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1042 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1043
1044 return serviceIntent;
1045 }
1046
1047 private void setSendToVoicemail(Intent intent) {
1048 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1049 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1050 if (contactUri == null) {
1051 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1052 return;
1053 }
1054
1055 final ContentValues values = new ContentValues(1);
1056 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1057 getContentResolver().update(contactUri, values, null, null);
1058 }
1059
1060 /**
1061 * Creates an intent that can be sent to this service to save the contact's ringtone.
1062 */
1063 public static Intent createSetRingtone(Context context, Uri contactUri,
1064 String value) {
1065 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1066 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1067 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1068 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1069
1070 return serviceIntent;
1071 }
1072
1073 private void setRingtone(Intent intent) {
1074 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1075 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1076 if (contactUri == null) {
1077 Log.e(TAG, "Invalid arguments for setRingtone");
1078 return;
1079 }
1080 ContentValues values = new ContentValues(1);
1081 values.put(Contacts.CUSTOM_RINGTONE, value);
1082 getContentResolver().update(contactUri, values, null, null);
1083 }
1084
1085 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001086 * Creates an intent that sets the selected data item as super primary (default)
1087 */
1088 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1089 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1090 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1091 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1092 return serviceIntent;
1093 }
1094
1095 private void setSuperPrimary(Intent intent) {
1096 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1097 if (dataId == -1) {
1098 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1099 return;
1100 }
1101
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001102 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001103 }
1104
1105 /**
1106 * Creates an intent that clears the primary flag of all data items that belong to the same
1107 * raw_contact as the given data item. Will only clear, if the data item was primary before
1108 * this call
1109 */
1110 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1111 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1112 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1113 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1114 return serviceIntent;
1115 }
1116
1117 private void clearPrimary(Intent intent) {
1118 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1119 if (dataId == -1) {
1120 Log.e(TAG, "Invalid arguments for clearPrimary request");
1121 return;
1122 }
1123
1124 // Update the primary values in the data record.
1125 ContentValues values = new ContentValues(1);
1126 values.put(Data.IS_SUPER_PRIMARY, 0);
1127 values.put(Data.IS_PRIMARY, 0);
1128
1129 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1130 values, null, null);
1131 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001132
1133 /**
1134 * Creates an intent that can be sent to this service to delete a contact.
1135 */
1136 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1137 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1138 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1139 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1140 return serviceIntent;
1141 }
1142
Brian Attwelld2962a32015-03-02 14:48:50 -08001143 /**
1144 * Creates an intent that can be sent to this service to delete multiple contacts.
1145 */
1146 public static Intent createDeleteMultipleContactsIntent(Context context,
1147 long[] contactIds) {
1148 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1149 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1150 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1151 return serviceIntent;
1152 }
1153
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001154 private void deleteContact(Intent intent) {
1155 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1156 if (contactUri == null) {
1157 Log.e(TAG, "Invalid arguments for deleteContact request");
1158 return;
1159 }
1160
1161 getContentResolver().delete(contactUri, null, null);
1162 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001163
Brian Attwelld2962a32015-03-02 14:48:50 -08001164 private void deleteMultipleContacts(Intent intent) {
1165 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1166 if (contactIds == null) {
1167 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1168 return;
1169 }
1170 for (long contactId : contactIds) {
1171 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1172 getContentResolver().delete(contactUri, null, null);
1173 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001174 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1175 .contacts_deleted_toast, contactIds.length);
1176 mMainHandler.post(new Runnable() {
1177 @Override
1178 public void run() {
1179 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1180 .show();
1181 }
1182 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001183 }
1184
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001185 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001186 * Creates an intent that can be sent to this service to split a contact into it's constituent
Gary Mai53fe0d22016-07-26 17:23:53 -07001187 * pieces. This will set the raw contact ids to TYPE_AUTOMATIC for AggregationExceptions so
1188 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001189 */
1190 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1191 ResultReceiver receiver) {
1192 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1193 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1194 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1195 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1196 return serviceIntent;
1197 }
1198
1199 private void splitContact(Intent intent) {
1200 final long rawContactIds[][] = (long[][]) intent
1201 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001202 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Mai7efa9942016-05-12 11:26:49 -07001203 if (rawContactIds == null) {
1204 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001205 if (receiver != null) {
1206 receiver.send(BAD_ARGUMENTS, new Bundle());
1207 }
Gary Mai7efa9942016-05-12 11:26:49 -07001208 return;
1209 }
1210 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1211 final ContentResolver resolver = getContentResolver();
1212 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001213 for (int i = 0; i < rawContactIds.length; i++) {
1214 for (int j = 0; j < rawContactIds.length; j++) {
1215 if (i != j) {
1216 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
1217 if (receiver != null) {
1218 receiver.send(CP2_ERROR, new Bundle());
1219 return;
1220 }
1221 }
1222 }
1223 }
1224 }
1225 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1226 if (receiver != null) {
1227 receiver.send(CP2_ERROR, new Bundle());
1228 }
1229 return;
1230 }
1231 if (receiver != null) {
1232 receiver.send(CONTACTS_SPLIT, new Bundle());
1233 } else {
1234 showToast(R.string.contactUnlinkedToast);
1235 }
1236 }
1237
1238 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001239 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001240 * and {@param rawContactIds2} to {@param operations}.
1241 * @return false if an error occurred, true otherwise.
1242 */
1243 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
1244 long[] rawContactIds1, long[] rawContactIds2) {
1245 if (rawContactIds1 == null || rawContactIds2 == null) {
1246 Log.e(TAG, "Invalid arguments for splitContact request");
1247 return false;
1248 }
1249 // For each pair of raw contacts, insert an aggregation exception
1250 final ContentResolver resolver = getContentResolver();
1251 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1252 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1253 for (int i = 0; i < rawContactIds1.length; i++) {
1254 for (int j = 0; j < rawContactIds2.length; j++) {
1255 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
1256 // Before we get to 500 we need to flush the operations list
1257 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1258 if (!applyOperations(resolver, operations)) {
1259 return false;
1260 }
1261 operations.clear();
1262 }
1263 }
1264 }
1265 return true;
1266 }
1267
1268 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001269 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001270 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001271 */
1272 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001273 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001274 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1275 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1276 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1277 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001278
1279 // Callback intent will be invoked by the service once the contacts are joined.
1280 Intent callbackIntent = new Intent(context, callbackActivity);
1281 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001282 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1283
1284 return serviceIntent;
1285 }
1286
Brian Attwelld3946ca2015-03-03 11:13:49 -08001287 /**
1288 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1289 * No special attention is paid to where the resulting contact's name is taken from.
1290 */
Gary Mai7efa9942016-05-12 11:26:49 -07001291 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1292 ResultReceiver receiver) {
1293 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001294 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1295 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001296 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001297 return serviceIntent;
1298 }
1299
Gary Mai7efa9942016-05-12 11:26:49 -07001300 /**
1301 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1302 * No special attention is paid to where the resulting contact's name is taken from.
1303 */
1304 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1305 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1306 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001307
1308 private interface JoinContactQuery {
1309 String[] PROJECTION = {
1310 RawContacts._ID,
1311 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001312 RawContacts.DISPLAY_NAME_SOURCE,
1313 };
1314
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001315 int _ID = 0;
1316 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001317 int DISPLAY_NAME_SOURCE = 2;
1318 }
1319
1320 private interface ContactEntityQuery {
1321 String[] PROJECTION = {
1322 Contacts.Entity.DATA_ID,
1323 Contacts.Entity.CONTACT_ID,
1324 Contacts.Entity.IS_SUPER_PRIMARY,
1325 };
1326 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1327 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1328 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1329 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1330
1331 int DATA_ID = 0;
1332 int CONTACT_ID = 1;
1333 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001334 }
1335
Brian Attwelld3946ca2015-03-03 11:13:49 -08001336 private void joinSeveralContacts(Intent intent) {
1337 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001338
Gary Mai7efa9942016-05-12 11:26:49 -07001339 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001340
Brian Attwelld3946ca2015-03-03 11:13:49 -08001341 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001342 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1343 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001344 if (rawContactIds == null) {
1345 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001346 if (receiver != null) {
1347 receiver.send(BAD_ARGUMENTS, new Bundle());
1348 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001349 return;
1350 }
1351
Brian Attwelld3946ca2015-03-03 11:13:49 -08001352 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001353 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001354 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1355 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1356 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001357 for (int i = 0; i < rawContactIds.length; i++) {
1358 for (int j = 0; j < rawContactIds.length; j++) {
1359 if (i != j) {
1360 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1361 }
Walter Jang0653de32015-07-24 12:12:40 -07001362 // Before we get to 500 we need to flush the operations list
1363 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001364 if (!applyOperations(resolver, operations)) {
1365 if (receiver != null) {
1366 receiver.send(CP2_ERROR, new Bundle());
1367 }
Walter Jang0653de32015-07-24 12:12:40 -07001368 return;
1369 }
1370 operations.clear();
1371 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001372 }
1373 }
Gary Mai7efa9942016-05-12 11:26:49 -07001374 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1375 if (receiver != null) {
1376 receiver.send(CP2_ERROR, new Bundle());
1377 }
Walter Jang0653de32015-07-24 12:12:40 -07001378 return;
1379 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001380
John Shaoa3c507a2016-09-13 14:26:17 -07001381
1382 final String name = queryNameOfLinkedContacts(contactIds);
1383 if (name != null) {
1384 if (receiver != null) {
1385 final Bundle result = new Bundle();
1386 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1387 result.putString(EXTRA_DISPLAY_NAME, name);
1388 receiver.send(CONTACTS_LINKED, result);
1389 } else {
1390 showToast(R.string.contactsJoinedMessage);
1391 }
Gary Mai7efa9942016-05-12 11:26:49 -07001392 } else {
John Shaoa3c507a2016-09-13 14:26:17 -07001393 if (receiver != null) {
1394 receiver.send(CP2_ERROR, new Bundle());
1395 }
1396 showToast(R.string.contactJoinErrorToast);
Gary Mai7efa9942016-05-12 11:26:49 -07001397 }
Walter Jang0653de32015-07-24 12:12:40 -07001398 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001399
John Shaoa3c507a2016-09-13 14:26:17 -07001400 /** Get the display name of the top-level contact after the contacts have been linked. */
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001401 private String queryNameOfLinkedContacts(long[] contactIds) {
1402 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1403 final String[] whereArgs = new String[contactIds.length];
1404 for (int i = 0; i < contactIds.length; i++) {
1405 whereArgs[i] = String.valueOf(contactIds[i]);
1406 whereBuilder.append("?,");
1407 }
1408 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1409 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
John Shaoa3c507a2016-09-13 14:26:17 -07001410 new String[]{Contacts._ID, Contacts.DISPLAY_NAME},
1411 whereBuilder.toString(), whereArgs, null);
1412
1413 String name = null;
1414 long contactId = 0;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001415 try {
1416 if (cursor.moveToFirst()) {
John Shaoa3c507a2016-09-13 14:26:17 -07001417 contactId = cursor.getLong(0);
1418 name = cursor.getString(1);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001419 }
John Shaoa3c507a2016-09-13 14:26:17 -07001420 while(cursor.moveToNext()) {
1421 if (cursor.getLong(0) != contactId) {
1422 return null;
1423 }
1424 }
1425 return name == null ? "" : name;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001426 } finally {
John Shaoa3c507a2016-09-13 14:26:17 -07001427 if (cursor != null) {
1428 cursor.close();
1429 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001430 }
1431 }
1432
Walter Jang0653de32015-07-24 12:12:40 -07001433 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001434 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001435 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001436 try {
John Shaoa3c507a2016-09-13 14:26:17 -07001437 final ContentProviderResult[] result =
1438 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1439 for (int i = 0; i < result.length; ++i) {
1440 // if no rows were modified in the operation then we count it as fail.
1441 if (result[i].count < 0) {
1442 throw new OperationApplicationException();
1443 }
1444 }
Walter Jang0653de32015-07-24 12:12:40 -07001445 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001446 } catch (RemoteException | OperationApplicationException e) {
1447 Log.e(TAG, "Failed to apply aggregation exception batch", e);
1448 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001449 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001450 }
1451 }
1452
Brian Attwelld3946ca2015-03-03 11:13:49 -08001453 private void joinContacts(Intent intent) {
1454 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1455 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001456
1457 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001458 // in the join UIs.
1459 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1460 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001461 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001462 return;
1463 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001464
Brian Attwell548f5c62015-01-27 17:46:46 -08001465 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001466
1467 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001468 for (int i = 0; i < rawContactIds.length; i++) {
1469 for (int j = 0; j < rawContactIds.length; j++) {
1470 if (i != j) {
1471 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1472 }
1473 }
1474 }
1475
Brian Attwelld3946ca2015-03-03 11:13:49 -08001476 final ContentResolver resolver = getContentResolver();
1477
Brian Attwell548f5c62015-01-27 17:46:46 -08001478 // Use the name for contactId1 as the name for the newly aggregated contact.
1479 final Uri contactId1Uri = ContentUris.withAppendedId(
1480 Contacts.CONTENT_URI, contactId1);
1481 final Uri entityUri = Uri.withAppendedPath(
1482 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1483 Cursor c = resolver.query(entityUri,
1484 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1485 if (c == null) {
1486 Log.e(TAG, "Unable to open Contacts DB cursor");
1487 showToast(R.string.contactSavedErrorToast);
1488 return;
1489 }
1490 long dataIdToAddSuperPrimary = -1;
1491 try {
1492 if (c.moveToFirst()) {
1493 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1494 }
1495 } finally {
1496 c.close();
1497 }
1498
1499 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1500 // display name does not change as a result of the join.
1501 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001502 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001503 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1504 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1505 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001506 operations.add(builder.build());
1507 }
1508
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001509 // Apply all aggregation exceptions as one batch
John Shaoa3c507a2016-09-13 14:26:17 -07001510 final boolean success = applyOperations(resolver, operations);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001511
John Shaoa3c507a2016-09-13 14:26:17 -07001512 final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2});
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001513 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
John Shaoa3c507a2016-09-13 14:26:17 -07001514 if (success && name != null) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001515 Uri uri = RawContacts.getContactLookupUri(resolver,
1516 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1517 callbackIntent.setData(uri);
1518 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001519 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001520 }
1521
Gary Mai7efa9942016-05-12 11:26:49 -07001522 /**
1523 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1524 * array of the return value holds an array of raw contact ids for one contactId.
1525 * @param contactIds
1526 * @return
1527 */
1528 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1529 final long[][] rawContactIds = new long[contactIds.length][];
1530 for (int i = 0; i < contactIds.length; i++) {
1531 rawContactIds[i] = getRawContactIds(contactIds[i]);
1532 }
1533 return rawContactIds;
1534 }
1535
1536 /**
1537 * Gets the raw contact ids associated with {@param contactId}.
1538 * @param contactId
1539 * @return Array of raw contact ids.
1540 */
1541 private long[] getRawContactIds(long contactId) {
1542 final ContentResolver resolver = getContentResolver();
1543 long rawContactIds[];
1544
1545 final StringBuilder queryBuilder = new StringBuilder();
1546 queryBuilder.append(RawContacts.CONTACT_ID)
1547 .append("=")
1548 .append(String.valueOf(contactId));
1549
1550 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1551 JoinContactQuery.PROJECTION,
1552 queryBuilder.toString(),
1553 null, null);
1554 if (c == null) {
1555 Log.e(TAG, "Unable to open Contacts DB cursor");
1556 return null;
1557 }
1558 try {
1559 rawContactIds = new long[c.getCount()];
1560 for (int i = 0; i < rawContactIds.length; i++) {
1561 c.moveToPosition(i);
1562 final long rawContactId = c.getLong(JoinContactQuery._ID);
1563 rawContactIds[i] = rawContactId;
1564 }
1565 } finally {
1566 c.close();
1567 }
1568 return rawContactIds;
1569 }
1570
Brian Attwelld3946ca2015-03-03 11:13:49 -08001571 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1572 if (contactIds == null) {
1573 return null;
1574 }
1575
Brian Attwell548f5c62015-01-27 17:46:46 -08001576 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001577
1578 final StringBuilder queryBuilder = new StringBuilder();
1579 final String stringContactIds[] = new String[contactIds.length];
1580 for (int i = 0; i < contactIds.length; i++) {
1581 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1582 stringContactIds[i] = String.valueOf(contactIds[i]);
1583 if (contactIds[i] == -1) {
1584 return null;
1585 }
1586 if (i == contactIds.length -1) {
1587 break;
1588 }
1589 queryBuilder.append(" OR ");
1590 }
1591
Brian Attwell548f5c62015-01-27 17:46:46 -08001592 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1593 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001594 queryBuilder.toString(),
1595 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001596 if (c == null) {
1597 Log.e(TAG, "Unable to open Contacts DB cursor");
1598 showToast(R.string.contactSavedErrorToast);
1599 return null;
1600 }
Gary Mai7efa9942016-05-12 11:26:49 -07001601 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001602 try {
1603 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001604 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001605 return null;
1606 }
1607 rawContactIds = new long[c.getCount()];
1608 for (int i = 0; i < rawContactIds.length; i++) {
1609 c.moveToPosition(i);
1610 long rawContactId = c.getLong(JoinContactQuery._ID);
1611 rawContactIds[i] = rawContactId;
1612 }
1613 } finally {
1614 c.close();
1615 }
1616 return rawContactIds;
1617 }
1618
Brian Attwelld3946ca2015-03-03 11:13:49 -08001619 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1620 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1621 }
1622
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001623 /**
1624 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1625 */
1626 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1627 long rawContactId1, long rawContactId2) {
1628 Builder builder =
1629 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1630 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1631 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1632 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1633 operations.add(builder.build());
1634 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001635
1636 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001637 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} ContentProviderOperation.
Gary Mai7efa9942016-05-12 11:26:49 -07001638 */
1639 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
1640 long rawContactId1, long rawContactId2) {
1641 final Builder builder =
1642 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Mai53fe0d22016-07-26 17:23:53 -07001643 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001644 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1645 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1646 operations.add(builder.build());
1647 }
1648
Marcus Hagerott819214d2016-09-29 14:58:27 -07001649 public static Intent createImportFromSimIntent(Context context,
1650 ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
1651 return new Intent(context, ContactSaveService.class)
1652 .setAction(ACTION_IMPORT_FROM_SIM)
1653 .putExtra(EXTRA_SIM_CONTACTS, contacts)
1654 .putExtra(EXTRA_ACCOUNT, targetAccount);
1655 }
1656
1657 private void importFromSim(Intent intent) {
1658 final Intent result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
1659 .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, System.currentTimeMillis());
1660 try {
1661 final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
1662 final ArrayList<SimContact> contacts =
1663 intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
1664 mSimContactDao.importContacts(contacts, targetAccount);
1665 // notify success
1666 LocalBroadcastManager.getInstance(this).sendBroadcast(result
1667 .putExtra(EXTRA_RESULT_COUNT, contacts.size())
1668 .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS));
1669 if (Log.isLoggable(TAG, Log.DEBUG)) {
1670 Log.d(TAG, "importFromSim completed successfully");
1671 }
1672 } catch (RemoteException|OperationApplicationException e) {
1673 Log.e(TAG, "Failed to import contacts from SIM card", e);
1674 LocalBroadcastManager.getInstance(this).sendBroadcast(result
1675 .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE));
1676 }
1677 }
1678
Gary Mai7efa9942016-05-12 11:26:49 -07001679 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001680 * Shows a toast on the UI thread.
1681 */
1682 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001683 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001684
1685 @Override
1686 public void run() {
1687 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1688 }
1689 });
1690 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001691
1692 private void deliverCallback(final Intent callbackIntent) {
1693 mMainHandler.post(new Runnable() {
1694
1695 @Override
1696 public void run() {
1697 deliverCallbackOnUiThread(callbackIntent);
1698 }
1699 });
1700 }
1701
1702 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1703 // TODO: this assumes that if there are multiple instances of the same
1704 // activity registered, the last one registered is the one waiting for
1705 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001706 for (Listener listener : sListeners) {
1707 if (callbackIntent.getComponent().equals(
1708 ((Activity) listener).getIntent().getComponent())) {
1709 listener.onServiceCompleted(callbackIntent);
1710 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001711 }
1712 }
1713 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001714
1715 public interface GroupsDao {
1716 Uri create(String title, AccountWithDataSet account);
1717 int delete(Uri groupUri);
1718 Bundle captureDeletionUndoData(Uri groupUri);
1719 Uri undoDeletion(Bundle undoData);
1720 }
1721
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001722 public static class GroupsDaoImpl implements GroupsDao {
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001723 public static final String KEY_GROUP_DATA = "groupData";
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001724 public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
1725
1726 private static final String TAG = "GroupsDao";
1727 private final Context context;
1728 private final ContentResolver contentResolver;
1729
1730 public GroupsDaoImpl(Context context) {
1731 this(context, context.getContentResolver());
1732 }
1733
1734 public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
1735 this.context = context;
1736 this.contentResolver = contentResolver;
1737 }
1738
1739 public Bundle captureDeletionUndoData(Uri groupUri) {
1740 final long groupId = ContentUris.parseId(groupUri);
1741 final Bundle result = new Bundle();
1742
1743 final Cursor cursor = contentResolver.query(groupUri,
1744 new String[]{
1745 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
1746 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
1747 Groups.SHOULD_SYNC
1748 },
1749 Groups.DELETED + "=?", new String[] { "0" }, null);
1750 try {
1751 if (cursor.moveToFirst()) {
1752 final ContentValues groupValues = new ContentValues();
1753 DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
1754 result.putParcelable(KEY_GROUP_DATA, groupValues);
1755 } else {
1756 // Group doesn't exist.
1757 return result;
1758 }
1759 } finally {
1760 cursor.close();
1761 }
1762
1763 final Cursor membersCursor = contentResolver.query(
1764 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
1765 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1766 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
1767 final long[] memberIds = new long[membersCursor.getCount()];
1768 int i = 0;
1769 while (membersCursor.moveToNext()) {
1770 memberIds[i++] = membersCursor.getLong(0);
1771 }
1772 result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
1773 return result;
1774 }
1775
1776 public Uri undoDeletion(Bundle deletedGroupData) {
1777 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
1778 if (groupData == null) {
1779 return null;
1780 }
1781 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
1782 final long groupId = ContentUris.parseId(groupUri);
1783
1784 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
1785 if (memberIds == null) {
1786 return groupUri;
1787 }
1788 final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
1789 for (int i = 0; i < memberIds.length; i++) {
1790 memberInsertions[i] = new ContentValues();
1791 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
1792 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1793 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
1794 }
1795 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
1796 if (inserted != memberIds.length) {
1797 Log.e(TAG, "Could not recover some members for group deletion undo");
1798 }
1799
1800 return groupUri;
1801 }
1802
1803 public Uri create(String title, AccountWithDataSet account) {
1804 final ContentValues values = new ContentValues();
1805 values.put(Groups.TITLE, title);
1806 values.put(Groups.ACCOUNT_NAME, account.name);
1807 values.put(Groups.ACCOUNT_TYPE, account.type);
1808 values.put(Groups.DATA_SET, account.dataSet);
1809 return contentResolver.insert(Groups.CONTENT_URI, values);
1810 }
1811
1812 public int delete(Uri groupUri) {
1813 return contentResolver.delete(groupUri, null, null);
1814 }
1815 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001816}