blob: 2e98932b59b92157f2988128c15201aba1eff2ff [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;
Walter Jang3a0b4832016-10-12 11:02:54 -070067import com.android.contactsbind.FeedbackHelper;
68
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070069import com.google.common.collect.Lists;
70import com.google.common.collect.Sets;
71
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080072import java.util.ArrayList;
73import java.util.HashSet;
74import java.util.List;
75import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070076
Marcus Hagerott819214d2016-09-29 14:58:27 -070077import static android.Manifest.permission.WRITE_CONTACTS;
78
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080079/**
80 * A service responsible for saving changes to the content provider.
81 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070082public class ContactSaveService extends IntentService {
83 private static final String TAG = "ContactSaveService";
84
Katherine Kuana007e442011-07-07 09:25:34 -070085 /** Set to true in order to view logs on content provider operations */
86 private static final boolean DEBUG = false;
87
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070088 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
89
90 public static final String EXTRA_ACCOUNT_NAME = "accountName";
91 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070092 public static final String EXTRA_DATA_SET = "dataSet";
Marcus Hagerott819214d2016-09-29 14:58:27 -070093 public static final String EXTRA_ACCOUNT = "account";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070094 public static final String EXTRA_CONTENT_VALUES = "contentValues";
95 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
Gary Mai7efa9942016-05-12 11:26:49 -070096 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
97 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070098
Dmitri Plotnikova0114142011-02-15 13:53:21 -080099 public static final String ACTION_SAVE_CONTACT = "saveContact";
100 public static final String EXTRA_CONTACT_STATE = "state";
101 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700102 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -0700103 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -0800104 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700105
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800106 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800107 public static final String ACTION_RENAME_GROUP = "renameGroup";
108 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700109 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800110 public static final String EXTRA_GROUP_ID = "groupId";
111 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700112 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
113 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800114
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800115 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800116 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800117 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800118 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800119 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800120 public static final String EXTRA_STARRED_FLAG = "starred";
Marcus Hagerott3bb85142016-07-29 10:46:36 -0700121 public static final String EXTRA_DISPLAY_NAME = "extraDisplayName";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800122
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800123 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
124 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
125 public static final String EXTRA_DATA_ID = "dataId";
126
Gary Mai7efa9942016-05-12 11:26:49 -0700127 public static final String ACTION_SPLIT_CONTACT = "splitContact";
128
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800129 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800130 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800131 public static final String EXTRA_CONTACT_ID1 = "contactId1";
132 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800133
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700134 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
135 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
136
137 public static final String ACTION_SET_RINGTONE = "setRingtone";
138 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
139
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700140 public static final String ACTION_UNDO = "undo";
141 public static final String EXTRA_UNDO_ACTION = "undoAction";
142 public static final String EXTRA_UNDO_DATA = "undoData";
143
Marcus Hagerott819214d2016-09-29 14:58:27 -0700144 public static final String ACTION_IMPORT_FROM_SIM = "importFromSim";
145 public static final String EXTRA_SIM_CONTACTS = "simContacts";
146
147 public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
148 public static final String BROADCAST_SIM_IMPORT_COMPLETE = "simImportComplete";
149
150 public static final String EXTRA_RESULT_CODE = "resultCode";
151 public static final String EXTRA_RESULT_COUNT = "count";
152 public static final String EXTRA_OPERATION_REQUESTED_AT_TIME = "requestedTime";
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700153
Gary Mai7efa9942016-05-12 11:26:49 -0700154 public static final int CP2_ERROR = 0;
155 public static final int CONTACTS_LINKED = 1;
156 public static final int CONTACTS_SPLIT = 2;
Gary Mai31d572e2016-06-03 14:04:32 -0700157 public static final int BAD_ARGUMENTS = 3;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700158 public static final int RESULT_UNKNOWN = 0;
159 public static final int RESULT_SUCCESS = 1;
160 public static final int RESULT_FAILURE = 2;
Gary Mai7efa9942016-05-12 11:26:49 -0700161
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700162 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
163 Data.MIMETYPE,
164 Data.IS_PRIMARY,
165 Data.DATA1,
166 Data.DATA2,
167 Data.DATA3,
168 Data.DATA4,
169 Data.DATA5,
170 Data.DATA6,
171 Data.DATA7,
172 Data.DATA8,
173 Data.DATA9,
174 Data.DATA10,
175 Data.DATA11,
176 Data.DATA12,
177 Data.DATA13,
178 Data.DATA14,
179 Data.DATA15
180 );
181
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800182 private static final int PERSIST_TRIES = 3;
183
Walter Jang0653de32015-07-24 12:12:40 -0700184 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
185
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800186 public interface Listener {
187 public void onServiceCompleted(Intent callbackIntent);
188 }
189
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100190 private static final CopyOnWriteArrayList<Listener> sListeners =
191 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800192
193 private Handler mMainHandler;
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700194 private GroupsDao mGroupsDao;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700195 private SimContactDao mSimContactDao;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800196
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700197 public ContactSaveService() {
198 super(TAG);
199 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800200 mMainHandler = new Handler(Looper.getMainLooper());
201 }
202
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700203 @Override
204 public void onCreate() {
205 super.onCreate();
206 mGroupsDao = new GroupsDaoImpl(this);
Marcus Hagerott819214d2016-09-29 14:58:27 -0700207 mSimContactDao = new SimContactDao(this);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700208 }
209
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800210 public static void registerListener(Listener listener) {
211 if (!(listener instanceof Activity)) {
212 throw new ClassCastException("Only activities can be registered to"
213 + " receive callback from " + ContactSaveService.class.getName());
214 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100215 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800216 }
217
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700218 public static boolean canUndo(Intent resultIntent) {
219 return resultIntent.hasExtra(EXTRA_UNDO_DATA);
220 }
221
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800222 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100223 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700224 }
225
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800226 /**
227 * Returns true if the ContactSaveService was started successfully and false if an exception
228 * was thrown and a Toast error message was displayed.
229 */
230 public static boolean startService(Context context, Intent intent, int saveMode) {
231 try {
232 context.startService(intent);
233 } catch (Exception exception) {
234 final int resId;
235 switch (saveMode) {
Gary Mai363af602016-09-28 10:01:23 -0700236 case ContactEditorActivity.ContactEditor.SaveMode.SPLIT:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800237 resId = R.string.contactUnlinkErrorToast;
238 break;
Gary Mai363af602016-09-28 10:01:23 -0700239 case ContactEditorActivity.ContactEditor.SaveMode.RELOAD:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800240 resId = R.string.contactJoinErrorToast;
241 break;
Gary Mai363af602016-09-28 10:01:23 -0700242 case ContactEditorActivity.ContactEditor.SaveMode.CLOSE:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800243 resId = R.string.contactSavedErrorToast;
244 break;
245 default:
246 resId = R.string.contactGenericErrorToast;
247 }
248 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
249 return false;
250 }
251 return true;
252 }
253
254 /**
255 * Utility method that starts service and handles exception.
256 */
257 public static void startService(Context context, Intent intent) {
258 try {
259 context.startService(intent);
260 } catch (Exception exception) {
261 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
262 }
263 }
264
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700265 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800266 public Object getSystemService(String name) {
267 Object service = super.getSystemService(name);
268 if (service != null) {
269 return service;
270 }
271
272 return getApplicationContext().getSystemService(name);
273 }
274
275 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700276 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800277 if (intent == null) {
278 Log.d(TAG, "onHandleIntent: could not handle null intent");
279 return;
280 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700281 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
282 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
283 // TODO: add more specific error string such as "Turn on Contacts
284 // permission to update your contacts"
285 showToast(R.string.contactSavedErrorToast);
286 return;
287 }
288
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700289 // Call an appropriate method. If we're sure it affects how incoming phone calls are
290 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700291 String action = intent.getAction();
292 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
293 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800294 } else if (ACTION_SAVE_CONTACT.equals(action)) {
295 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800296 } else if (ACTION_CREATE_GROUP.equals(action)) {
297 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800298 } else if (ACTION_RENAME_GROUP.equals(action)) {
299 renameGroup(intent);
300 } else if (ACTION_DELETE_GROUP.equals(action)) {
301 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700302 } else if (ACTION_UPDATE_GROUP.equals(action)) {
303 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800304 } else if (ACTION_SET_STARRED.equals(action)) {
305 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800306 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
307 setSuperPrimary(intent);
308 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
309 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800310 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
311 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800312 } else if (ACTION_DELETE_CONTACT.equals(action)) {
313 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700314 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
315 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800316 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
317 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800318 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
319 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700320 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
321 setSendToVoicemail(intent);
322 } else if (ACTION_SET_RINGTONE.equals(action)) {
323 setRingtone(intent);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700324 } else if (ACTION_UNDO.equals(action)) {
325 undo(intent);
Marcus Hagerott819214d2016-09-29 14:58:27 -0700326 } else if (ACTION_IMPORT_FROM_SIM.equals(action)) {
327 importFromSim(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700328 }
329 }
330
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800331 /**
332 * Creates an intent that can be sent to this service to create a new raw contact
333 * using data presented as a set of ContentValues.
334 */
335 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700336 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700337 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800338 Intent serviceIntent = new Intent(
339 context, ContactSaveService.class);
340 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
341 if (account != null) {
342 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
343 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700344 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800345 }
346 serviceIntent.putParcelableArrayListExtra(
347 ContactSaveService.EXTRA_CONTENT_VALUES, values);
348
349 // Callback intent will be invoked by the service once the new contact is
350 // created. The service will put the URI of the new contact as "data" on
351 // the callback intent.
352 Intent callbackIntent = new Intent(context, callbackActivity);
353 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800354 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
355 return serviceIntent;
356 }
357
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700358 private void createRawContact(Intent intent) {
359 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
360 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700361 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700362 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
363 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
364
365 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
366 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
367 .withValue(RawContacts.ACCOUNT_NAME, accountName)
368 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700369 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700370 .build());
371
372 int size = valueList.size();
373 for (int i = 0; i < size; i++) {
374 ContentValues values = valueList.get(i);
375 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
376 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
377 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
378 .withValues(values)
379 .build());
380 }
381
382 ContentResolver resolver = getContentResolver();
383 ContentProviderResult[] results;
384 try {
385 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
386 } catch (Exception e) {
387 throw new RuntimeException("Failed to store new contact", e);
388 }
389
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700390 Uri rawContactUri = results[0].uri;
391 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
392
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800393 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700394 }
395
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700396 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800397 * Creates an intent that can be sent to this service to create a new raw contact
398 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800399 * This variant is more convenient to use when there is only one photo that can
400 * possibly be updated, as in the Contact Details screen.
401 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
402 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800403 */
Maurice Chu851222a2012-06-21 11:43:08 -0700404 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700405 String saveModeExtraKey, int saveMode, boolean isProfile,
406 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700407 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800408 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700409 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800410 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700411 callbackActivity, callbackAction, bundle,
412 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800413 }
414
415 /**
416 * Creates an intent that can be sent to this service to create a new raw contact
417 * using data presented as a set of ContentValues.
418 * This variant is used when multiple contacts' photos may be updated, as in the
419 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700420 *
Josh Garguse692e012012-01-18 14:53:11 -0800421 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700422 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
423 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800424 */
Maurice Chu851222a2012-06-21 11:43:08 -0700425 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700426 String saveModeExtraKey, int saveMode, boolean isProfile,
427 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700428 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000429 Intent serviceIntent = new Intent(
430 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800431 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
432 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700433 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800434 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
435
Josh Garguse692e012012-01-18 14:53:11 -0800436 if (updatedPhotos != null) {
437 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
438 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800439
Josh Garguse5d3f892012-04-11 11:56:15 -0700440 if (callbackActivity != null) {
441 // Callback intent will be invoked by the service once the contact is
442 // saved. The service will put the URI of the new contact as "data" on
443 // the callback intent.
444 Intent callbackIntent = new Intent(context, callbackActivity);
445 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700446 if (joinContactIdExtraKey != null && joinContactId != null) {
447 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
448 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700449 callbackIntent.setAction(callbackAction);
450 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
451 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800452 return serviceIntent;
453 }
454
455 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700456 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700457 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800458 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800459
Jay Shrauner08099782015-03-25 14:17:11 -0700460 if (state == null) {
461 Log.e(TAG, "Invalid arguments for saveContact request");
462 return;
463 }
464
benny.lin3a4e7a22014-01-08 10:58:08 +0800465 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800466 // Trim any empty fields, and RawContacts, before persisting
467 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700468 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800469
470 Uri lookupUri = null;
471
472 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700473
Josh Garguse692e012012-01-18 14:53:11 -0800474 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800475
Josh Gargusef15c8e2012-01-30 16:42:02 -0800476 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
477 long insertedRawContactId = -1;
478
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800479 // Attempt to persist changes
480 int tries = 0;
481 while (tries++ < PERSIST_TRIES) {
482 try {
483 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800484 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
485
486 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
487
488 for (CPOWrapper cpoWrapper : diffWrapper) {
489 diff.add(cpoWrapper.getOperation());
490 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700491
Katherine Kuana007e442011-07-07 09:25:34 -0700492 if (DEBUG) {
493 Log.v(TAG, "Content Provider Operations:");
494 for (ContentProviderOperation operation : diff) {
495 Log.v(TAG, operation.toString());
496 }
497 }
498
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700499 int numberProcessed = 0;
500 boolean batchFailed = false;
501 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
502 while (numberProcessed < diff.size()) {
503 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
504 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700505 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700506 batchFailed = true;
507 break;
508 } else {
509 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700510 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800511 }
512
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700513 if (batchFailed) {
514 // Retry save
515 continue;
516 }
517
Wenyi Wang67addcc2015-11-23 10:07:48 -0800518 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800519 if (rawContactId == -1) {
520 throw new IllegalStateException("Could not determine RawContact ID after save");
521 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800522 // We don't have to check to see if the value is still -1. If we reach here,
523 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800524 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700525 if (isProfile) {
526 // Since the profile supports local raw contacts, which may have been completely
527 // removed if all information was removed, we need to do a special query to
528 // get the lookup URI for the profile contact (if it still exists).
529 Cursor c = resolver.query(Profile.CONTENT_URI,
530 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
531 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800532 if (c == null) {
533 continue;
534 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700535 try {
Erik162b7e32011-09-20 15:23:55 -0700536 if (c.moveToFirst()) {
537 final long contactId = c.getLong(0);
538 final String lookupKey = c.getString(1);
539 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
540 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700541 } finally {
542 c.close();
543 }
544 } else {
545 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
546 rawContactId);
547 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
548 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800549 if (lookupUri != null) {
550 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
551 }
Josh Garguse692e012012-01-18 14:53:11 -0800552
553 // We can change this back to false later, if we fail to save the contact photo.
554 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800555 break;
556
557 } catch (RemoteException e) {
558 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700559 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800560 break;
561
Jay Shrauner57fca182014-01-17 14:20:50 -0800562 } catch (IllegalArgumentException e) {
563 // This is thrown by applyBatch on malformed requests
Walter Jang3a0b4832016-10-12 11:02:54 -0700564 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Jay Shrauner57fca182014-01-17 14:20:50 -0800565 showToast(R.string.contactSavedErrorToast);
566 break;
567
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800568 } catch (OperationApplicationException e) {
569 // Version consistency failed, re-parent change and try again
570 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
571 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
572 boolean first = true;
573 final int count = state.size();
574 for (int i = 0; i < count; i++) {
575 Long rawContactId = state.getRawContactId(i);
576 if (rawContactId != null && rawContactId != -1) {
577 if (!first) {
578 sb.append(',');
579 }
580 sb.append(rawContactId);
581 first = false;
582 }
583 }
584 sb.append(")");
585
586 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800587 throw new IllegalStateException(
588 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800589 }
590
Maurice Chu851222a2012-06-21 11:43:08 -0700591 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700592 isProfile
593 ? RawContactsEntity.PROFILE_CONTENT_URI
594 : RawContactsEntity.CONTENT_URI,
595 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700596 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700597
598 // Update the new state to use profile URIs if appropriate.
599 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700600 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700601 delta.setProfileQueryUri();
602 }
603 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800604 }
605 }
606
Josh Garguse692e012012-01-18 14:53:11 -0800607 // Now save any updated photos. We do this at the end to ensure that
608 // the ContactProvider already knows about newly-created contacts.
609 if (updatedPhotos != null) {
610 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700611 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800612 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800613
614 // If the raw-contact ID is negative, we are saving a new raw-contact;
615 // replace the bogus ID with the new one that we actually saved the contact at.
616 if (rawContactId < 0) {
617 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800618 }
619
Jay Shrauner511561d2015-04-02 10:35:33 -0700620 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700621 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700622 succeeded = false;
623 }
Josh Garguse692e012012-01-18 14:53:11 -0800624 }
625 }
626
Josh Garguse5d3f892012-04-11 11:56:15 -0700627 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
628 if (callbackIntent != null) {
629 if (succeeded) {
630 // Mark the intent to indicate that the save was successful (even if the lookup URI
631 // is now null). For local contacts or the local profile, it's possible that the
632 // save triggered removal of the contact, so no lookup URI would exist..
633 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
634 }
635 callbackIntent.setData(lookupUri);
636 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800637 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800638 }
639
Josh Garguse692e012012-01-18 14:53:11 -0800640 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700641 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
642 * subsets, adds the returned array to "results".
643 *
644 * @return the size of the array, if not null; -1 when the array is null.
645 */
646 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
647 ContentProviderResult[] results, ContentResolver resolver)
648 throws RemoteException, OperationApplicationException {
649 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
650 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
651 subset.addAll(diff.subList(offset, offset + subsetCount));
652 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
653 .AUTHORITY, subset);
654 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
655 return -1;
656 }
657 for (ContentProviderResult c : subsetResult) {
658 results[offset++] = c;
659 }
660 return subsetResult.length;
661 }
662
663 /**
Josh Garguse692e012012-01-18 14:53:11 -0800664 * Save updated photo for the specified raw-contact.
665 * @return true for success, false for failure
666 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800667 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800668 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800669 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
670 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
671
benny.lin3a4e7a22014-01-08 10:58:08 +0800672 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800673 }
674
Josh Gargusef15c8e2012-01-30 16:42:02 -0800675 /**
676 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
677 */
Maurice Chu851222a2012-06-21 11:43:08 -0700678 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800679 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800680 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800681 long existingRawContactId = state.findRawContactId();
682 if (existingRawContactId != -1) {
683 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800684 }
685
Wenyi Wang67addcc2015-11-23 10:07:48 -0800686 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800687 }
688
689 /**
690 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
691 */
692 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800693 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800694 if (results == null) {
695 return -1;
696 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800697 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800698 final int numResults = results.length;
699 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800700 final CPOWrapper cpoWrapper = diffWrapper.get(i);
701 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
702 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
703 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800704 return ContentUris.parseId(results[i].uri);
705 }
706 }
707 return -1;
708 }
709
710 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700711 * Creates an intent that can be sent to this service to create a new group as
712 * well as add new members at the same time.
713 *
714 * @param context of the application
715 * @param account in which the group should be created
716 * @param label is the name of the group (cannot be null)
717 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
718 * should be added to the group
719 * @param callbackActivity is the activity to send the callback intent to
720 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700721 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700722 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700723 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700724 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800725 Intent serviceIntent = new Intent(context, ContactSaveService.class);
726 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
727 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
728 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700729 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800730 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700731 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700732
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800733 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700734 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800735 Intent callbackIntent = new Intent(context, callbackActivity);
736 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700737 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800738
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700739 return serviceIntent;
740 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800741
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800742 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700743 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
744 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
745 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
746 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700747 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800748
Katherine Kuan717e3432011-07-13 17:03:24 -0700749 // Create the new group
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700750 final Uri groupUri = mGroupsDao.create(label,
751 new AccountWithDataSet(accountName, accountType, dataSet));
752 final ContentResolver resolver = getContentResolver();
Katherine Kuan717e3432011-07-13 17:03:24 -0700753
754 // If there's no URI, then the insertion failed. Abort early because group members can't be
755 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800756 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700757 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800758 return;
759 }
760
Katherine Kuan717e3432011-07-13 17:03:24 -0700761 // Add new group members
762 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
763
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700764 ContentValues values = new ContentValues();
Katherine Kuan717e3432011-07-13 17:03:24 -0700765 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
Walter Jang8bac28b2016-08-30 10:34:55 -0700766 // with the way other intent extras that are passed to the
Gary Mai363af602016-09-28 10:01:23 -0700767 // {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800768 values.clear();
769 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
770 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
771
772 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700773 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700774 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800775 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800776 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800777 }
778
779 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800780 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800781 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700782 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700783 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800784 Intent serviceIntent = new Intent(context, ContactSaveService.class);
785 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
786 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
787 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700788
789 // Callback intent will be invoked by the service once the group is renamed.
790 Intent callbackIntent = new Intent(context, callbackActivity);
791 callbackIntent.setAction(callbackAction);
792 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
793
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800794 return serviceIntent;
795 }
796
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800797 private void renameGroup(Intent intent) {
798 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
799 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
800
801 if (groupId == -1) {
802 Log.e(TAG, "Invalid arguments for renameGroup request");
803 return;
804 }
805
806 ContentValues values = new ContentValues();
807 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700808 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
809 getContentResolver().update(groupUri, values, null, null);
810
811 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
812 callbackIntent.setData(groupUri);
813 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800814 }
815
816 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800817 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800818 */
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700819 public static Intent createGroupDeletionIntent(Context context, long groupId) {
820 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800821 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800822 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700823
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800824 return serviceIntent;
825 }
826
827 private void deleteGroup(Intent intent) {
828 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
829 if (groupId == -1) {
830 Log.e(TAG, "Invalid arguments for deleteGroup request");
831 return;
832 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700833 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800834
Marcus Hagerott819214d2016-09-29 14:58:27 -0700835 final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700836 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
837 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
838 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
Walter Jang72f99882016-05-26 09:01:31 -0700839
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700840 mGroupsDao.delete(groupUri);
841
842 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
843 }
844
845 public static Intent createUndoIntent(Context context, Intent resultIntent) {
846 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
847 serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
848 serviceIntent.putExtras(resultIntent);
849 return serviceIntent;
850 }
851
852 private void undo(Intent intent) {
853 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
854 if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
855 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
Walter Jang72f99882016-05-26 09:01:31 -0700856 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800857 }
858
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700859
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800860 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700861 * Creates an intent that can be sent to this service to rename a group as
862 * well as add and remove members from the group.
863 *
864 * @param context of the application
865 * @param groupId of the group that should be modified
866 * @param newLabel is the updated name of the group (can be null if the name
867 * should not be updated)
868 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
869 * should be added to the group
870 * @param rawContactsToRemove is an array of raw contact IDs for contacts
871 * that should be removed from the group
872 * @param callbackActivity is the activity to send the callback intent to
873 * @param callbackAction is the intent action for the callback intent
874 */
875 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
876 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700877 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700878 Intent serviceIntent = new Intent(context, ContactSaveService.class);
879 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
880 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
881 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
882 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
883 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
884 rawContactsToRemove);
885
886 // Callback intent will be invoked by the service once the group is updated
887 Intent callbackIntent = new Intent(context, callbackActivity);
888 callbackIntent.setAction(callbackAction);
889 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
890
891 return serviceIntent;
892 }
893
894 private void updateGroup(Intent intent) {
895 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
896 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
897 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
898 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
899
900 if (groupId == -1) {
901 Log.e(TAG, "Invalid arguments for updateGroup request");
902 return;
903 }
904
905 final ContentResolver resolver = getContentResolver();
906 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
907
908 // Update group name if necessary
909 if (label != null) {
910 ContentValues values = new ContentValues();
911 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700912 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700913 }
914
Katherine Kuan717e3432011-07-13 17:03:24 -0700915 // Add and remove members if necessary
916 addMembersToGroup(resolver, rawContactsToAdd, groupId);
917 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
918
919 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
920 callbackIntent.setData(groupUri);
921 deliverCallback(callbackIntent);
922 }
923
Walter Jang3a0b4832016-10-12 11:02:54 -0700924 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700925 long groupId) {
926 if (rawContactsToAdd == null) {
927 return;
928 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700929 for (long rawContactId : rawContactsToAdd) {
930 try {
931 final ArrayList<ContentProviderOperation> rawContactOperations =
932 new ArrayList<ContentProviderOperation>();
933
934 // Build an assert operation to ensure the contact is not already in the group
935 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
936 .newAssertQuery(Data.CONTENT_URI);
937 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
938 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
939 new String[] { String.valueOf(rawContactId),
940 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
941 assertBuilder.withExpectedCount(0);
942 rawContactOperations.add(assertBuilder.build());
943
944 // Build an insert operation to add the contact to the group
945 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
946 .newInsert(Data.CONTENT_URI);
947 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
948 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
949 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
950 rawContactOperations.add(insertBuilder.build());
951
952 if (DEBUG) {
953 for (ContentProviderOperation operation : rawContactOperations) {
954 Log.v(TAG, operation.toString());
955 }
956 }
957
958 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700959 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800960 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700961 }
962 } catch (RemoteException e) {
963 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700964 FeedbackHelper.sendFeedback(this, TAG,
965 "Problem persisting user edits for raw contact ID " +
966 String.valueOf(rawContactId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700967 } catch (OperationApplicationException e) {
968 // The assert could have failed because the contact is already in the group,
969 // just continue to the next contact
Walter Jang3a0b4832016-10-12 11:02:54 -0700970 FeedbackHelper.sendFeedback(this, TAG,
971 "Assert failed in adding raw contact ID " +
972 String.valueOf(rawContactId) + ". Already exists in group " +
973 String.valueOf(groupId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700974 }
975 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700976 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700977
Daniel Lehmann18958a22012-02-28 17:45:25 -0800978 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700979 long groupId) {
980 if (rawContactsToRemove == null) {
981 return;
982 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700983 for (long rawContactId : rawContactsToRemove) {
984 // Apply the delete operation on the data row for the given raw contact's
985 // membership in the given group. If no contact matches the provided selection, then
986 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800987 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700988 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
989 new String[] { String.valueOf(rawContactId),
990 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
991 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700992 }
993
994 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800995 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800996 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800997 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
998 Intent serviceIntent = new Intent(context, ContactSaveService.class);
999 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
1000 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1001 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
1002
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001003 return serviceIntent;
1004 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001005
1006 private void setStarred(Intent intent) {
1007 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1008 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
1009 if (contactUri == null) {
1010 Log.e(TAG, "Invalid arguments for setStarred request");
1011 return;
1012 }
1013
1014 final ContentValues values = new ContentValues(1);
1015 values.put(Contacts.STARRED, value);
1016 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -07001017
1018 // Undemote the contact if necessary
1019 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
1020 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -08001021 if (c == null) {
1022 return;
1023 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001024 try {
1025 if (c.moveToFirst()) {
1026 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -07001027
1028 // Don't bother undemoting if this contact is the user's profile.
1029 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -08001030 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -07001031 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001032 }
1033 } finally {
1034 c.close();
1035 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001036 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001037
1038 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -07001039 * Creates an intent that can be sent to this service to set the redirect to voicemail.
1040 */
1041 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
1042 boolean value) {
1043 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1044 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1045 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1046 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1047
1048 return serviceIntent;
1049 }
1050
1051 private void setSendToVoicemail(Intent intent) {
1052 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1053 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1054 if (contactUri == null) {
1055 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1056 return;
1057 }
1058
1059 final ContentValues values = new ContentValues(1);
1060 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1061 getContentResolver().update(contactUri, values, null, null);
1062 }
1063
1064 /**
1065 * Creates an intent that can be sent to this service to save the contact's ringtone.
1066 */
1067 public static Intent createSetRingtone(Context context, Uri contactUri,
1068 String value) {
1069 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1070 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1071 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1072 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1073
1074 return serviceIntent;
1075 }
1076
1077 private void setRingtone(Intent intent) {
1078 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1079 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1080 if (contactUri == null) {
1081 Log.e(TAG, "Invalid arguments for setRingtone");
1082 return;
1083 }
1084 ContentValues values = new ContentValues(1);
1085 values.put(Contacts.CUSTOM_RINGTONE, value);
1086 getContentResolver().update(contactUri, values, null, null);
1087 }
1088
1089 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001090 * Creates an intent that sets the selected data item as super primary (default)
1091 */
1092 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1093 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1094 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1095 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1096 return serviceIntent;
1097 }
1098
1099 private void setSuperPrimary(Intent intent) {
1100 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1101 if (dataId == -1) {
1102 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1103 return;
1104 }
1105
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001106 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001107 }
1108
1109 /**
1110 * Creates an intent that clears the primary flag of all data items that belong to the same
1111 * raw_contact as the given data item. Will only clear, if the data item was primary before
1112 * this call
1113 */
1114 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1115 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1116 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1117 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1118 return serviceIntent;
1119 }
1120
1121 private void clearPrimary(Intent intent) {
1122 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1123 if (dataId == -1) {
1124 Log.e(TAG, "Invalid arguments for clearPrimary request");
1125 return;
1126 }
1127
1128 // Update the primary values in the data record.
1129 ContentValues values = new ContentValues(1);
1130 values.put(Data.IS_SUPER_PRIMARY, 0);
1131 values.put(Data.IS_PRIMARY, 0);
1132
1133 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1134 values, null, null);
1135 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001136
1137 /**
1138 * Creates an intent that can be sent to this service to delete a contact.
1139 */
1140 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1141 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1142 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1143 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1144 return serviceIntent;
1145 }
1146
Brian Attwelld2962a32015-03-02 14:48:50 -08001147 /**
1148 * Creates an intent that can be sent to this service to delete multiple contacts.
1149 */
1150 public static Intent createDeleteMultipleContactsIntent(Context context,
1151 long[] contactIds) {
1152 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1153 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1154 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
1155 return serviceIntent;
1156 }
1157
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001158 private void deleteContact(Intent intent) {
1159 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1160 if (contactUri == null) {
1161 Log.e(TAG, "Invalid arguments for deleteContact request");
1162 return;
1163 }
1164
1165 getContentResolver().delete(contactUri, null, null);
1166 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001167
Brian Attwelld2962a32015-03-02 14:48:50 -08001168 private void deleteMultipleContacts(Intent intent) {
1169 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1170 if (contactIds == null) {
1171 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1172 return;
1173 }
1174 for (long contactId : contactIds) {
1175 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1176 getContentResolver().delete(contactUri, null, null);
1177 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001178 final String deleteToastMessage = getResources().getQuantityString(R.plurals
1179 .contacts_deleted_toast, contactIds.length);
1180 mMainHandler.post(new Runnable() {
1181 @Override
1182 public void run() {
1183 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1184 .show();
1185 }
1186 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001187 }
1188
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001189 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001190 * 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 -07001191 * pieces. This will set the raw contact ids to TYPE_AUTOMATIC for AggregationExceptions so
1192 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001193 */
1194 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1195 ResultReceiver receiver) {
1196 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1197 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1198 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1199 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1200 return serviceIntent;
1201 }
1202
1203 private void splitContact(Intent intent) {
1204 final long rawContactIds[][] = (long[][]) intent
1205 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001206 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Mai7efa9942016-05-12 11:26:49 -07001207 if (rawContactIds == null) {
1208 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001209 if (receiver != null) {
1210 receiver.send(BAD_ARGUMENTS, new Bundle());
1211 }
Gary Mai7efa9942016-05-12 11:26:49 -07001212 return;
1213 }
1214 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1215 final ContentResolver resolver = getContentResolver();
1216 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001217 for (int i = 0; i < rawContactIds.length; i++) {
1218 for (int j = 0; j < rawContactIds.length; j++) {
1219 if (i != j) {
1220 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
1221 if (receiver != null) {
1222 receiver.send(CP2_ERROR, new Bundle());
1223 return;
1224 }
1225 }
1226 }
1227 }
1228 }
1229 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1230 if (receiver != null) {
1231 receiver.send(CP2_ERROR, new Bundle());
1232 }
1233 return;
1234 }
1235 if (receiver != null) {
1236 receiver.send(CONTACTS_SPLIT, new Bundle());
1237 } else {
1238 showToast(R.string.contactUnlinkedToast);
1239 }
1240 }
1241
1242 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001243 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001244 * and {@param rawContactIds2} to {@param operations}.
1245 * @return false if an error occurred, true otherwise.
1246 */
1247 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
1248 long[] rawContactIds1, long[] rawContactIds2) {
1249 if (rawContactIds1 == null || rawContactIds2 == null) {
1250 Log.e(TAG, "Invalid arguments for splitContact request");
1251 return false;
1252 }
1253 // For each pair of raw contacts, insert an aggregation exception
1254 final ContentResolver resolver = getContentResolver();
1255 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1256 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1257 for (int i = 0; i < rawContactIds1.length; i++) {
1258 for (int j = 0; j < rawContactIds2.length; j++) {
1259 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
1260 // Before we get to 500 we need to flush the operations list
1261 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1262 if (!applyOperations(resolver, operations)) {
1263 return false;
1264 }
1265 operations.clear();
1266 }
1267 }
1268 }
1269 return true;
1270 }
1271
1272 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001273 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001274 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001275 */
1276 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001277 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001278 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1279 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1280 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1281 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001282
1283 // Callback intent will be invoked by the service once the contacts are joined.
1284 Intent callbackIntent = new Intent(context, callbackActivity);
1285 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001286 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1287
1288 return serviceIntent;
1289 }
1290
Brian Attwelld3946ca2015-03-03 11:13:49 -08001291 /**
1292 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1293 * No special attention is paid to where the resulting contact's name is taken from.
1294 */
Gary Mai7efa9942016-05-12 11:26:49 -07001295 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1296 ResultReceiver receiver) {
1297 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001298 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1299 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001300 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001301 return serviceIntent;
1302 }
1303
Gary Mai7efa9942016-05-12 11:26:49 -07001304 /**
1305 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1306 * No special attention is paid to where the resulting contact's name is taken from.
1307 */
1308 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1309 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1310 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001311
1312 private interface JoinContactQuery {
1313 String[] PROJECTION = {
1314 RawContacts._ID,
1315 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001316 RawContacts.DISPLAY_NAME_SOURCE,
1317 };
1318
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001319 int _ID = 0;
1320 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001321 int DISPLAY_NAME_SOURCE = 2;
1322 }
1323
1324 private interface ContactEntityQuery {
1325 String[] PROJECTION = {
1326 Contacts.Entity.DATA_ID,
1327 Contacts.Entity.CONTACT_ID,
1328 Contacts.Entity.IS_SUPER_PRIMARY,
1329 };
1330 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1331 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1332 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1333 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1334
1335 int DATA_ID = 0;
1336 int CONTACT_ID = 1;
1337 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001338 }
1339
Brian Attwelld3946ca2015-03-03 11:13:49 -08001340 private void joinSeveralContacts(Intent intent) {
1341 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001342
Gary Mai7efa9942016-05-12 11:26:49 -07001343 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001344
Brian Attwelld3946ca2015-03-03 11:13:49 -08001345 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001346 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1347 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001348 if (rawContactIds == null) {
1349 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001350 if (receiver != null) {
1351 receiver.send(BAD_ARGUMENTS, new Bundle());
1352 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001353 return;
1354 }
1355
Brian Attwelld3946ca2015-03-03 11:13:49 -08001356 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001357 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001358 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1359 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1360 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001361 for (int i = 0; i < rawContactIds.length; i++) {
1362 for (int j = 0; j < rawContactIds.length; j++) {
1363 if (i != j) {
1364 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1365 }
Walter Jang0653de32015-07-24 12:12:40 -07001366 // Before we get to 500 we need to flush the operations list
1367 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001368 if (!applyOperations(resolver, operations)) {
1369 if (receiver != null) {
1370 receiver.send(CP2_ERROR, new Bundle());
1371 }
Walter Jang0653de32015-07-24 12:12:40 -07001372 return;
1373 }
1374 operations.clear();
1375 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001376 }
1377 }
Gary Mai7efa9942016-05-12 11:26:49 -07001378 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1379 if (receiver != null) {
1380 receiver.send(CP2_ERROR, new Bundle());
1381 }
Walter Jang0653de32015-07-24 12:12:40 -07001382 return;
1383 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001384
John Shaoa3c507a2016-09-13 14:26:17 -07001385
1386 final String name = queryNameOfLinkedContacts(contactIds);
1387 if (name != null) {
1388 if (receiver != null) {
1389 final Bundle result = new Bundle();
1390 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1391 result.putString(EXTRA_DISPLAY_NAME, name);
1392 receiver.send(CONTACTS_LINKED, result);
1393 } else {
1394 showToast(R.string.contactsJoinedMessage);
1395 }
Gary Mai7efa9942016-05-12 11:26:49 -07001396 } else {
John Shaoa3c507a2016-09-13 14:26:17 -07001397 if (receiver != null) {
1398 receiver.send(CP2_ERROR, new Bundle());
1399 }
1400 showToast(R.string.contactJoinErrorToast);
Gary Mai7efa9942016-05-12 11:26:49 -07001401 }
Walter Jang0653de32015-07-24 12:12:40 -07001402 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001403
John Shaoa3c507a2016-09-13 14:26:17 -07001404 /** Get the display name of the top-level contact after the contacts have been linked. */
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001405 private String queryNameOfLinkedContacts(long[] contactIds) {
1406 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1407 final String[] whereArgs = new String[contactIds.length];
1408 for (int i = 0; i < contactIds.length; i++) {
1409 whereArgs[i] = String.valueOf(contactIds[i]);
1410 whereBuilder.append("?,");
1411 }
1412 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1413 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
John Shaoa3c507a2016-09-13 14:26:17 -07001414 new String[]{Contacts._ID, Contacts.DISPLAY_NAME},
1415 whereBuilder.toString(), whereArgs, null);
1416
1417 String name = null;
1418 long contactId = 0;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001419 try {
1420 if (cursor.moveToFirst()) {
John Shaoa3c507a2016-09-13 14:26:17 -07001421 contactId = cursor.getLong(0);
1422 name = cursor.getString(1);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001423 }
John Shaoa3c507a2016-09-13 14:26:17 -07001424 while(cursor.moveToNext()) {
1425 if (cursor.getLong(0) != contactId) {
1426 return null;
1427 }
1428 }
1429 return name == null ? "" : name;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001430 } finally {
John Shaoa3c507a2016-09-13 14:26:17 -07001431 if (cursor != null) {
1432 cursor.close();
1433 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001434 }
1435 }
1436
Walter Jang0653de32015-07-24 12:12:40 -07001437 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001438 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001439 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001440 try {
John Shaoa3c507a2016-09-13 14:26:17 -07001441 final ContentProviderResult[] result =
1442 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1443 for (int i = 0; i < result.length; ++i) {
1444 // if no rows were modified in the operation then we count it as fail.
1445 if (result[i].count < 0) {
1446 throw new OperationApplicationException();
1447 }
1448 }
Walter Jang0653de32015-07-24 12:12:40 -07001449 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001450 } catch (RemoteException | OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001451 FeedbackHelper.sendFeedback(this, TAG,
1452 "Failed to apply aggregation exception batch", e);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001453 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001454 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001455 }
1456 }
1457
Brian Attwelld3946ca2015-03-03 11:13:49 -08001458 private void joinContacts(Intent intent) {
1459 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1460 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001461
1462 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001463 // in the join UIs.
1464 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1465 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001466 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001467 return;
1468 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001469
Brian Attwell548f5c62015-01-27 17:46:46 -08001470 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001471
1472 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001473 for (int i = 0; i < rawContactIds.length; i++) {
1474 for (int j = 0; j < rawContactIds.length; j++) {
1475 if (i != j) {
1476 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1477 }
1478 }
1479 }
1480
Brian Attwelld3946ca2015-03-03 11:13:49 -08001481 final ContentResolver resolver = getContentResolver();
1482
Brian Attwell548f5c62015-01-27 17:46:46 -08001483 // Use the name for contactId1 as the name for the newly aggregated contact.
1484 final Uri contactId1Uri = ContentUris.withAppendedId(
1485 Contacts.CONTENT_URI, contactId1);
1486 final Uri entityUri = Uri.withAppendedPath(
1487 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1488 Cursor c = resolver.query(entityUri,
1489 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1490 if (c == null) {
1491 Log.e(TAG, "Unable to open Contacts DB cursor");
1492 showToast(R.string.contactSavedErrorToast);
1493 return;
1494 }
1495 long dataIdToAddSuperPrimary = -1;
1496 try {
1497 if (c.moveToFirst()) {
1498 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1499 }
1500 } finally {
1501 c.close();
1502 }
1503
1504 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1505 // display name does not change as a result of the join.
1506 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001507 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001508 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1509 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1510 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001511 operations.add(builder.build());
1512 }
1513
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001514 // Apply all aggregation exceptions as one batch
John Shaoa3c507a2016-09-13 14:26:17 -07001515 final boolean success = applyOperations(resolver, operations);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001516
John Shaoa3c507a2016-09-13 14:26:17 -07001517 final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2});
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001518 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
John Shaoa3c507a2016-09-13 14:26:17 -07001519 if (success && name != null) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001520 Uri uri = RawContacts.getContactLookupUri(resolver,
1521 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1522 callbackIntent.setData(uri);
1523 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001524 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001525 }
1526
Gary Mai7efa9942016-05-12 11:26:49 -07001527 /**
1528 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1529 * array of the return value holds an array of raw contact ids for one contactId.
1530 * @param contactIds
1531 * @return
1532 */
1533 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1534 final long[][] rawContactIds = new long[contactIds.length][];
1535 for (int i = 0; i < contactIds.length; i++) {
1536 rawContactIds[i] = getRawContactIds(contactIds[i]);
1537 }
1538 return rawContactIds;
1539 }
1540
1541 /**
1542 * Gets the raw contact ids associated with {@param contactId}.
1543 * @param contactId
1544 * @return Array of raw contact ids.
1545 */
1546 private long[] getRawContactIds(long contactId) {
1547 final ContentResolver resolver = getContentResolver();
1548 long rawContactIds[];
1549
1550 final StringBuilder queryBuilder = new StringBuilder();
1551 queryBuilder.append(RawContacts.CONTACT_ID)
1552 .append("=")
1553 .append(String.valueOf(contactId));
1554
1555 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1556 JoinContactQuery.PROJECTION,
1557 queryBuilder.toString(),
1558 null, null);
1559 if (c == null) {
1560 Log.e(TAG, "Unable to open Contacts DB cursor");
1561 return null;
1562 }
1563 try {
1564 rawContactIds = new long[c.getCount()];
1565 for (int i = 0; i < rawContactIds.length; i++) {
1566 c.moveToPosition(i);
1567 final long rawContactId = c.getLong(JoinContactQuery._ID);
1568 rawContactIds[i] = rawContactId;
1569 }
1570 } finally {
1571 c.close();
1572 }
1573 return rawContactIds;
1574 }
1575
Brian Attwelld3946ca2015-03-03 11:13:49 -08001576 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1577 if (contactIds == null) {
1578 return null;
1579 }
1580
Brian Attwell548f5c62015-01-27 17:46:46 -08001581 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001582
1583 final StringBuilder queryBuilder = new StringBuilder();
1584 final String stringContactIds[] = new String[contactIds.length];
1585 for (int i = 0; i < contactIds.length; i++) {
1586 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1587 stringContactIds[i] = String.valueOf(contactIds[i]);
1588 if (contactIds[i] == -1) {
1589 return null;
1590 }
1591 if (i == contactIds.length -1) {
1592 break;
1593 }
1594 queryBuilder.append(" OR ");
1595 }
1596
Brian Attwell548f5c62015-01-27 17:46:46 -08001597 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1598 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001599 queryBuilder.toString(),
1600 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001601 if (c == null) {
1602 Log.e(TAG, "Unable to open Contacts DB cursor");
1603 showToast(R.string.contactSavedErrorToast);
1604 return null;
1605 }
Gary Mai7efa9942016-05-12 11:26:49 -07001606 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001607 try {
1608 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001609 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001610 return null;
1611 }
1612 rawContactIds = new long[c.getCount()];
1613 for (int i = 0; i < rawContactIds.length; i++) {
1614 c.moveToPosition(i);
1615 long rawContactId = c.getLong(JoinContactQuery._ID);
1616 rawContactIds[i] = rawContactId;
1617 }
1618 } finally {
1619 c.close();
1620 }
1621 return rawContactIds;
1622 }
1623
Brian Attwelld3946ca2015-03-03 11:13:49 -08001624 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1625 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1626 }
1627
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001628 /**
1629 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1630 */
1631 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1632 long rawContactId1, long rawContactId2) {
1633 Builder builder =
1634 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1635 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1636 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1637 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1638 operations.add(builder.build());
1639 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001640
1641 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001642 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} ContentProviderOperation.
Gary Mai7efa9942016-05-12 11:26:49 -07001643 */
1644 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
1645 long rawContactId1, long rawContactId2) {
1646 final Builder builder =
1647 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Mai53fe0d22016-07-26 17:23:53 -07001648 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001649 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1650 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1651 operations.add(builder.build());
1652 }
1653
Marcus Hagerott819214d2016-09-29 14:58:27 -07001654 public static Intent createImportFromSimIntent(Context context,
1655 ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
1656 return new Intent(context, ContactSaveService.class)
1657 .setAction(ACTION_IMPORT_FROM_SIM)
1658 .putExtra(EXTRA_SIM_CONTACTS, contacts)
1659 .putExtra(EXTRA_ACCOUNT, targetAccount);
1660 }
1661
1662 private void importFromSim(Intent intent) {
1663 final Intent result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
1664 .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, System.currentTimeMillis());
1665 try {
1666 final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
1667 final ArrayList<SimContact> contacts =
1668 intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
1669 mSimContactDao.importContacts(contacts, targetAccount);
1670 // notify success
1671 LocalBroadcastManager.getInstance(this).sendBroadcast(result
1672 .putExtra(EXTRA_RESULT_COUNT, contacts.size())
1673 .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS));
1674 if (Log.isLoggable(TAG, Log.DEBUG)) {
1675 Log.d(TAG, "importFromSim completed successfully");
1676 }
1677 } catch (RemoteException|OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001678 FeedbackHelper.sendFeedback(this, TAG, "Failed to import contacts from SIM card", e);
Marcus Hagerott819214d2016-09-29 14:58:27 -07001679 LocalBroadcastManager.getInstance(this).sendBroadcast(result
1680 .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE));
1681 }
1682 }
1683
Gary Mai7efa9942016-05-12 11:26:49 -07001684 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001685 * Shows a toast on the UI thread.
1686 */
1687 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001688 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001689
1690 @Override
1691 public void run() {
1692 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1693 }
1694 });
1695 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001696
1697 private void deliverCallback(final Intent callbackIntent) {
1698 mMainHandler.post(new Runnable() {
1699
1700 @Override
1701 public void run() {
1702 deliverCallbackOnUiThread(callbackIntent);
1703 }
1704 });
1705 }
1706
1707 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1708 // TODO: this assumes that if there are multiple instances of the same
1709 // activity registered, the last one registered is the one waiting for
1710 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001711 for (Listener listener : sListeners) {
1712 if (callbackIntent.getComponent().equals(
1713 ((Activity) listener).getIntent().getComponent())) {
1714 listener.onServiceCompleted(callbackIntent);
1715 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001716 }
1717 }
1718 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001719
1720 public interface GroupsDao {
1721 Uri create(String title, AccountWithDataSet account);
1722 int delete(Uri groupUri);
1723 Bundle captureDeletionUndoData(Uri groupUri);
1724 Uri undoDeletion(Bundle undoData);
1725 }
1726
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001727 public static class GroupsDaoImpl implements GroupsDao {
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001728 public static final String KEY_GROUP_DATA = "groupData";
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001729 public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
1730
1731 private static final String TAG = "GroupsDao";
1732 private final Context context;
1733 private final ContentResolver contentResolver;
1734
1735 public GroupsDaoImpl(Context context) {
1736 this(context, context.getContentResolver());
1737 }
1738
1739 public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
1740 this.context = context;
1741 this.contentResolver = contentResolver;
1742 }
1743
1744 public Bundle captureDeletionUndoData(Uri groupUri) {
1745 final long groupId = ContentUris.parseId(groupUri);
1746 final Bundle result = new Bundle();
1747
1748 final Cursor cursor = contentResolver.query(groupUri,
1749 new String[]{
1750 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
1751 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
1752 Groups.SHOULD_SYNC
1753 },
1754 Groups.DELETED + "=?", new String[] { "0" }, null);
1755 try {
1756 if (cursor.moveToFirst()) {
1757 final ContentValues groupValues = new ContentValues();
1758 DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
1759 result.putParcelable(KEY_GROUP_DATA, groupValues);
1760 } else {
1761 // Group doesn't exist.
1762 return result;
1763 }
1764 } finally {
1765 cursor.close();
1766 }
1767
1768 final Cursor membersCursor = contentResolver.query(
1769 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
1770 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1771 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
1772 final long[] memberIds = new long[membersCursor.getCount()];
1773 int i = 0;
1774 while (membersCursor.moveToNext()) {
1775 memberIds[i++] = membersCursor.getLong(0);
1776 }
1777 result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
1778 return result;
1779 }
1780
1781 public Uri undoDeletion(Bundle deletedGroupData) {
1782 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
1783 if (groupData == null) {
1784 return null;
1785 }
1786 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
1787 final long groupId = ContentUris.parseId(groupUri);
1788
1789 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
1790 if (memberIds == null) {
1791 return groupUri;
1792 }
1793 final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
1794 for (int i = 0; i < memberIds.length; i++) {
1795 memberInsertions[i] = new ContentValues();
1796 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
1797 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1798 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
1799 }
1800 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
1801 if (inserted != memberIds.length) {
1802 Log.e(TAG, "Could not recover some members for group deletion undo");
1803 }
1804
1805 return groupUri;
1806 }
1807
1808 public Uri create(String title, AccountWithDataSet account) {
1809 final ContentValues values = new ContentValues();
1810 values.put(Groups.TITLE, title);
1811 values.put(Groups.ACCOUNT_NAME, account.name);
1812 values.put(Groups.ACCOUNT_TYPE, account.type);
1813 values.put(Groups.DATA_SET, account.dataSet);
1814 return contentResolver.insert(Groups.CONTENT_URI, values);
1815 }
1816
1817 public int delete(Uri groupUri) {
1818 return contentResolver.delete(groupUri, null, null);
1819 }
1820 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001821}