blob: 1b2c22dea10b3a8f44d08e2c4539f2094a43ba22 [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;
James Laskeyf62b4882016-10-21 11:36:40 -070050import android.text.TextUtils;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070051import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080052import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070053
Gary Mai363af602016-09-28 10:01:23 -070054import com.android.contacts.activities.ContactEditorActivity;
Wenyi Wang67addcc2015-11-23 10:07:48 -080055import com.android.contacts.common.compat.CompatUtils;
Chiao Chengd7ca03e2012-10-24 15:14:08 -070056import com.android.contacts.common.database.ContactUpdateUtils;
Marcus Hagerott819214d2016-09-29 14:58:27 -070057import com.android.contacts.common.database.SimContactDao;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080058import com.android.contacts.common.model.AccountTypeManager;
Wenyi Wang67addcc2015-11-23 10:07:48 -080059import com.android.contacts.common.model.CPOWrapper;
Yorke Leecd321f62013-10-28 15:20:15 -070060import com.android.contacts.common.model.RawContactDelta;
61import com.android.contacts.common.model.RawContactDeltaList;
62import com.android.contacts.common.model.RawContactModifier;
Marcus Hagerott819214d2016-09-29 14:58:27 -070063import com.android.contacts.common.model.SimContact;
Chiao Cheng428f0082012-11-13 18:38:56 -080064import com.android.contacts.common.model.account.AccountWithDataSet;
James Laskeyf62b4882016-10-21 11:36:40 -070065import com.android.contacts.common.preference.ContactsPreferences;
66import com.android.contacts.common.util.ContactDisplayUtils;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070067import com.android.contacts.common.util.PermissionsUtil;
Wenyi Wangaac0e662015-12-18 17:17:33 -080068import com.android.contacts.compat.PinnedPositionsCompat;
Yorke Lee637a38e2013-09-14 08:36:33 -070069import com.android.contacts.util.ContactPhotoUtils;
Walter Jang3a0b4832016-10-12 11:02:54 -070070import com.android.contactsbind.FeedbackHelper;
71
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070072import com.google.common.collect.Lists;
73import com.google.common.collect.Sets;
74
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080075import java.util.ArrayList;
76import java.util.HashSet;
77import java.util.List;
78import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070079
Marcus Hagerott819214d2016-09-29 14:58:27 -070080import static android.Manifest.permission.WRITE_CONTACTS;
81
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080082/**
83 * A service responsible for saving changes to the content provider.
84 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070085public class ContactSaveService extends IntentService {
86 private static final String TAG = "ContactSaveService";
87
Katherine Kuana007e442011-07-07 09:25:34 -070088 /** Set to true in order to view logs on content provider operations */
89 private static final boolean DEBUG = false;
90
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070091 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
92
93 public static final String EXTRA_ACCOUNT_NAME = "accountName";
94 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070095 public static final String EXTRA_DATA_SET = "dataSet";
Marcus Hagerott819214d2016-09-29 14:58:27 -070096 public static final String EXTRA_ACCOUNT = "account";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070097 public static final String EXTRA_CONTENT_VALUES = "contentValues";
98 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
Gary Mai7efa9942016-05-12 11:26:49 -070099 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
100 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700101
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800102 public static final String ACTION_SAVE_CONTACT = "saveContact";
103 public static final String EXTRA_CONTACT_STATE = "state";
104 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700105 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -0700106 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -0800107 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700108
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800109 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800110 public static final String ACTION_RENAME_GROUP = "renameGroup";
111 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700112 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800113 public static final String EXTRA_GROUP_ID = "groupId";
114 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700115 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
116 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800117
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800118 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800119 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800120 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800121 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800122 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800123 public static final String EXTRA_STARRED_FLAG = "starred";
Marcus Hagerott3bb85142016-07-29 10:46:36 -0700124 public static final String EXTRA_DISPLAY_NAME = "extraDisplayName";
James Laskeye5a140a2016-10-18 15:43:42 -0700125 public static final String EXTRA_DISPLAY_NAME_ARRAY = "extraDisplayNameArray";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800126
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800127 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
128 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
129 public static final String EXTRA_DATA_ID = "dataId";
130
Gary Mai7efa9942016-05-12 11:26:49 -0700131 public static final String ACTION_SPLIT_CONTACT = "splitContact";
132
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800133 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800134 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800135 public static final String EXTRA_CONTACT_ID1 = "contactId1";
136 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800137
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700138 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
139 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
140
141 public static final String ACTION_SET_RINGTONE = "setRingtone";
142 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
143
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700144 public static final String ACTION_UNDO = "undo";
145 public static final String EXTRA_UNDO_ACTION = "undoAction";
146 public static final String EXTRA_UNDO_DATA = "undoData";
147
Marcus Hagerott819214d2016-09-29 14:58:27 -0700148 public static final String ACTION_IMPORT_FROM_SIM = "importFromSim";
149 public static final String EXTRA_SIM_CONTACTS = "simContacts";
150
151 public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
152 public static final String BROADCAST_SIM_IMPORT_COMPLETE = "simImportComplete";
153
154 public static final String EXTRA_RESULT_CODE = "resultCode";
155 public static final String EXTRA_RESULT_COUNT = "count";
156 public static final String EXTRA_OPERATION_REQUESTED_AT_TIME = "requestedTime";
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700157
Gary Mai7efa9942016-05-12 11:26:49 -0700158 public static final int CP2_ERROR = 0;
159 public static final int CONTACTS_LINKED = 1;
160 public static final int CONTACTS_SPLIT = 2;
Gary Mai31d572e2016-06-03 14:04:32 -0700161 public static final int BAD_ARGUMENTS = 3;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700162 public static final int RESULT_UNKNOWN = 0;
163 public static final int RESULT_SUCCESS = 1;
164 public static final int RESULT_FAILURE = 2;
Gary Mai7efa9942016-05-12 11:26:49 -0700165
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700166 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
167 Data.MIMETYPE,
168 Data.IS_PRIMARY,
169 Data.DATA1,
170 Data.DATA2,
171 Data.DATA3,
172 Data.DATA4,
173 Data.DATA5,
174 Data.DATA6,
175 Data.DATA7,
176 Data.DATA8,
177 Data.DATA9,
178 Data.DATA10,
179 Data.DATA11,
180 Data.DATA12,
181 Data.DATA13,
182 Data.DATA14,
183 Data.DATA15
184 );
185
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800186 private static final int PERSIST_TRIES = 3;
187
Walter Jang0653de32015-07-24 12:12:40 -0700188 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
189
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800190 public interface Listener {
191 public void onServiceCompleted(Intent callbackIntent);
192 }
193
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100194 private static final CopyOnWriteArrayList<Listener> sListeners =
195 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800196
197 private Handler mMainHandler;
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700198 private GroupsDao mGroupsDao;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700199 private SimContactDao mSimContactDao;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800200
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700201 public ContactSaveService() {
202 super(TAG);
203 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800204 mMainHandler = new Handler(Looper.getMainLooper());
205 }
206
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700207 @Override
208 public void onCreate() {
209 super.onCreate();
210 mGroupsDao = new GroupsDaoImpl(this);
Marcus Hagerott819214d2016-09-29 14:58:27 -0700211 mSimContactDao = new SimContactDao(this);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700212 }
213
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800214 public static void registerListener(Listener listener) {
215 if (!(listener instanceof Activity)) {
216 throw new ClassCastException("Only activities can be registered to"
217 + " receive callback from " + ContactSaveService.class.getName());
218 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100219 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800220 }
221
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700222 public static boolean canUndo(Intent resultIntent) {
223 return resultIntent.hasExtra(EXTRA_UNDO_DATA);
224 }
225
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800226 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100227 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700228 }
229
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800230 /**
231 * Returns true if the ContactSaveService was started successfully and false if an exception
232 * was thrown and a Toast error message was displayed.
233 */
234 public static boolean startService(Context context, Intent intent, int saveMode) {
235 try {
236 context.startService(intent);
237 } catch (Exception exception) {
238 final int resId;
239 switch (saveMode) {
Gary Mai363af602016-09-28 10:01:23 -0700240 case ContactEditorActivity.ContactEditor.SaveMode.SPLIT:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800241 resId = R.string.contactUnlinkErrorToast;
242 break;
Gary Mai363af602016-09-28 10:01:23 -0700243 case ContactEditorActivity.ContactEditor.SaveMode.RELOAD:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800244 resId = R.string.contactJoinErrorToast;
245 break;
Gary Mai363af602016-09-28 10:01:23 -0700246 case ContactEditorActivity.ContactEditor.SaveMode.CLOSE:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800247 resId = R.string.contactSavedErrorToast;
248 break;
249 default:
250 resId = R.string.contactGenericErrorToast;
251 }
252 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
253 return false;
254 }
255 return true;
256 }
257
258 /**
259 * Utility method that starts service and handles exception.
260 */
261 public static void startService(Context context, Intent intent) {
262 try {
263 context.startService(intent);
264 } catch (Exception exception) {
265 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
266 }
267 }
268
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700269 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800270 public Object getSystemService(String name) {
271 Object service = super.getSystemService(name);
272 if (service != null) {
273 return service;
274 }
275
276 return getApplicationContext().getSystemService(name);
277 }
278
279 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700280 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800281 if (intent == null) {
282 Log.d(TAG, "onHandleIntent: could not handle null intent");
283 return;
284 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700285 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
286 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
287 // TODO: add more specific error string such as "Turn on Contacts
288 // permission to update your contacts"
289 showToast(R.string.contactSavedErrorToast);
290 return;
291 }
292
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700293 // Call an appropriate method. If we're sure it affects how incoming phone calls are
294 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700295 String action = intent.getAction();
296 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
297 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800298 } else if (ACTION_SAVE_CONTACT.equals(action)) {
299 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800300 } else if (ACTION_CREATE_GROUP.equals(action)) {
301 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800302 } else if (ACTION_RENAME_GROUP.equals(action)) {
303 renameGroup(intent);
304 } else if (ACTION_DELETE_GROUP.equals(action)) {
305 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700306 } else if (ACTION_UPDATE_GROUP.equals(action)) {
307 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800308 } else if (ACTION_SET_STARRED.equals(action)) {
309 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800310 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
311 setSuperPrimary(intent);
312 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
313 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800314 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
315 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800316 } else if (ACTION_DELETE_CONTACT.equals(action)) {
317 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700318 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
319 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800320 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
321 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800322 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
323 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700324 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
325 setSendToVoicemail(intent);
326 } else if (ACTION_SET_RINGTONE.equals(action)) {
327 setRingtone(intent);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700328 } else if (ACTION_UNDO.equals(action)) {
329 undo(intent);
Marcus Hagerott819214d2016-09-29 14:58:27 -0700330 } else if (ACTION_IMPORT_FROM_SIM.equals(action)) {
331 importFromSim(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700332 }
333 }
334
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800335 /**
336 * Creates an intent that can be sent to this service to create a new raw contact
337 * using data presented as a set of ContentValues.
338 */
339 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700340 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700341 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800342 Intent serviceIntent = new Intent(
343 context, ContactSaveService.class);
344 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
345 if (account != null) {
346 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
347 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700348 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800349 }
350 serviceIntent.putParcelableArrayListExtra(
351 ContactSaveService.EXTRA_CONTENT_VALUES, values);
352
353 // Callback intent will be invoked by the service once the new contact is
354 // created. The service will put the URI of the new contact as "data" on
355 // the callback intent.
356 Intent callbackIntent = new Intent(context, callbackActivity);
357 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800358 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
359 return serviceIntent;
360 }
361
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700362 private void createRawContact(Intent intent) {
363 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
364 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700365 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700366 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
367 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
368
369 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
370 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
371 .withValue(RawContacts.ACCOUNT_NAME, accountName)
372 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700373 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700374 .build());
375
376 int size = valueList.size();
377 for (int i = 0; i < size; i++) {
378 ContentValues values = valueList.get(i);
379 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
380 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
381 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
382 .withValues(values)
383 .build());
384 }
385
386 ContentResolver resolver = getContentResolver();
387 ContentProviderResult[] results;
388 try {
389 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
390 } catch (Exception e) {
391 throw new RuntimeException("Failed to store new contact", e);
392 }
393
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700394 Uri rawContactUri = results[0].uri;
395 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
396
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800397 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700398 }
399
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700400 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800401 * Creates an intent that can be sent to this service to create a new raw contact
402 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800403 * This variant is more convenient to use when there is only one photo that can
404 * possibly be updated, as in the Contact Details screen.
405 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
406 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800407 */
Maurice Chu851222a2012-06-21 11:43:08 -0700408 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700409 String saveModeExtraKey, int saveMode, boolean isProfile,
410 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700411 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800412 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700413 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800414 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700415 callbackActivity, callbackAction, bundle,
416 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800417 }
418
419 /**
420 * Creates an intent that can be sent to this service to create a new raw contact
421 * using data presented as a set of ContentValues.
422 * This variant is used when multiple contacts' photos may be updated, as in the
423 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700424 *
Josh Garguse692e012012-01-18 14:53:11 -0800425 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700426 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
427 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800428 */
Maurice Chu851222a2012-06-21 11:43:08 -0700429 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700430 String saveModeExtraKey, int saveMode, boolean isProfile,
431 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700432 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000433 Intent serviceIntent = new Intent(
434 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800435 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
436 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700437 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800438 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
439
Josh Garguse692e012012-01-18 14:53:11 -0800440 if (updatedPhotos != null) {
441 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
442 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800443
Josh Garguse5d3f892012-04-11 11:56:15 -0700444 if (callbackActivity != null) {
445 // Callback intent will be invoked by the service once the contact is
446 // saved. The service will put the URI of the new contact as "data" on
447 // the callback intent.
448 Intent callbackIntent = new Intent(context, callbackActivity);
449 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700450 if (joinContactIdExtraKey != null && joinContactId != null) {
451 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
452 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700453 callbackIntent.setAction(callbackAction);
454 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
455 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800456 return serviceIntent;
457 }
458
459 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700460 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700461 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800462 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800463
Jay Shrauner08099782015-03-25 14:17:11 -0700464 if (state == null) {
465 Log.e(TAG, "Invalid arguments for saveContact request");
466 return;
467 }
468
benny.lin3a4e7a22014-01-08 10:58:08 +0800469 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800470 // Trim any empty fields, and RawContacts, before persisting
471 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700472 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800473
474 Uri lookupUri = null;
475
476 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700477
Josh Garguse692e012012-01-18 14:53:11 -0800478 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800479
Josh Gargusef15c8e2012-01-30 16:42:02 -0800480 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
481 long insertedRawContactId = -1;
482
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800483 // Attempt to persist changes
484 int tries = 0;
485 while (tries++ < PERSIST_TRIES) {
486 try {
487 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800488 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
489
490 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
491
492 for (CPOWrapper cpoWrapper : diffWrapper) {
493 diff.add(cpoWrapper.getOperation());
494 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700495
Katherine Kuana007e442011-07-07 09:25:34 -0700496 if (DEBUG) {
497 Log.v(TAG, "Content Provider Operations:");
498 for (ContentProviderOperation operation : diff) {
499 Log.v(TAG, operation.toString());
500 }
501 }
502
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700503 int numberProcessed = 0;
504 boolean batchFailed = false;
505 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
506 while (numberProcessed < diff.size()) {
507 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
508 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700509 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700510 batchFailed = true;
511 break;
512 } else {
513 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700514 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800515 }
516
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700517 if (batchFailed) {
518 // Retry save
519 continue;
520 }
521
Wenyi Wang67addcc2015-11-23 10:07:48 -0800522 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800523 if (rawContactId == -1) {
524 throw new IllegalStateException("Could not determine RawContact ID after save");
525 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800526 // We don't have to check to see if the value is still -1. If we reach here,
527 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800528 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700529 if (isProfile) {
530 // Since the profile supports local raw contacts, which may have been completely
531 // removed if all information was removed, we need to do a special query to
532 // get the lookup URI for the profile contact (if it still exists).
533 Cursor c = resolver.query(Profile.CONTENT_URI,
534 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
535 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800536 if (c == null) {
537 continue;
538 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700539 try {
Erik162b7e32011-09-20 15:23:55 -0700540 if (c.moveToFirst()) {
541 final long contactId = c.getLong(0);
542 final String lookupKey = c.getString(1);
543 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
544 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700545 } finally {
546 c.close();
547 }
548 } else {
549 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
550 rawContactId);
551 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
552 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800553 if (lookupUri != null) {
554 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
555 }
Josh Garguse692e012012-01-18 14:53:11 -0800556
557 // We can change this back to false later, if we fail to save the contact photo.
558 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800559 break;
560
561 } catch (RemoteException e) {
562 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700563 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800564 break;
565
Jay Shrauner57fca182014-01-17 14:20:50 -0800566 } catch (IllegalArgumentException e) {
567 // This is thrown by applyBatch on malformed requests
Walter Jang3a0b4832016-10-12 11:02:54 -0700568 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Jay Shrauner57fca182014-01-17 14:20:50 -0800569 showToast(R.string.contactSavedErrorToast);
570 break;
571
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800572 } catch (OperationApplicationException e) {
573 // Version consistency failed, re-parent change and try again
574 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
575 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
576 boolean first = true;
577 final int count = state.size();
578 for (int i = 0; i < count; i++) {
579 Long rawContactId = state.getRawContactId(i);
580 if (rawContactId != null && rawContactId != -1) {
581 if (!first) {
582 sb.append(',');
583 }
584 sb.append(rawContactId);
585 first = false;
586 }
587 }
588 sb.append(")");
589
590 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800591 throw new IllegalStateException(
592 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800593 }
594
Maurice Chu851222a2012-06-21 11:43:08 -0700595 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700596 isProfile
597 ? RawContactsEntity.PROFILE_CONTENT_URI
598 : RawContactsEntity.CONTENT_URI,
599 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700600 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700601
602 // Update the new state to use profile URIs if appropriate.
603 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700604 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700605 delta.setProfileQueryUri();
606 }
607 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800608 }
609 }
610
Josh Garguse692e012012-01-18 14:53:11 -0800611 // Now save any updated photos. We do this at the end to ensure that
612 // the ContactProvider already knows about newly-created contacts.
613 if (updatedPhotos != null) {
614 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700615 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800616 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800617
618 // If the raw-contact ID is negative, we are saving a new raw-contact;
619 // replace the bogus ID with the new one that we actually saved the contact at.
620 if (rawContactId < 0) {
621 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800622 }
623
Jay Shrauner511561d2015-04-02 10:35:33 -0700624 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700625 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700626 succeeded = false;
627 }
Josh Garguse692e012012-01-18 14:53:11 -0800628 }
629 }
630
Josh Garguse5d3f892012-04-11 11:56:15 -0700631 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
632 if (callbackIntent != null) {
633 if (succeeded) {
634 // Mark the intent to indicate that the save was successful (even if the lookup URI
635 // is now null). For local contacts or the local profile, it's possible that the
636 // save triggered removal of the contact, so no lookup URI would exist..
637 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
638 }
639 callbackIntent.setData(lookupUri);
640 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800641 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800642 }
643
Josh Garguse692e012012-01-18 14:53:11 -0800644 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700645 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
646 * subsets, adds the returned array to "results".
647 *
648 * @return the size of the array, if not null; -1 when the array is null.
649 */
650 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
651 ContentProviderResult[] results, ContentResolver resolver)
652 throws RemoteException, OperationApplicationException {
653 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
654 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
655 subset.addAll(diff.subList(offset, offset + subsetCount));
656 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
657 .AUTHORITY, subset);
658 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
659 return -1;
660 }
661 for (ContentProviderResult c : subsetResult) {
662 results[offset++] = c;
663 }
664 return subsetResult.length;
665 }
666
667 /**
Josh Garguse692e012012-01-18 14:53:11 -0800668 * Save updated photo for the specified raw-contact.
669 * @return true for success, false for failure
670 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800671 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800672 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800673 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
674 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
675
benny.lin3a4e7a22014-01-08 10:58:08 +0800676 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800677 }
678
Josh Gargusef15c8e2012-01-30 16:42:02 -0800679 /**
680 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
681 */
Maurice Chu851222a2012-06-21 11:43:08 -0700682 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800683 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800684 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800685 long existingRawContactId = state.findRawContactId();
686 if (existingRawContactId != -1) {
687 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800688 }
689
Wenyi Wang67addcc2015-11-23 10:07:48 -0800690 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800691 }
692
693 /**
694 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
695 */
696 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800697 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800698 if (results == null) {
699 return -1;
700 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800701 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800702 final int numResults = results.length;
703 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800704 final CPOWrapper cpoWrapper = diffWrapper.get(i);
705 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
706 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
707 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800708 return ContentUris.parseId(results[i].uri);
709 }
710 }
711 return -1;
712 }
713
714 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700715 * Creates an intent that can be sent to this service to create a new group as
716 * well as add new members at the same time.
717 *
718 * @param context of the application
719 * @param account in which the group should be created
720 * @param label is the name of the group (cannot be null)
721 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
722 * should be added to the group
723 * @param callbackActivity is the activity to send the callback intent to
724 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700725 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700726 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700727 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700728 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800729 Intent serviceIntent = new Intent(context, ContactSaveService.class);
730 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
731 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
732 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700733 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800734 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700735 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700736
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800737 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700738 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800739 Intent callbackIntent = new Intent(context, callbackActivity);
740 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700741 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800742
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700743 return serviceIntent;
744 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800745
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800746 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700747 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
748 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
749 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
750 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700751 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800752
Katherine Kuan717e3432011-07-13 17:03:24 -0700753 // Create the new group
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700754 final Uri groupUri = mGroupsDao.create(label,
755 new AccountWithDataSet(accountName, accountType, dataSet));
756 final ContentResolver resolver = getContentResolver();
Katherine Kuan717e3432011-07-13 17:03:24 -0700757
758 // If there's no URI, then the insertion failed. Abort early because group members can't be
759 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800760 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700761 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800762 return;
763 }
764
Katherine Kuan717e3432011-07-13 17:03:24 -0700765 // Add new group members
766 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
767
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700768 ContentValues values = new ContentValues();
Katherine Kuan717e3432011-07-13 17:03:24 -0700769 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
Walter Jang8bac28b2016-08-30 10:34:55 -0700770 // with the way other intent extras that are passed to the
Gary Mai363af602016-09-28 10:01:23 -0700771 // {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800772 values.clear();
773 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
774 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
775
776 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700777 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700778 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800779 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800780 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800781 }
782
783 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800784 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800785 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700786 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700787 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800788 Intent serviceIntent = new Intent(context, ContactSaveService.class);
789 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
790 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
791 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700792
793 // Callback intent will be invoked by the service once the group is renamed.
794 Intent callbackIntent = new Intent(context, callbackActivity);
795 callbackIntent.setAction(callbackAction);
796 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
797
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800798 return serviceIntent;
799 }
800
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800801 private void renameGroup(Intent intent) {
802 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
803 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
804
805 if (groupId == -1) {
806 Log.e(TAG, "Invalid arguments for renameGroup request");
807 return;
808 }
809
810 ContentValues values = new ContentValues();
811 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700812 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
813 getContentResolver().update(groupUri, values, null, null);
814
815 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
816 callbackIntent.setData(groupUri);
817 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800818 }
819
820 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800821 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800822 */
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700823 public static Intent createGroupDeletionIntent(Context context, long groupId) {
824 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800825 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800826 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700827
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800828 return serviceIntent;
829 }
830
831 private void deleteGroup(Intent intent) {
832 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
833 if (groupId == -1) {
834 Log.e(TAG, "Invalid arguments for deleteGroup request");
835 return;
836 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700837 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800838
Marcus Hagerott819214d2016-09-29 14:58:27 -0700839 final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700840 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
841 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
842 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
Walter Jang72f99882016-05-26 09:01:31 -0700843
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700844 mGroupsDao.delete(groupUri);
845
846 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
847 }
848
849 public static Intent createUndoIntent(Context context, Intent resultIntent) {
850 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
851 serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
852 serviceIntent.putExtras(resultIntent);
853 return serviceIntent;
854 }
855
856 private void undo(Intent intent) {
857 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
858 if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
859 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
Walter Jang72f99882016-05-26 09:01:31 -0700860 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800861 }
862
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700863
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800864 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700865 * Creates an intent that can be sent to this service to rename a group as
866 * well as add and remove members from the group.
867 *
868 * @param context of the application
869 * @param groupId of the group that should be modified
870 * @param newLabel is the updated name of the group (can be null if the name
871 * should not be updated)
872 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
873 * should be added to the group
874 * @param rawContactsToRemove is an array of raw contact IDs for contacts
875 * that should be removed from the group
876 * @param callbackActivity is the activity to send the callback intent to
877 * @param callbackAction is the intent action for the callback intent
878 */
879 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
880 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700881 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700882 Intent serviceIntent = new Intent(context, ContactSaveService.class);
883 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
884 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
885 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
886 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
887 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
888 rawContactsToRemove);
889
890 // Callback intent will be invoked by the service once the group is updated
891 Intent callbackIntent = new Intent(context, callbackActivity);
892 callbackIntent.setAction(callbackAction);
893 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
894
895 return serviceIntent;
896 }
897
898 private void updateGroup(Intent intent) {
899 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
900 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
901 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
902 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
903
904 if (groupId == -1) {
905 Log.e(TAG, "Invalid arguments for updateGroup request");
906 return;
907 }
908
909 final ContentResolver resolver = getContentResolver();
910 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
911
912 // Update group name if necessary
913 if (label != null) {
914 ContentValues values = new ContentValues();
915 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700916 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700917 }
918
Katherine Kuan717e3432011-07-13 17:03:24 -0700919 // Add and remove members if necessary
920 addMembersToGroup(resolver, rawContactsToAdd, groupId);
921 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
922
923 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
924 callbackIntent.setData(groupUri);
925 deliverCallback(callbackIntent);
926 }
927
Walter Jang3a0b4832016-10-12 11:02:54 -0700928 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700929 long groupId) {
930 if (rawContactsToAdd == null) {
931 return;
932 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700933 for (long rawContactId : rawContactsToAdd) {
934 try {
935 final ArrayList<ContentProviderOperation> rawContactOperations =
936 new ArrayList<ContentProviderOperation>();
937
938 // Build an assert operation to ensure the contact is not already in the group
939 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
940 .newAssertQuery(Data.CONTENT_URI);
941 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
942 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
943 new String[] { String.valueOf(rawContactId),
944 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
945 assertBuilder.withExpectedCount(0);
946 rawContactOperations.add(assertBuilder.build());
947
948 // Build an insert operation to add the contact to the group
949 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
950 .newInsert(Data.CONTENT_URI);
951 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
952 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
953 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
954 rawContactOperations.add(insertBuilder.build());
955
956 if (DEBUG) {
957 for (ContentProviderOperation operation : rawContactOperations) {
958 Log.v(TAG, operation.toString());
959 }
960 }
961
962 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700963 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800964 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700965 }
966 } catch (RemoteException e) {
967 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700968 FeedbackHelper.sendFeedback(this, TAG,
969 "Problem persisting user edits for raw contact ID " +
970 String.valueOf(rawContactId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700971 } catch (OperationApplicationException e) {
972 // The assert could have failed because the contact is already in the group,
973 // just continue to the next contact
Walter Jang3a0b4832016-10-12 11:02:54 -0700974 FeedbackHelper.sendFeedback(this, TAG,
975 "Assert failed in adding raw contact ID " +
976 String.valueOf(rawContactId) + ". Already exists in group " +
977 String.valueOf(groupId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700978 }
979 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700980 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700981
Daniel Lehmann18958a22012-02-28 17:45:25 -0800982 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700983 long groupId) {
984 if (rawContactsToRemove == null) {
985 return;
986 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700987 for (long rawContactId : rawContactsToRemove) {
988 // Apply the delete operation on the data row for the given raw contact's
989 // membership in the given group. If no contact matches the provided selection, then
990 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800991 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700992 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
993 new String[] { String.valueOf(rawContactId),
994 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
995 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700996 }
997
998 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800999 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001000 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001001 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
1002 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1003 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
1004 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1005 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
1006
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001007 return serviceIntent;
1008 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001009
1010 private void setStarred(Intent intent) {
1011 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1012 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
1013 if (contactUri == null) {
1014 Log.e(TAG, "Invalid arguments for setStarred request");
1015 return;
1016 }
1017
1018 final ContentValues values = new ContentValues(1);
1019 values.put(Contacts.STARRED, value);
1020 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -07001021
1022 // Undemote the contact if necessary
1023 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
1024 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -08001025 if (c == null) {
1026 return;
1027 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001028 try {
1029 if (c.moveToFirst()) {
1030 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -07001031
1032 // Don't bother undemoting if this contact is the user's profile.
1033 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -08001034 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -07001035 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001036 }
1037 } finally {
1038 c.close();
1039 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001040 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001041
1042 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -07001043 * Creates an intent that can be sent to this service to set the redirect to voicemail.
1044 */
1045 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
1046 boolean value) {
1047 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1048 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1049 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1050 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1051
1052 return serviceIntent;
1053 }
1054
1055 private void setSendToVoicemail(Intent intent) {
1056 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1057 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1058 if (contactUri == null) {
1059 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1060 return;
1061 }
1062
1063 final ContentValues values = new ContentValues(1);
1064 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1065 getContentResolver().update(contactUri, values, null, null);
1066 }
1067
1068 /**
1069 * Creates an intent that can be sent to this service to save the contact's ringtone.
1070 */
1071 public static Intent createSetRingtone(Context context, Uri contactUri,
1072 String value) {
1073 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1074 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1075 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1076 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1077
1078 return serviceIntent;
1079 }
1080
1081 private void setRingtone(Intent intent) {
1082 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1083 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1084 if (contactUri == null) {
1085 Log.e(TAG, "Invalid arguments for setRingtone");
1086 return;
1087 }
1088 ContentValues values = new ContentValues(1);
1089 values.put(Contacts.CUSTOM_RINGTONE, value);
1090 getContentResolver().update(contactUri, values, null, null);
1091 }
1092
1093 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001094 * Creates an intent that sets the selected data item as super primary (default)
1095 */
1096 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1097 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1098 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1099 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1100 return serviceIntent;
1101 }
1102
1103 private void setSuperPrimary(Intent intent) {
1104 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1105 if (dataId == -1) {
1106 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1107 return;
1108 }
1109
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001110 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001111 }
1112
1113 /**
1114 * Creates an intent that clears the primary flag of all data items that belong to the same
1115 * raw_contact as the given data item. Will only clear, if the data item was primary before
1116 * this call
1117 */
1118 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1119 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1120 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1121 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1122 return serviceIntent;
1123 }
1124
1125 private void clearPrimary(Intent intent) {
1126 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1127 if (dataId == -1) {
1128 Log.e(TAG, "Invalid arguments for clearPrimary request");
1129 return;
1130 }
1131
1132 // Update the primary values in the data record.
1133 ContentValues values = new ContentValues(1);
1134 values.put(Data.IS_SUPER_PRIMARY, 0);
1135 values.put(Data.IS_PRIMARY, 0);
1136
1137 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1138 values, null, null);
1139 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001140
1141 /**
1142 * Creates an intent that can be sent to this service to delete a contact.
1143 */
1144 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1145 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1146 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1147 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1148 return serviceIntent;
1149 }
1150
Brian Attwelld2962a32015-03-02 14:48:50 -08001151 /**
1152 * Creates an intent that can be sent to this service to delete multiple contacts.
1153 */
1154 public static Intent createDeleteMultipleContactsIntent(Context context,
James Laskeye5a140a2016-10-18 15:43:42 -07001155 long[] contactIds, final String[] names) {
Brian Attwelld2962a32015-03-02 14:48:50 -08001156 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1157 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1158 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
James Laskeye5a140a2016-10-18 15:43:42 -07001159 serviceIntent.putExtra(ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY, names);
Brian Attwelld2962a32015-03-02 14:48:50 -08001160 return serviceIntent;
1161 }
1162
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001163 private void deleteContact(Intent intent) {
1164 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1165 if (contactUri == null) {
1166 Log.e(TAG, "Invalid arguments for deleteContact request");
1167 return;
1168 }
1169
1170 getContentResolver().delete(contactUri, null, null);
1171 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001172
Brian Attwelld2962a32015-03-02 14:48:50 -08001173 private void deleteMultipleContacts(Intent intent) {
1174 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1175 if (contactIds == null) {
1176 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1177 return;
1178 }
1179 for (long contactId : contactIds) {
1180 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1181 getContentResolver().delete(contactUri, null, null);
1182 }
James Laskeye5a140a2016-10-18 15:43:42 -07001183 final String[] names = intent.getStringArrayExtra(
1184 ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY);
1185 final String deleteToastMessage;
1186 if (names.length == 0) {
1187 deleteToastMessage = getResources().getQuantityString(
1188 R.plurals.contacts_deleted_toast, contactIds.length);
1189 } else if (names.length == 1) {
1190 deleteToastMessage = getResources().getString(
1191 R.string.contacts_deleted_one_named_toast, names);
1192 } else if (names.length == 2) {
1193 deleteToastMessage = getResources().getString(
1194 R.string.contacts_deleted_two_named_toast, names);
1195 } else {
1196 deleteToastMessage = getResources().getString(
1197 R.string.contacts_deleted_many_named_toast, names);
1198 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001199 mMainHandler.post(new Runnable() {
1200 @Override
1201 public void run() {
1202 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1203 .show();
1204 }
1205 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001206 }
1207
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001208 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001209 * 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 -07001210 * pieces. This will set the raw contact ids to TYPE_AUTOMATIC for AggregationExceptions so
1211 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001212 */
1213 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1214 ResultReceiver receiver) {
1215 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1216 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1217 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1218 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1219 return serviceIntent;
1220 }
1221
1222 private void splitContact(Intent intent) {
1223 final long rawContactIds[][] = (long[][]) intent
1224 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001225 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Mai7efa9942016-05-12 11:26:49 -07001226 if (rawContactIds == null) {
1227 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001228 if (receiver != null) {
1229 receiver.send(BAD_ARGUMENTS, new Bundle());
1230 }
Gary Mai7efa9942016-05-12 11:26:49 -07001231 return;
1232 }
1233 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1234 final ContentResolver resolver = getContentResolver();
1235 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001236 for (int i = 0; i < rawContactIds.length; i++) {
1237 for (int j = 0; j < rawContactIds.length; j++) {
1238 if (i != j) {
1239 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
1240 if (receiver != null) {
1241 receiver.send(CP2_ERROR, new Bundle());
1242 return;
1243 }
1244 }
1245 }
1246 }
1247 }
1248 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1249 if (receiver != null) {
1250 receiver.send(CP2_ERROR, new Bundle());
1251 }
1252 return;
1253 }
1254 if (receiver != null) {
1255 receiver.send(CONTACTS_SPLIT, new Bundle());
1256 } else {
1257 showToast(R.string.contactUnlinkedToast);
1258 }
1259 }
1260
1261 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001262 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001263 * and {@param rawContactIds2} to {@param operations}.
1264 * @return false if an error occurred, true otherwise.
1265 */
1266 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
1267 long[] rawContactIds1, long[] rawContactIds2) {
1268 if (rawContactIds1 == null || rawContactIds2 == null) {
1269 Log.e(TAG, "Invalid arguments for splitContact request");
1270 return false;
1271 }
1272 // For each pair of raw contacts, insert an aggregation exception
1273 final ContentResolver resolver = getContentResolver();
1274 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1275 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1276 for (int i = 0; i < rawContactIds1.length; i++) {
1277 for (int j = 0; j < rawContactIds2.length; j++) {
1278 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
1279 // Before we get to 500 we need to flush the operations list
1280 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1281 if (!applyOperations(resolver, operations)) {
1282 return false;
1283 }
1284 operations.clear();
1285 }
1286 }
1287 }
1288 return true;
1289 }
1290
1291 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001292 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001293 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001294 */
1295 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001296 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001297 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1298 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1299 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1300 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001301
1302 // Callback intent will be invoked by the service once the contacts are joined.
1303 Intent callbackIntent = new Intent(context, callbackActivity);
1304 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001305 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1306
1307 return serviceIntent;
1308 }
1309
Brian Attwelld3946ca2015-03-03 11:13:49 -08001310 /**
1311 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1312 * No special attention is paid to where the resulting contact's name is taken from.
1313 */
Gary Mai7efa9942016-05-12 11:26:49 -07001314 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1315 ResultReceiver receiver) {
1316 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001317 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1318 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001319 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001320 return serviceIntent;
1321 }
1322
Gary Mai7efa9942016-05-12 11:26:49 -07001323 /**
1324 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1325 * No special attention is paid to where the resulting contact's name is taken from.
1326 */
1327 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1328 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1329 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001330
1331 private interface JoinContactQuery {
1332 String[] PROJECTION = {
1333 RawContacts._ID,
1334 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001335 RawContacts.DISPLAY_NAME_SOURCE,
1336 };
1337
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001338 int _ID = 0;
1339 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001340 int DISPLAY_NAME_SOURCE = 2;
1341 }
1342
1343 private interface ContactEntityQuery {
1344 String[] PROJECTION = {
1345 Contacts.Entity.DATA_ID,
1346 Contacts.Entity.CONTACT_ID,
1347 Contacts.Entity.IS_SUPER_PRIMARY,
1348 };
1349 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1350 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1351 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1352 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1353
1354 int DATA_ID = 0;
1355 int CONTACT_ID = 1;
1356 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001357 }
1358
Brian Attwelld3946ca2015-03-03 11:13:49 -08001359 private void joinSeveralContacts(Intent intent) {
1360 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001361
Gary Mai7efa9942016-05-12 11:26:49 -07001362 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001363
Brian Attwelld3946ca2015-03-03 11:13:49 -08001364 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001365 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1366 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001367 if (rawContactIds == null) {
1368 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001369 if (receiver != null) {
1370 receiver.send(BAD_ARGUMENTS, new Bundle());
1371 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001372 return;
1373 }
1374
Brian Attwelld3946ca2015-03-03 11:13:49 -08001375 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001376 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001377 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1378 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1379 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001380 for (int i = 0; i < rawContactIds.length; i++) {
1381 for (int j = 0; j < rawContactIds.length; j++) {
1382 if (i != j) {
1383 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1384 }
Walter Jang0653de32015-07-24 12:12:40 -07001385 // Before we get to 500 we need to flush the operations list
1386 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001387 if (!applyOperations(resolver, operations)) {
1388 if (receiver != null) {
1389 receiver.send(CP2_ERROR, new Bundle());
1390 }
Walter Jang0653de32015-07-24 12:12:40 -07001391 return;
1392 }
1393 operations.clear();
1394 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001395 }
1396 }
Gary Mai7efa9942016-05-12 11:26:49 -07001397 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1398 if (receiver != null) {
1399 receiver.send(CP2_ERROR, new Bundle());
1400 }
Walter Jang0653de32015-07-24 12:12:40 -07001401 return;
1402 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001403
John Shaoa3c507a2016-09-13 14:26:17 -07001404
1405 final String name = queryNameOfLinkedContacts(contactIds);
1406 if (name != null) {
1407 if (receiver != null) {
1408 final Bundle result = new Bundle();
1409 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1410 result.putString(EXTRA_DISPLAY_NAME, name);
1411 receiver.send(CONTACTS_LINKED, result);
1412 } else {
James Laskeyf62b4882016-10-21 11:36:40 -07001413 if (TextUtils.isEmpty(name)) {
1414 showToast(R.string.contactsJoinedMessage);
1415 } else {
1416 showToast(R.string.contactsJoinedNamedMessage, name);
1417 }
John Shaoa3c507a2016-09-13 14:26:17 -07001418 }
Gary Mai7efa9942016-05-12 11:26:49 -07001419 } else {
John Shaoa3c507a2016-09-13 14:26:17 -07001420 if (receiver != null) {
1421 receiver.send(CP2_ERROR, new Bundle());
1422 }
1423 showToast(R.string.contactJoinErrorToast);
Gary Mai7efa9942016-05-12 11:26:49 -07001424 }
Walter Jang0653de32015-07-24 12:12:40 -07001425 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001426
John Shaoa3c507a2016-09-13 14:26:17 -07001427 /** Get the display name of the top-level contact after the contacts have been linked. */
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001428 private String queryNameOfLinkedContacts(long[] contactIds) {
1429 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1430 final String[] whereArgs = new String[contactIds.length];
1431 for (int i = 0; i < contactIds.length; i++) {
1432 whereArgs[i] = String.valueOf(contactIds[i]);
1433 whereBuilder.append("?,");
1434 }
1435 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1436 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
James Laskeyf62b4882016-10-21 11:36:40 -07001437 new String[]{Contacts._ID, Contacts.DISPLAY_NAME,
1438 Contacts.DISPLAY_NAME_ALTERNATIVE},
John Shaoa3c507a2016-09-13 14:26:17 -07001439 whereBuilder.toString(), whereArgs, null);
1440
1441 String name = null;
James Laskeyf62b4882016-10-21 11:36:40 -07001442 String nameAlt = null;
John Shaoa3c507a2016-09-13 14:26:17 -07001443 long contactId = 0;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001444 try {
1445 if (cursor.moveToFirst()) {
John Shaoa3c507a2016-09-13 14:26:17 -07001446 contactId = cursor.getLong(0);
1447 name = cursor.getString(1);
James Laskeyf62b4882016-10-21 11:36:40 -07001448 nameAlt = cursor.getString(2);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001449 }
John Shaoa3c507a2016-09-13 14:26:17 -07001450 while(cursor.moveToNext()) {
1451 if (cursor.getLong(0) != contactId) {
1452 return null;
1453 }
1454 }
James Laskeyf62b4882016-10-21 11:36:40 -07001455
1456 final String formattedName = ContactDisplayUtils.getPreferredDisplayName(name, nameAlt,
1457 new ContactsPreferences(getApplicationContext()));
1458 return formattedName == null ? "" : formattedName;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001459 } finally {
John Shaoa3c507a2016-09-13 14:26:17 -07001460 if (cursor != null) {
1461 cursor.close();
1462 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001463 }
1464 }
1465
Walter Jang0653de32015-07-24 12:12:40 -07001466 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001467 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001468 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001469 try {
John Shaoa3c507a2016-09-13 14:26:17 -07001470 final ContentProviderResult[] result =
1471 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1472 for (int i = 0; i < result.length; ++i) {
1473 // if no rows were modified in the operation then we count it as fail.
1474 if (result[i].count < 0) {
1475 throw new OperationApplicationException();
1476 }
1477 }
Walter Jang0653de32015-07-24 12:12:40 -07001478 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001479 } catch (RemoteException | OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001480 FeedbackHelper.sendFeedback(this, TAG,
1481 "Failed to apply aggregation exception batch", e);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001482 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001483 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001484 }
1485 }
1486
Brian Attwelld3946ca2015-03-03 11:13:49 -08001487 private void joinContacts(Intent intent) {
1488 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1489 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001490
1491 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001492 // in the join UIs.
1493 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1494 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001495 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001496 return;
1497 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001498
Brian Attwell548f5c62015-01-27 17:46:46 -08001499 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001500
1501 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001502 for (int i = 0; i < rawContactIds.length; i++) {
1503 for (int j = 0; j < rawContactIds.length; j++) {
1504 if (i != j) {
1505 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1506 }
1507 }
1508 }
1509
Brian Attwelld3946ca2015-03-03 11:13:49 -08001510 final ContentResolver resolver = getContentResolver();
1511
Brian Attwell548f5c62015-01-27 17:46:46 -08001512 // Use the name for contactId1 as the name for the newly aggregated contact.
1513 final Uri contactId1Uri = ContentUris.withAppendedId(
1514 Contacts.CONTENT_URI, contactId1);
1515 final Uri entityUri = Uri.withAppendedPath(
1516 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1517 Cursor c = resolver.query(entityUri,
1518 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1519 if (c == null) {
1520 Log.e(TAG, "Unable to open Contacts DB cursor");
1521 showToast(R.string.contactSavedErrorToast);
1522 return;
1523 }
1524 long dataIdToAddSuperPrimary = -1;
1525 try {
1526 if (c.moveToFirst()) {
1527 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1528 }
1529 } finally {
1530 c.close();
1531 }
1532
1533 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1534 // display name does not change as a result of the join.
1535 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001536 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001537 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1538 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1539 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001540 operations.add(builder.build());
1541 }
1542
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001543 // Apply all aggregation exceptions as one batch
John Shaoa3c507a2016-09-13 14:26:17 -07001544 final boolean success = applyOperations(resolver, operations);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001545
John Shaoa3c507a2016-09-13 14:26:17 -07001546 final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2});
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001547 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
John Shaoa3c507a2016-09-13 14:26:17 -07001548 if (success && name != null) {
James Laskeyf62b4882016-10-21 11:36:40 -07001549 if (TextUtils.isEmpty(name)) {
1550 showToast(R.string.contactsJoinedMessage);
1551 } else {
1552 showToast(R.string.contactsJoinedNamedMessage, name);
1553 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001554 Uri uri = RawContacts.getContactLookupUri(resolver,
1555 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1556 callbackIntent.setData(uri);
1557 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001558 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001559 }
1560
Gary Mai7efa9942016-05-12 11:26:49 -07001561 /**
1562 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1563 * array of the return value holds an array of raw contact ids for one contactId.
1564 * @param contactIds
1565 * @return
1566 */
1567 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1568 final long[][] rawContactIds = new long[contactIds.length][];
1569 for (int i = 0; i < contactIds.length; i++) {
1570 rawContactIds[i] = getRawContactIds(contactIds[i]);
1571 }
1572 return rawContactIds;
1573 }
1574
1575 /**
1576 * Gets the raw contact ids associated with {@param contactId}.
1577 * @param contactId
1578 * @return Array of raw contact ids.
1579 */
1580 private long[] getRawContactIds(long contactId) {
1581 final ContentResolver resolver = getContentResolver();
1582 long rawContactIds[];
1583
1584 final StringBuilder queryBuilder = new StringBuilder();
1585 queryBuilder.append(RawContacts.CONTACT_ID)
1586 .append("=")
1587 .append(String.valueOf(contactId));
1588
1589 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1590 JoinContactQuery.PROJECTION,
1591 queryBuilder.toString(),
1592 null, null);
1593 if (c == null) {
1594 Log.e(TAG, "Unable to open Contacts DB cursor");
1595 return null;
1596 }
1597 try {
1598 rawContactIds = new long[c.getCount()];
1599 for (int i = 0; i < rawContactIds.length; i++) {
1600 c.moveToPosition(i);
1601 final long rawContactId = c.getLong(JoinContactQuery._ID);
1602 rawContactIds[i] = rawContactId;
1603 }
1604 } finally {
1605 c.close();
1606 }
1607 return rawContactIds;
1608 }
1609
Brian Attwelld3946ca2015-03-03 11:13:49 -08001610 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1611 if (contactIds == null) {
1612 return null;
1613 }
1614
Brian Attwell548f5c62015-01-27 17:46:46 -08001615 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001616
1617 final StringBuilder queryBuilder = new StringBuilder();
1618 final String stringContactIds[] = new String[contactIds.length];
1619 for (int i = 0; i < contactIds.length; i++) {
1620 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1621 stringContactIds[i] = String.valueOf(contactIds[i]);
1622 if (contactIds[i] == -1) {
1623 return null;
1624 }
1625 if (i == contactIds.length -1) {
1626 break;
1627 }
1628 queryBuilder.append(" OR ");
1629 }
1630
Brian Attwell548f5c62015-01-27 17:46:46 -08001631 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1632 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001633 queryBuilder.toString(),
1634 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001635 if (c == null) {
1636 Log.e(TAG, "Unable to open Contacts DB cursor");
1637 showToast(R.string.contactSavedErrorToast);
1638 return null;
1639 }
Gary Mai7efa9942016-05-12 11:26:49 -07001640 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001641 try {
1642 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001643 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001644 return null;
1645 }
1646 rawContactIds = new long[c.getCount()];
1647 for (int i = 0; i < rawContactIds.length; i++) {
1648 c.moveToPosition(i);
1649 long rawContactId = c.getLong(JoinContactQuery._ID);
1650 rawContactIds[i] = rawContactId;
1651 }
1652 } finally {
1653 c.close();
1654 }
1655 return rawContactIds;
1656 }
1657
Brian Attwelld3946ca2015-03-03 11:13:49 -08001658 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1659 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1660 }
1661
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001662 /**
1663 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1664 */
1665 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1666 long rawContactId1, long rawContactId2) {
1667 Builder builder =
1668 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1669 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1670 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1671 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1672 operations.add(builder.build());
1673 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001674
1675 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001676 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} ContentProviderOperation.
Gary Mai7efa9942016-05-12 11:26:49 -07001677 */
1678 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
1679 long rawContactId1, long rawContactId2) {
1680 final Builder builder =
1681 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Mai53fe0d22016-07-26 17:23:53 -07001682 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001683 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1684 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1685 operations.add(builder.build());
1686 }
1687
Marcus Hagerott819214d2016-09-29 14:58:27 -07001688 public static Intent createImportFromSimIntent(Context context,
1689 ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
1690 return new Intent(context, ContactSaveService.class)
1691 .setAction(ACTION_IMPORT_FROM_SIM)
1692 .putExtra(EXTRA_SIM_CONTACTS, contacts)
1693 .putExtra(EXTRA_ACCOUNT, targetAccount);
1694 }
1695
1696 private void importFromSim(Intent intent) {
1697 final Intent result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
1698 .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, System.currentTimeMillis());
1699 try {
1700 final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
1701 final ArrayList<SimContact> contacts =
1702 intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
1703 mSimContactDao.importContacts(contacts, targetAccount);
1704 // notify success
1705 LocalBroadcastManager.getInstance(this).sendBroadcast(result
1706 .putExtra(EXTRA_RESULT_COUNT, contacts.size())
1707 .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS));
1708 if (Log.isLoggable(TAG, Log.DEBUG)) {
1709 Log.d(TAG, "importFromSim completed successfully");
1710 }
1711 } catch (RemoteException|OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001712 FeedbackHelper.sendFeedback(this, TAG, "Failed to import contacts from SIM card", e);
Marcus Hagerott819214d2016-09-29 14:58:27 -07001713 LocalBroadcastManager.getInstance(this).sendBroadcast(result
1714 .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE));
1715 }
1716 }
1717
Gary Mai7efa9942016-05-12 11:26:49 -07001718 /**
James Laskeyf62b4882016-10-21 11:36:40 -07001719 * Shows a toast on the UI thread by formatting messageId using args.
1720 * @param messageId id of message string
1721 * @param args args to format string
1722 */
1723 private void showToast(final int messageId, final Object... args) {
1724 final String message = getResources().getString(messageId, args);
1725 mMainHandler.post(new Runnable() {
1726 @Override
1727 public void run() {
1728 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1729 }
1730 });
1731 }
1732
1733
1734 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001735 * Shows a toast on the UI thread.
1736 */
1737 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001738 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001739
1740 @Override
1741 public void run() {
1742 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1743 }
1744 });
1745 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001746
1747 private void deliverCallback(final Intent callbackIntent) {
1748 mMainHandler.post(new Runnable() {
1749
1750 @Override
1751 public void run() {
1752 deliverCallbackOnUiThread(callbackIntent);
1753 }
1754 });
1755 }
1756
1757 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1758 // TODO: this assumes that if there are multiple instances of the same
1759 // activity registered, the last one registered is the one waiting for
1760 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001761 for (Listener listener : sListeners) {
1762 if (callbackIntent.getComponent().equals(
1763 ((Activity) listener).getIntent().getComponent())) {
1764 listener.onServiceCompleted(callbackIntent);
1765 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001766 }
1767 }
1768 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001769
1770 public interface GroupsDao {
1771 Uri create(String title, AccountWithDataSet account);
1772 int delete(Uri groupUri);
1773 Bundle captureDeletionUndoData(Uri groupUri);
1774 Uri undoDeletion(Bundle undoData);
1775 }
1776
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001777 public static class GroupsDaoImpl implements GroupsDao {
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001778 public static final String KEY_GROUP_DATA = "groupData";
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001779 public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
1780
1781 private static final String TAG = "GroupsDao";
1782 private final Context context;
1783 private final ContentResolver contentResolver;
1784
1785 public GroupsDaoImpl(Context context) {
1786 this(context, context.getContentResolver());
1787 }
1788
1789 public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
1790 this.context = context;
1791 this.contentResolver = contentResolver;
1792 }
1793
1794 public Bundle captureDeletionUndoData(Uri groupUri) {
1795 final long groupId = ContentUris.parseId(groupUri);
1796 final Bundle result = new Bundle();
1797
1798 final Cursor cursor = contentResolver.query(groupUri,
1799 new String[]{
1800 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
1801 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
1802 Groups.SHOULD_SYNC
1803 },
1804 Groups.DELETED + "=?", new String[] { "0" }, null);
1805 try {
1806 if (cursor.moveToFirst()) {
1807 final ContentValues groupValues = new ContentValues();
1808 DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
1809 result.putParcelable(KEY_GROUP_DATA, groupValues);
1810 } else {
1811 // Group doesn't exist.
1812 return result;
1813 }
1814 } finally {
1815 cursor.close();
1816 }
1817
1818 final Cursor membersCursor = contentResolver.query(
1819 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
1820 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1821 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
1822 final long[] memberIds = new long[membersCursor.getCount()];
1823 int i = 0;
1824 while (membersCursor.moveToNext()) {
1825 memberIds[i++] = membersCursor.getLong(0);
1826 }
1827 result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
1828 return result;
1829 }
1830
1831 public Uri undoDeletion(Bundle deletedGroupData) {
1832 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
1833 if (groupData == null) {
1834 return null;
1835 }
1836 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
1837 final long groupId = ContentUris.parseId(groupUri);
1838
1839 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
1840 if (memberIds == null) {
1841 return groupUri;
1842 }
1843 final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
1844 for (int i = 0; i < memberIds.length; i++) {
1845 memberInsertions[i] = new ContentValues();
1846 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
1847 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1848 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
1849 }
1850 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
1851 if (inserted != memberIds.length) {
1852 Log.e(TAG, "Could not recover some members for group deletion undo");
1853 }
1854
1855 return groupUri;
1856 }
1857
1858 public Uri create(String title, AccountWithDataSet account) {
1859 final ContentValues values = new ContentValues();
1860 values.put(Groups.TITLE, title);
1861 values.put(Groups.ACCOUNT_NAME, account.name);
1862 values.put(Groups.ACCOUNT_TYPE, account.type);
1863 values.put(Groups.DATA_SET, account.dataSet);
1864 return contentResolver.insert(Groups.CONTENT_URI, values);
1865 }
1866
1867 public int delete(Uri groupUri) {
1868 return contentResolver.delete(groupUri, null, null);
1869 }
1870 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001871}