blob: 1dcc8380ae7cd4b64b6507a1648bd052e118da56 [file] [log] [blame]
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080017package com.android.contacts;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070018
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080019import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070020import android.app.IntentService;
21import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080022import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070023import android.content.ContentProviderResult;
24import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080025import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070026import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080027import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070028import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080029import android.content.OperationApplicationException;
30import android.database.Cursor;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070031import android.database.DatabaseUtils;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070032import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080033import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080034import android.os.Handler;
35import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080036import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080037import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070038import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080039import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080040import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080041import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080042import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070043import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080044import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070045import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070046import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070047import android.provider.ContactsContract.RawContactsEntity;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070048import android.support.v4.content.LocalBroadcastManager;
Gary Mai7efa9942016-05-12 11:26:49 -070049import android.support.v4.os.ResultReceiver;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070050import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080051import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070052
Gary Mai363af602016-09-28 10:01:23 -070053import com.android.contacts.activities.ContactEditorActivity;
Wenyi Wang67addcc2015-11-23 10:07:48 -080054import com.android.contacts.common.compat.CompatUtils;
Chiao Chengd7ca03e2012-10-24 15:14:08 -070055import com.android.contacts.common.database.ContactUpdateUtils;
Marcus Hagerott819214d2016-09-29 14:58:27 -070056import com.android.contacts.common.database.SimContactDao;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080057import com.android.contacts.common.model.AccountTypeManager;
Wenyi Wang67addcc2015-11-23 10:07:48 -080058import com.android.contacts.common.model.CPOWrapper;
Yorke Leecd321f62013-10-28 15:20:15 -070059import com.android.contacts.common.model.RawContactDelta;
60import com.android.contacts.common.model.RawContactDeltaList;
61import com.android.contacts.common.model.RawContactModifier;
Marcus Hagerott819214d2016-09-29 14:58:27 -070062import com.android.contacts.common.model.SimContact;
Chiao Cheng428f0082012-11-13 18:38:56 -080063import com.android.contacts.common.model.account.AccountWithDataSet;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070064import com.android.contacts.common.util.PermissionsUtil;
Wenyi Wangaac0e662015-12-18 17:17:33 -080065import com.android.contacts.compat.PinnedPositionsCompat;
Yorke Lee637a38e2013-09-14 08:36:33 -070066import com.android.contacts.util.ContactPhotoUtils;
Walter Jang3a0b4832016-10-12 11:02:54 -070067import com.android.contactsbind.FeedbackHelper;
68
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070069import com.google.common.collect.Lists;
70import com.google.common.collect.Sets;
71
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080072import java.util.ArrayList;
73import java.util.HashSet;
74import java.util.List;
75import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070076
Marcus Hagerott819214d2016-09-29 14:58:27 -070077import static android.Manifest.permission.WRITE_CONTACTS;
78
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080079/**
80 * A service responsible for saving changes to the content provider.
81 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070082public class ContactSaveService extends IntentService {
83 private static final String TAG = "ContactSaveService";
84
Katherine Kuana007e442011-07-07 09:25:34 -070085 /** Set to true in order to view logs on content provider operations */
86 private static final boolean DEBUG = false;
87
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070088 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
89
90 public static final String EXTRA_ACCOUNT_NAME = "accountName";
91 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070092 public static final String EXTRA_DATA_SET = "dataSet";
Marcus Hagerott819214d2016-09-29 14:58:27 -070093 public static final String EXTRA_ACCOUNT = "account";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070094 public static final String EXTRA_CONTENT_VALUES = "contentValues";
95 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
Gary Mai7efa9942016-05-12 11:26:49 -070096 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
97 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070098
Dmitri Plotnikova0114142011-02-15 13:53:21 -080099 public static final String ACTION_SAVE_CONTACT = "saveContact";
100 public static final String EXTRA_CONTACT_STATE = "state";
101 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700102 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -0700103 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -0800104 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700105
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800106 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800107 public static final String ACTION_RENAME_GROUP = "renameGroup";
108 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700109 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800110 public static final String EXTRA_GROUP_ID = "groupId";
111 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700112 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
113 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800114
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800115 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800116 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800117 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800118 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800119 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800120 public static final String EXTRA_STARRED_FLAG = "starred";
Marcus Hagerott3bb85142016-07-29 10:46:36 -0700121 public static final String EXTRA_DISPLAY_NAME = "extraDisplayName";
James Laskeye5a140a2016-10-18 15:43:42 -0700122 public static final String EXTRA_DISPLAY_NAME_ARRAY = "extraDisplayNameArray";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800123
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800124 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
125 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
126 public static final String EXTRA_DATA_ID = "dataId";
127
Gary Mai7efa9942016-05-12 11:26:49 -0700128 public static final String ACTION_SPLIT_CONTACT = "splitContact";
129
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800130 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800131 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800132 public static final String EXTRA_CONTACT_ID1 = "contactId1";
133 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800134
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700135 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
136 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
137
138 public static final String ACTION_SET_RINGTONE = "setRingtone";
139 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
140
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700141 public static final String ACTION_UNDO = "undo";
142 public static final String EXTRA_UNDO_ACTION = "undoAction";
143 public static final String EXTRA_UNDO_DATA = "undoData";
144
Marcus Hagerott819214d2016-09-29 14:58:27 -0700145 public static final String ACTION_IMPORT_FROM_SIM = "importFromSim";
146 public static final String EXTRA_SIM_CONTACTS = "simContacts";
147
148 public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
149 public static final String BROADCAST_SIM_IMPORT_COMPLETE = "simImportComplete";
150
151 public static final String EXTRA_RESULT_CODE = "resultCode";
152 public static final String EXTRA_RESULT_COUNT = "count";
153 public static final String EXTRA_OPERATION_REQUESTED_AT_TIME = "requestedTime";
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700154
Gary Mai7efa9942016-05-12 11:26:49 -0700155 public static final int CP2_ERROR = 0;
156 public static final int CONTACTS_LINKED = 1;
157 public static final int CONTACTS_SPLIT = 2;
Gary Mai31d572e2016-06-03 14:04:32 -0700158 public static final int BAD_ARGUMENTS = 3;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700159 public static final int RESULT_UNKNOWN = 0;
160 public static final int RESULT_SUCCESS = 1;
161 public static final int RESULT_FAILURE = 2;
Gary Mai7efa9942016-05-12 11:26:49 -0700162
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700163 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
164 Data.MIMETYPE,
165 Data.IS_PRIMARY,
166 Data.DATA1,
167 Data.DATA2,
168 Data.DATA3,
169 Data.DATA4,
170 Data.DATA5,
171 Data.DATA6,
172 Data.DATA7,
173 Data.DATA8,
174 Data.DATA9,
175 Data.DATA10,
176 Data.DATA11,
177 Data.DATA12,
178 Data.DATA13,
179 Data.DATA14,
180 Data.DATA15
181 );
182
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800183 private static final int PERSIST_TRIES = 3;
184
Walter Jang0653de32015-07-24 12:12:40 -0700185 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
186
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800187 public interface Listener {
188 public void onServiceCompleted(Intent callbackIntent);
189 }
190
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100191 private static final CopyOnWriteArrayList<Listener> sListeners =
192 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800193
194 private Handler mMainHandler;
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700195 private GroupsDao mGroupsDao;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700196 private SimContactDao mSimContactDao;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800197
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700198 public ContactSaveService() {
199 super(TAG);
200 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800201 mMainHandler = new Handler(Looper.getMainLooper());
202 }
203
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700204 @Override
205 public void onCreate() {
206 super.onCreate();
207 mGroupsDao = new GroupsDaoImpl(this);
Marcus Hagerott819214d2016-09-29 14:58:27 -0700208 mSimContactDao = new SimContactDao(this);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700209 }
210
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800211 public static void registerListener(Listener listener) {
212 if (!(listener instanceof Activity)) {
213 throw new ClassCastException("Only activities can be registered to"
214 + " receive callback from " + ContactSaveService.class.getName());
215 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100216 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800217 }
218
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700219 public static boolean canUndo(Intent resultIntent) {
220 return resultIntent.hasExtra(EXTRA_UNDO_DATA);
221 }
222
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800223 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100224 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700225 }
226
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800227 /**
228 * Returns true if the ContactSaveService was started successfully and false if an exception
229 * was thrown and a Toast error message was displayed.
230 */
231 public static boolean startService(Context context, Intent intent, int saveMode) {
232 try {
233 context.startService(intent);
234 } catch (Exception exception) {
235 final int resId;
236 switch (saveMode) {
Gary Mai363af602016-09-28 10:01:23 -0700237 case ContactEditorActivity.ContactEditor.SaveMode.SPLIT:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800238 resId = R.string.contactUnlinkErrorToast;
239 break;
Gary Mai363af602016-09-28 10:01:23 -0700240 case ContactEditorActivity.ContactEditor.SaveMode.RELOAD:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800241 resId = R.string.contactJoinErrorToast;
242 break;
Gary Mai363af602016-09-28 10:01:23 -0700243 case ContactEditorActivity.ContactEditor.SaveMode.CLOSE:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800244 resId = R.string.contactSavedErrorToast;
245 break;
246 default:
247 resId = R.string.contactGenericErrorToast;
248 }
249 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
250 return false;
251 }
252 return true;
253 }
254
255 /**
256 * Utility method that starts service and handles exception.
257 */
258 public static void startService(Context context, Intent intent) {
259 try {
260 context.startService(intent);
261 } catch (Exception exception) {
262 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
263 }
264 }
265
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700266 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800267 public Object getSystemService(String name) {
268 Object service = super.getSystemService(name);
269 if (service != null) {
270 return service;
271 }
272
273 return getApplicationContext().getSystemService(name);
274 }
275
276 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700277 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800278 if (intent == null) {
279 Log.d(TAG, "onHandleIntent: could not handle null intent");
280 return;
281 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700282 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
283 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
284 // TODO: add more specific error string such as "Turn on Contacts
285 // permission to update your contacts"
286 showToast(R.string.contactSavedErrorToast);
287 return;
288 }
289
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700290 // Call an appropriate method. If we're sure it affects how incoming phone calls are
291 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700292 String action = intent.getAction();
293 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
294 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800295 } else if (ACTION_SAVE_CONTACT.equals(action)) {
296 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800297 } else if (ACTION_CREATE_GROUP.equals(action)) {
298 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800299 } else if (ACTION_RENAME_GROUP.equals(action)) {
300 renameGroup(intent);
301 } else if (ACTION_DELETE_GROUP.equals(action)) {
302 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700303 } else if (ACTION_UPDATE_GROUP.equals(action)) {
304 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800305 } else if (ACTION_SET_STARRED.equals(action)) {
306 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800307 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
308 setSuperPrimary(intent);
309 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
310 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800311 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
312 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800313 } else if (ACTION_DELETE_CONTACT.equals(action)) {
314 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700315 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
316 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800317 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
318 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800319 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
320 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700321 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
322 setSendToVoicemail(intent);
323 } else if (ACTION_SET_RINGTONE.equals(action)) {
324 setRingtone(intent);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700325 } else if (ACTION_UNDO.equals(action)) {
326 undo(intent);
Marcus Hagerott819214d2016-09-29 14:58:27 -0700327 } else if (ACTION_IMPORT_FROM_SIM.equals(action)) {
328 importFromSim(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700329 }
330 }
331
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800332 /**
333 * Creates an intent that can be sent to this service to create a new raw contact
334 * using data presented as a set of ContentValues.
335 */
336 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700337 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700338 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800339 Intent serviceIntent = new Intent(
340 context, ContactSaveService.class);
341 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
342 if (account != null) {
343 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
344 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700345 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800346 }
347 serviceIntent.putParcelableArrayListExtra(
348 ContactSaveService.EXTRA_CONTENT_VALUES, values);
349
350 // Callback intent will be invoked by the service once the new contact is
351 // created. The service will put the URI of the new contact as "data" on
352 // the callback intent.
353 Intent callbackIntent = new Intent(context, callbackActivity);
354 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800355 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
356 return serviceIntent;
357 }
358
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700359 private void createRawContact(Intent intent) {
360 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
361 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700362 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700363 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
364 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
365
366 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
367 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
368 .withValue(RawContacts.ACCOUNT_NAME, accountName)
369 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700370 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700371 .build());
372
373 int size = valueList.size();
374 for (int i = 0; i < size; i++) {
375 ContentValues values = valueList.get(i);
376 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
377 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
378 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
379 .withValues(values)
380 .build());
381 }
382
383 ContentResolver resolver = getContentResolver();
384 ContentProviderResult[] results;
385 try {
386 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
387 } catch (Exception e) {
388 throw new RuntimeException("Failed to store new contact", e);
389 }
390
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700391 Uri rawContactUri = results[0].uri;
392 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
393
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800394 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700395 }
396
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700397 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800398 * Creates an intent that can be sent to this service to create a new raw contact
399 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800400 * This variant is more convenient to use when there is only one photo that can
401 * possibly be updated, as in the Contact Details screen.
402 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
403 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800404 */
Maurice Chu851222a2012-06-21 11:43:08 -0700405 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700406 String saveModeExtraKey, int saveMode, boolean isProfile,
407 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700408 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800409 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700410 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800411 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700412 callbackActivity, callbackAction, bundle,
413 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800414 }
415
416 /**
417 * Creates an intent that can be sent to this service to create a new raw contact
418 * using data presented as a set of ContentValues.
419 * This variant is used when multiple contacts' photos may be updated, as in the
420 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700421 *
Josh Garguse692e012012-01-18 14:53:11 -0800422 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700423 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
424 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800425 */
Maurice Chu851222a2012-06-21 11:43:08 -0700426 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700427 String saveModeExtraKey, int saveMode, boolean isProfile,
428 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700429 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000430 Intent serviceIntent = new Intent(
431 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800432 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
433 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700434 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800435 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
436
Josh Garguse692e012012-01-18 14:53:11 -0800437 if (updatedPhotos != null) {
438 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
439 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800440
Josh Garguse5d3f892012-04-11 11:56:15 -0700441 if (callbackActivity != null) {
442 // Callback intent will be invoked by the service once the contact is
443 // saved. The service will put the URI of the new contact as "data" on
444 // the callback intent.
445 Intent callbackIntent = new Intent(context, callbackActivity);
446 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700447 if (joinContactIdExtraKey != null && joinContactId != null) {
448 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
449 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700450 callbackIntent.setAction(callbackAction);
451 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
452 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800453 return serviceIntent;
454 }
455
456 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700457 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700458 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800459 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800460
Jay Shrauner08099782015-03-25 14:17:11 -0700461 if (state == null) {
462 Log.e(TAG, "Invalid arguments for saveContact request");
463 return;
464 }
465
benny.lin3a4e7a22014-01-08 10:58:08 +0800466 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800467 // Trim any empty fields, and RawContacts, before persisting
468 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700469 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800470
471 Uri lookupUri = null;
472
473 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700474
Josh Garguse692e012012-01-18 14:53:11 -0800475 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800476
Josh Gargusef15c8e2012-01-30 16:42:02 -0800477 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
478 long insertedRawContactId = -1;
479
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800480 // Attempt to persist changes
481 int tries = 0;
482 while (tries++ < PERSIST_TRIES) {
483 try {
484 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800485 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
486
487 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
488
489 for (CPOWrapper cpoWrapper : diffWrapper) {
490 diff.add(cpoWrapper.getOperation());
491 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700492
Katherine Kuana007e442011-07-07 09:25:34 -0700493 if (DEBUG) {
494 Log.v(TAG, "Content Provider Operations:");
495 for (ContentProviderOperation operation : diff) {
496 Log.v(TAG, operation.toString());
497 }
498 }
499
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700500 int numberProcessed = 0;
501 boolean batchFailed = false;
502 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
503 while (numberProcessed < diff.size()) {
504 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
505 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700506 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700507 batchFailed = true;
508 break;
509 } else {
510 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700511 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800512 }
513
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700514 if (batchFailed) {
515 // Retry save
516 continue;
517 }
518
Wenyi Wang67addcc2015-11-23 10:07:48 -0800519 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800520 if (rawContactId == -1) {
521 throw new IllegalStateException("Could not determine RawContact ID after save");
522 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800523 // We don't have to check to see if the value is still -1. If we reach here,
524 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800525 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700526 if (isProfile) {
527 // Since the profile supports local raw contacts, which may have been completely
528 // removed if all information was removed, we need to do a special query to
529 // get the lookup URI for the profile contact (if it still exists).
530 Cursor c = resolver.query(Profile.CONTENT_URI,
531 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
532 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800533 if (c == null) {
534 continue;
535 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700536 try {
Erik162b7e32011-09-20 15:23:55 -0700537 if (c.moveToFirst()) {
538 final long contactId = c.getLong(0);
539 final String lookupKey = c.getString(1);
540 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
541 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700542 } finally {
543 c.close();
544 }
545 } else {
546 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
547 rawContactId);
548 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
549 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800550 if (lookupUri != null) {
551 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
552 }
Josh Garguse692e012012-01-18 14:53:11 -0800553
554 // We can change this back to false later, if we fail to save the contact photo.
555 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800556 break;
557
558 } catch (RemoteException e) {
559 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700560 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800561 break;
562
Jay Shrauner57fca182014-01-17 14:20:50 -0800563 } catch (IllegalArgumentException e) {
564 // This is thrown by applyBatch on malformed requests
Walter Jang3a0b4832016-10-12 11:02:54 -0700565 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Jay Shrauner57fca182014-01-17 14:20:50 -0800566 showToast(R.string.contactSavedErrorToast);
567 break;
568
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800569 } catch (OperationApplicationException e) {
570 // Version consistency failed, re-parent change and try again
571 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
572 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
573 boolean first = true;
574 final int count = state.size();
575 for (int i = 0; i < count; i++) {
576 Long rawContactId = state.getRawContactId(i);
577 if (rawContactId != null && rawContactId != -1) {
578 if (!first) {
579 sb.append(',');
580 }
581 sb.append(rawContactId);
582 first = false;
583 }
584 }
585 sb.append(")");
586
587 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800588 throw new IllegalStateException(
589 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800590 }
591
Maurice Chu851222a2012-06-21 11:43:08 -0700592 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700593 isProfile
594 ? RawContactsEntity.PROFILE_CONTENT_URI
595 : RawContactsEntity.CONTENT_URI,
596 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700597 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700598
599 // Update the new state to use profile URIs if appropriate.
600 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700601 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700602 delta.setProfileQueryUri();
603 }
604 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800605 }
606 }
607
Josh Garguse692e012012-01-18 14:53:11 -0800608 // Now save any updated photos. We do this at the end to ensure that
609 // the ContactProvider already knows about newly-created contacts.
610 if (updatedPhotos != null) {
611 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700612 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800613 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800614
615 // If the raw-contact ID is negative, we are saving a new raw-contact;
616 // replace the bogus ID with the new one that we actually saved the contact at.
617 if (rawContactId < 0) {
618 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800619 }
620
Jay Shrauner511561d2015-04-02 10:35:33 -0700621 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700622 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700623 succeeded = false;
624 }
Josh Garguse692e012012-01-18 14:53:11 -0800625 }
626 }
627
Josh Garguse5d3f892012-04-11 11:56:15 -0700628 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
629 if (callbackIntent != null) {
630 if (succeeded) {
631 // Mark the intent to indicate that the save was successful (even if the lookup URI
632 // is now null). For local contacts or the local profile, it's possible that the
633 // save triggered removal of the contact, so no lookup URI would exist..
634 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
635 }
636 callbackIntent.setData(lookupUri);
637 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800638 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800639 }
640
Josh Garguse692e012012-01-18 14:53:11 -0800641 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700642 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
643 * subsets, adds the returned array to "results".
644 *
645 * @return the size of the array, if not null; -1 when the array is null.
646 */
647 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
648 ContentProviderResult[] results, ContentResolver resolver)
649 throws RemoteException, OperationApplicationException {
650 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
651 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
652 subset.addAll(diff.subList(offset, offset + subsetCount));
653 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
654 .AUTHORITY, subset);
655 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
656 return -1;
657 }
658 for (ContentProviderResult c : subsetResult) {
659 results[offset++] = c;
660 }
661 return subsetResult.length;
662 }
663
664 /**
Josh Garguse692e012012-01-18 14:53:11 -0800665 * Save updated photo for the specified raw-contact.
666 * @return true for success, false for failure
667 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800668 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800669 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800670 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
671 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
672
benny.lin3a4e7a22014-01-08 10:58:08 +0800673 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800674 }
675
Josh Gargusef15c8e2012-01-30 16:42:02 -0800676 /**
677 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
678 */
Maurice Chu851222a2012-06-21 11:43:08 -0700679 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800680 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800681 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800682 long existingRawContactId = state.findRawContactId();
683 if (existingRawContactId != -1) {
684 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800685 }
686
Wenyi Wang67addcc2015-11-23 10:07:48 -0800687 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800688 }
689
690 /**
691 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
692 */
693 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800694 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800695 if (results == null) {
696 return -1;
697 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800698 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800699 final int numResults = results.length;
700 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800701 final CPOWrapper cpoWrapper = diffWrapper.get(i);
702 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
703 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
704 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800705 return ContentUris.parseId(results[i].uri);
706 }
707 }
708 return -1;
709 }
710
711 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700712 * Creates an intent that can be sent to this service to create a new group as
713 * well as add new members at the same time.
714 *
715 * @param context of the application
716 * @param account in which the group should be created
717 * @param label is the name of the group (cannot be null)
718 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
719 * should be added to the group
720 * @param callbackActivity is the activity to send the callback intent to
721 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700722 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700723 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700724 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700725 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800726 Intent serviceIntent = new Intent(context, ContactSaveService.class);
727 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
728 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
729 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700730 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800731 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700732 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700733
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800734 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700735 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800736 Intent callbackIntent = new Intent(context, callbackActivity);
737 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700738 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800739
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700740 return serviceIntent;
741 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800742
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800743 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700744 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
745 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
746 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
747 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700748 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800749
Katherine Kuan717e3432011-07-13 17:03:24 -0700750 // Create the new group
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700751 final Uri groupUri = mGroupsDao.create(label,
752 new AccountWithDataSet(accountName, accountType, dataSet));
753 final ContentResolver resolver = getContentResolver();
Katherine Kuan717e3432011-07-13 17:03:24 -0700754
755 // If there's no URI, then the insertion failed. Abort early because group members can't be
756 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800757 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700758 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800759 return;
760 }
761
Katherine Kuan717e3432011-07-13 17:03:24 -0700762 // Add new group members
763 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
764
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700765 ContentValues values = new ContentValues();
Katherine Kuan717e3432011-07-13 17:03:24 -0700766 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
Walter Jang8bac28b2016-08-30 10:34:55 -0700767 // with the way other intent extras that are passed to the
Gary Mai363af602016-09-28 10:01:23 -0700768 // {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800769 values.clear();
770 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
771 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
772
773 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700774 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700775 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800776 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800777 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800778 }
779
780 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800781 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800782 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700783 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700784 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800785 Intent serviceIntent = new Intent(context, ContactSaveService.class);
786 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
787 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
788 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700789
790 // Callback intent will be invoked by the service once the group is renamed.
791 Intent callbackIntent = new Intent(context, callbackActivity);
792 callbackIntent.setAction(callbackAction);
793 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
794
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800795 return serviceIntent;
796 }
797
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800798 private void renameGroup(Intent intent) {
799 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
800 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
801
802 if (groupId == -1) {
803 Log.e(TAG, "Invalid arguments for renameGroup request");
804 return;
805 }
806
807 ContentValues values = new ContentValues();
808 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700809 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
810 getContentResolver().update(groupUri, values, null, null);
811
812 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
813 callbackIntent.setData(groupUri);
814 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800815 }
816
817 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800818 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800819 */
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700820 public static Intent createGroupDeletionIntent(Context context, long groupId) {
821 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800822 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800823 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700824
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800825 return serviceIntent;
826 }
827
828 private void deleteGroup(Intent intent) {
829 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
830 if (groupId == -1) {
831 Log.e(TAG, "Invalid arguments for deleteGroup request");
832 return;
833 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700834 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800835
Marcus Hagerott819214d2016-09-29 14:58:27 -0700836 final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700837 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
838 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
839 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
Walter Jang72f99882016-05-26 09:01:31 -0700840
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700841 mGroupsDao.delete(groupUri);
842
843 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
844 }
845
846 public static Intent createUndoIntent(Context context, Intent resultIntent) {
847 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
848 serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
849 serviceIntent.putExtras(resultIntent);
850 return serviceIntent;
851 }
852
853 private void undo(Intent intent) {
854 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
855 if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
856 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
Walter Jang72f99882016-05-26 09:01:31 -0700857 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800858 }
859
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700860
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800861 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700862 * Creates an intent that can be sent to this service to rename a group as
863 * well as add and remove members from the group.
864 *
865 * @param context of the application
866 * @param groupId of the group that should be modified
867 * @param newLabel is the updated name of the group (can be null if the name
868 * should not be updated)
869 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
870 * should be added to the group
871 * @param rawContactsToRemove is an array of raw contact IDs for contacts
872 * that should be removed from the group
873 * @param callbackActivity is the activity to send the callback intent to
874 * @param callbackAction is the intent action for the callback intent
875 */
876 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
877 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700878 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700879 Intent serviceIntent = new Intent(context, ContactSaveService.class);
880 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
881 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
882 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
883 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
884 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
885 rawContactsToRemove);
886
887 // Callback intent will be invoked by the service once the group is updated
888 Intent callbackIntent = new Intent(context, callbackActivity);
889 callbackIntent.setAction(callbackAction);
890 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
891
892 return serviceIntent;
893 }
894
895 private void updateGroup(Intent intent) {
896 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
897 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
898 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
899 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
900
901 if (groupId == -1) {
902 Log.e(TAG, "Invalid arguments for updateGroup request");
903 return;
904 }
905
906 final ContentResolver resolver = getContentResolver();
907 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
908
909 // Update group name if necessary
910 if (label != null) {
911 ContentValues values = new ContentValues();
912 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700913 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700914 }
915
Katherine Kuan717e3432011-07-13 17:03:24 -0700916 // Add and remove members if necessary
917 addMembersToGroup(resolver, rawContactsToAdd, groupId);
918 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
919
920 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
921 callbackIntent.setData(groupUri);
922 deliverCallback(callbackIntent);
923 }
924
Walter Jang3a0b4832016-10-12 11:02:54 -0700925 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700926 long groupId) {
927 if (rawContactsToAdd == null) {
928 return;
929 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700930 for (long rawContactId : rawContactsToAdd) {
931 try {
932 final ArrayList<ContentProviderOperation> rawContactOperations =
933 new ArrayList<ContentProviderOperation>();
934
935 // Build an assert operation to ensure the contact is not already in the group
936 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
937 .newAssertQuery(Data.CONTENT_URI);
938 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
939 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
940 new String[] { String.valueOf(rawContactId),
941 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
942 assertBuilder.withExpectedCount(0);
943 rawContactOperations.add(assertBuilder.build());
944
945 // Build an insert operation to add the contact to the group
946 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
947 .newInsert(Data.CONTENT_URI);
948 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
949 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
950 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
951 rawContactOperations.add(insertBuilder.build());
952
953 if (DEBUG) {
954 for (ContentProviderOperation operation : rawContactOperations) {
955 Log.v(TAG, operation.toString());
956 }
957 }
958
959 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700960 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800961 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700962 }
963 } catch (RemoteException e) {
964 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700965 FeedbackHelper.sendFeedback(this, TAG,
966 "Problem persisting user edits for raw contact ID " +
967 String.valueOf(rawContactId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700968 } catch (OperationApplicationException e) {
969 // The assert could have failed because the contact is already in the group,
970 // just continue to the next contact
Walter Jang3a0b4832016-10-12 11:02:54 -0700971 FeedbackHelper.sendFeedback(this, TAG,
972 "Assert failed in adding raw contact ID " +
973 String.valueOf(rawContactId) + ". Already exists in group " +
974 String.valueOf(groupId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700975 }
976 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700977 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700978
Daniel Lehmann18958a22012-02-28 17:45:25 -0800979 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700980 long groupId) {
981 if (rawContactsToRemove == null) {
982 return;
983 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700984 for (long rawContactId : rawContactsToRemove) {
985 // Apply the delete operation on the data row for the given raw contact's
986 // membership in the given group. If no contact matches the provided selection, then
987 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800988 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700989 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
990 new String[] { String.valueOf(rawContactId),
991 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
992 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700993 }
994
995 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800996 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800997 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800998 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
999 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1000 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
1001 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1002 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
1003
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001004 return serviceIntent;
1005 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001006
1007 private void setStarred(Intent intent) {
1008 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1009 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
1010 if (contactUri == null) {
1011 Log.e(TAG, "Invalid arguments for setStarred request");
1012 return;
1013 }
1014
1015 final ContentValues values = new ContentValues(1);
1016 values.put(Contacts.STARRED, value);
1017 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -07001018
1019 // Undemote the contact if necessary
1020 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
1021 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -08001022 if (c == null) {
1023 return;
1024 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001025 try {
1026 if (c.moveToFirst()) {
1027 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -07001028
1029 // Don't bother undemoting if this contact is the user's profile.
1030 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -08001031 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -07001032 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001033 }
1034 } finally {
1035 c.close();
1036 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001037 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001038
1039 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -07001040 * Creates an intent that can be sent to this service to set the redirect to voicemail.
1041 */
1042 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
1043 boolean value) {
1044 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1045 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1046 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1047 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1048
1049 return serviceIntent;
1050 }
1051
1052 private void setSendToVoicemail(Intent intent) {
1053 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1054 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1055 if (contactUri == null) {
1056 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1057 return;
1058 }
1059
1060 final ContentValues values = new ContentValues(1);
1061 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1062 getContentResolver().update(contactUri, values, null, null);
1063 }
1064
1065 /**
1066 * Creates an intent that can be sent to this service to save the contact's ringtone.
1067 */
1068 public static Intent createSetRingtone(Context context, Uri contactUri,
1069 String value) {
1070 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1071 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1072 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1073 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1074
1075 return serviceIntent;
1076 }
1077
1078 private void setRingtone(Intent intent) {
1079 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1080 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1081 if (contactUri == null) {
1082 Log.e(TAG, "Invalid arguments for setRingtone");
1083 return;
1084 }
1085 ContentValues values = new ContentValues(1);
1086 values.put(Contacts.CUSTOM_RINGTONE, value);
1087 getContentResolver().update(contactUri, values, null, null);
1088 }
1089
1090 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001091 * Creates an intent that sets the selected data item as super primary (default)
1092 */
1093 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1094 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1095 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1096 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1097 return serviceIntent;
1098 }
1099
1100 private void setSuperPrimary(Intent intent) {
1101 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1102 if (dataId == -1) {
1103 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1104 return;
1105 }
1106
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001107 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001108 }
1109
1110 /**
1111 * Creates an intent that clears the primary flag of all data items that belong to the same
1112 * raw_contact as the given data item. Will only clear, if the data item was primary before
1113 * this call
1114 */
1115 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1116 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1117 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1118 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1119 return serviceIntent;
1120 }
1121
1122 private void clearPrimary(Intent intent) {
1123 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1124 if (dataId == -1) {
1125 Log.e(TAG, "Invalid arguments for clearPrimary request");
1126 return;
1127 }
1128
1129 // Update the primary values in the data record.
1130 ContentValues values = new ContentValues(1);
1131 values.put(Data.IS_SUPER_PRIMARY, 0);
1132 values.put(Data.IS_PRIMARY, 0);
1133
1134 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1135 values, null, null);
1136 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001137
1138 /**
1139 * Creates an intent that can be sent to this service to delete a contact.
1140 */
1141 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1142 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1143 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1144 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1145 return serviceIntent;
1146 }
1147
Brian Attwelld2962a32015-03-02 14:48:50 -08001148 /**
1149 * Creates an intent that can be sent to this service to delete multiple contacts.
1150 */
1151 public static Intent createDeleteMultipleContactsIntent(Context context,
James Laskeye5a140a2016-10-18 15:43:42 -07001152 long[] contactIds, final String[] names) {
Brian Attwelld2962a32015-03-02 14:48:50 -08001153 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1154 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1155 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
James Laskeye5a140a2016-10-18 15:43:42 -07001156 serviceIntent.putExtra(ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY, names);
Brian Attwelld2962a32015-03-02 14:48:50 -08001157 return serviceIntent;
1158 }
1159
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001160 private void deleteContact(Intent intent) {
1161 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1162 if (contactUri == null) {
1163 Log.e(TAG, "Invalid arguments for deleteContact request");
1164 return;
1165 }
1166
1167 getContentResolver().delete(contactUri, null, null);
1168 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001169
Brian Attwelld2962a32015-03-02 14:48:50 -08001170 private void deleteMultipleContacts(Intent intent) {
1171 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1172 if (contactIds == null) {
1173 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1174 return;
1175 }
1176 for (long contactId : contactIds) {
1177 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1178 getContentResolver().delete(contactUri, null, null);
1179 }
James Laskeye5a140a2016-10-18 15:43:42 -07001180 final String[] names = intent.getStringArrayExtra(
1181 ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY);
1182 final String deleteToastMessage;
1183 if (names.length == 0) {
1184 deleteToastMessage = getResources().getQuantityString(
1185 R.plurals.contacts_deleted_toast, contactIds.length);
1186 } else if (names.length == 1) {
1187 deleteToastMessage = getResources().getString(
1188 R.string.contacts_deleted_one_named_toast, names);
1189 } else if (names.length == 2) {
1190 deleteToastMessage = getResources().getString(
1191 R.string.contacts_deleted_two_named_toast, names);
1192 } else {
1193 deleteToastMessage = getResources().getString(
1194 R.string.contacts_deleted_many_named_toast, names);
1195 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001196 mMainHandler.post(new Runnable() {
1197 @Override
1198 public void run() {
1199 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1200 .show();
1201 }
1202 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001203 }
1204
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001205 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001206 * 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 -07001207 * pieces. This will set the raw contact ids to TYPE_AUTOMATIC for AggregationExceptions so
1208 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001209 */
1210 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1211 ResultReceiver receiver) {
1212 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1213 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1214 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1215 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1216 return serviceIntent;
1217 }
1218
1219 private void splitContact(Intent intent) {
1220 final long rawContactIds[][] = (long[][]) intent
1221 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001222 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Mai7efa9942016-05-12 11:26:49 -07001223 if (rawContactIds == null) {
1224 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001225 if (receiver != null) {
1226 receiver.send(BAD_ARGUMENTS, new Bundle());
1227 }
Gary Mai7efa9942016-05-12 11:26:49 -07001228 return;
1229 }
1230 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1231 final ContentResolver resolver = getContentResolver();
1232 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001233 for (int i = 0; i < rawContactIds.length; i++) {
1234 for (int j = 0; j < rawContactIds.length; j++) {
1235 if (i != j) {
1236 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
1237 if (receiver != null) {
1238 receiver.send(CP2_ERROR, new Bundle());
1239 return;
1240 }
1241 }
1242 }
1243 }
1244 }
1245 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1246 if (receiver != null) {
1247 receiver.send(CP2_ERROR, new Bundle());
1248 }
1249 return;
1250 }
1251 if (receiver != null) {
1252 receiver.send(CONTACTS_SPLIT, new Bundle());
1253 } else {
1254 showToast(R.string.contactUnlinkedToast);
1255 }
1256 }
1257
1258 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001259 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001260 * and {@param rawContactIds2} to {@param operations}.
1261 * @return false if an error occurred, true otherwise.
1262 */
1263 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
1264 long[] rawContactIds1, long[] rawContactIds2) {
1265 if (rawContactIds1 == null || rawContactIds2 == null) {
1266 Log.e(TAG, "Invalid arguments for splitContact request");
1267 return false;
1268 }
1269 // For each pair of raw contacts, insert an aggregation exception
1270 final ContentResolver resolver = getContentResolver();
1271 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1272 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1273 for (int i = 0; i < rawContactIds1.length; i++) {
1274 for (int j = 0; j < rawContactIds2.length; j++) {
1275 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
1276 // Before we get to 500 we need to flush the operations list
1277 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1278 if (!applyOperations(resolver, operations)) {
1279 return false;
1280 }
1281 operations.clear();
1282 }
1283 }
1284 }
1285 return true;
1286 }
1287
1288 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001289 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001290 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001291 */
1292 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001293 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001294 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1295 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1296 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1297 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001298
1299 // Callback intent will be invoked by the service once the contacts are joined.
1300 Intent callbackIntent = new Intent(context, callbackActivity);
1301 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001302 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1303
1304 return serviceIntent;
1305 }
1306
Brian Attwelld3946ca2015-03-03 11:13:49 -08001307 /**
1308 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1309 * No special attention is paid to where the resulting contact's name is taken from.
1310 */
Gary Mai7efa9942016-05-12 11:26:49 -07001311 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1312 ResultReceiver receiver) {
1313 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001314 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1315 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001316 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001317 return serviceIntent;
1318 }
1319
Gary Mai7efa9942016-05-12 11:26:49 -07001320 /**
1321 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1322 * No special attention is paid to where the resulting contact's name is taken from.
1323 */
1324 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1325 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1326 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001327
1328 private interface JoinContactQuery {
1329 String[] PROJECTION = {
1330 RawContacts._ID,
1331 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001332 RawContacts.DISPLAY_NAME_SOURCE,
1333 };
1334
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001335 int _ID = 0;
1336 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001337 int DISPLAY_NAME_SOURCE = 2;
1338 }
1339
1340 private interface ContactEntityQuery {
1341 String[] PROJECTION = {
1342 Contacts.Entity.DATA_ID,
1343 Contacts.Entity.CONTACT_ID,
1344 Contacts.Entity.IS_SUPER_PRIMARY,
1345 };
1346 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1347 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1348 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1349 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1350
1351 int DATA_ID = 0;
1352 int CONTACT_ID = 1;
1353 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001354 }
1355
Brian Attwelld3946ca2015-03-03 11:13:49 -08001356 private void joinSeveralContacts(Intent intent) {
1357 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001358
Gary Mai7efa9942016-05-12 11:26:49 -07001359 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001360
Brian Attwelld3946ca2015-03-03 11:13:49 -08001361 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001362 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1363 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001364 if (rawContactIds == null) {
1365 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001366 if (receiver != null) {
1367 receiver.send(BAD_ARGUMENTS, new Bundle());
1368 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001369 return;
1370 }
1371
Brian Attwelld3946ca2015-03-03 11:13:49 -08001372 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001373 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001374 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1375 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1376 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001377 for (int i = 0; i < rawContactIds.length; i++) {
1378 for (int j = 0; j < rawContactIds.length; j++) {
1379 if (i != j) {
1380 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1381 }
Walter Jang0653de32015-07-24 12:12:40 -07001382 // Before we get to 500 we need to flush the operations list
1383 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001384 if (!applyOperations(resolver, operations)) {
1385 if (receiver != null) {
1386 receiver.send(CP2_ERROR, new Bundle());
1387 }
Walter Jang0653de32015-07-24 12:12:40 -07001388 return;
1389 }
1390 operations.clear();
1391 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001392 }
1393 }
Gary Mai7efa9942016-05-12 11:26:49 -07001394 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1395 if (receiver != null) {
1396 receiver.send(CP2_ERROR, new Bundle());
1397 }
Walter Jang0653de32015-07-24 12:12:40 -07001398 return;
1399 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001400
John Shaoa3c507a2016-09-13 14:26:17 -07001401
1402 final String name = queryNameOfLinkedContacts(contactIds);
1403 if (name != null) {
1404 if (receiver != null) {
1405 final Bundle result = new Bundle();
1406 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1407 result.putString(EXTRA_DISPLAY_NAME, name);
1408 receiver.send(CONTACTS_LINKED, result);
1409 } else {
1410 showToast(R.string.contactsJoinedMessage);
1411 }
Gary Mai7efa9942016-05-12 11:26:49 -07001412 } else {
John Shaoa3c507a2016-09-13 14:26:17 -07001413 if (receiver != null) {
1414 receiver.send(CP2_ERROR, new Bundle());
1415 }
1416 showToast(R.string.contactJoinErrorToast);
Gary Mai7efa9942016-05-12 11:26:49 -07001417 }
Walter Jang0653de32015-07-24 12:12:40 -07001418 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001419
John Shaoa3c507a2016-09-13 14:26:17 -07001420 /** Get the display name of the top-level contact after the contacts have been linked. */
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001421 private String queryNameOfLinkedContacts(long[] contactIds) {
1422 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1423 final String[] whereArgs = new String[contactIds.length];
1424 for (int i = 0; i < contactIds.length; i++) {
1425 whereArgs[i] = String.valueOf(contactIds[i]);
1426 whereBuilder.append("?,");
1427 }
1428 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1429 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
John Shaoa3c507a2016-09-13 14:26:17 -07001430 new String[]{Contacts._ID, Contacts.DISPLAY_NAME},
1431 whereBuilder.toString(), whereArgs, null);
1432
1433 String name = null;
1434 long contactId = 0;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001435 try {
1436 if (cursor.moveToFirst()) {
John Shaoa3c507a2016-09-13 14:26:17 -07001437 contactId = cursor.getLong(0);
1438 name = cursor.getString(1);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001439 }
John Shaoa3c507a2016-09-13 14:26:17 -07001440 while(cursor.moveToNext()) {
1441 if (cursor.getLong(0) != contactId) {
1442 return null;
1443 }
1444 }
1445 return name == null ? "" : name;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001446 } finally {
John Shaoa3c507a2016-09-13 14:26:17 -07001447 if (cursor != null) {
1448 cursor.close();
1449 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001450 }
1451 }
1452
Walter Jang0653de32015-07-24 12:12:40 -07001453 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001454 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001455 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001456 try {
John Shaoa3c507a2016-09-13 14:26:17 -07001457 final ContentProviderResult[] result =
1458 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1459 for (int i = 0; i < result.length; ++i) {
1460 // if no rows were modified in the operation then we count it as fail.
1461 if (result[i].count < 0) {
1462 throw new OperationApplicationException();
1463 }
1464 }
Walter Jang0653de32015-07-24 12:12:40 -07001465 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001466 } catch (RemoteException | OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001467 FeedbackHelper.sendFeedback(this, TAG,
1468 "Failed to apply aggregation exception batch", e);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001469 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001470 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001471 }
1472 }
1473
Brian Attwelld3946ca2015-03-03 11:13:49 -08001474 private void joinContacts(Intent intent) {
1475 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1476 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001477
1478 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001479 // in the join UIs.
1480 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1481 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001482 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001483 return;
1484 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001485
Brian Attwell548f5c62015-01-27 17:46:46 -08001486 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001487
1488 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001489 for (int i = 0; i < rawContactIds.length; i++) {
1490 for (int j = 0; j < rawContactIds.length; j++) {
1491 if (i != j) {
1492 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1493 }
1494 }
1495 }
1496
Brian Attwelld3946ca2015-03-03 11:13:49 -08001497 final ContentResolver resolver = getContentResolver();
1498
Brian Attwell548f5c62015-01-27 17:46:46 -08001499 // Use the name for contactId1 as the name for the newly aggregated contact.
1500 final Uri contactId1Uri = ContentUris.withAppendedId(
1501 Contacts.CONTENT_URI, contactId1);
1502 final Uri entityUri = Uri.withAppendedPath(
1503 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1504 Cursor c = resolver.query(entityUri,
1505 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1506 if (c == null) {
1507 Log.e(TAG, "Unable to open Contacts DB cursor");
1508 showToast(R.string.contactSavedErrorToast);
1509 return;
1510 }
1511 long dataIdToAddSuperPrimary = -1;
1512 try {
1513 if (c.moveToFirst()) {
1514 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1515 }
1516 } finally {
1517 c.close();
1518 }
1519
1520 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1521 // display name does not change as a result of the join.
1522 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001523 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001524 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1525 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1526 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001527 operations.add(builder.build());
1528 }
1529
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001530 // Apply all aggregation exceptions as one batch
John Shaoa3c507a2016-09-13 14:26:17 -07001531 final boolean success = applyOperations(resolver, operations);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001532
John Shaoa3c507a2016-09-13 14:26:17 -07001533 final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2});
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001534 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
John Shaoa3c507a2016-09-13 14:26:17 -07001535 if (success && name != null) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001536 Uri uri = RawContacts.getContactLookupUri(resolver,
1537 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1538 callbackIntent.setData(uri);
1539 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001540 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001541 }
1542
Gary Mai7efa9942016-05-12 11:26:49 -07001543 /**
1544 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1545 * array of the return value holds an array of raw contact ids for one contactId.
1546 * @param contactIds
1547 * @return
1548 */
1549 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1550 final long[][] rawContactIds = new long[contactIds.length][];
1551 for (int i = 0; i < contactIds.length; i++) {
1552 rawContactIds[i] = getRawContactIds(contactIds[i]);
1553 }
1554 return rawContactIds;
1555 }
1556
1557 /**
1558 * Gets the raw contact ids associated with {@param contactId}.
1559 * @param contactId
1560 * @return Array of raw contact ids.
1561 */
1562 private long[] getRawContactIds(long contactId) {
1563 final ContentResolver resolver = getContentResolver();
1564 long rawContactIds[];
1565
1566 final StringBuilder queryBuilder = new StringBuilder();
1567 queryBuilder.append(RawContacts.CONTACT_ID)
1568 .append("=")
1569 .append(String.valueOf(contactId));
1570
1571 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1572 JoinContactQuery.PROJECTION,
1573 queryBuilder.toString(),
1574 null, null);
1575 if (c == null) {
1576 Log.e(TAG, "Unable to open Contacts DB cursor");
1577 return null;
1578 }
1579 try {
1580 rawContactIds = new long[c.getCount()];
1581 for (int i = 0; i < rawContactIds.length; i++) {
1582 c.moveToPosition(i);
1583 final long rawContactId = c.getLong(JoinContactQuery._ID);
1584 rawContactIds[i] = rawContactId;
1585 }
1586 } finally {
1587 c.close();
1588 }
1589 return rawContactIds;
1590 }
1591
Brian Attwelld3946ca2015-03-03 11:13:49 -08001592 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1593 if (contactIds == null) {
1594 return null;
1595 }
1596
Brian Attwell548f5c62015-01-27 17:46:46 -08001597 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001598
1599 final StringBuilder queryBuilder = new StringBuilder();
1600 final String stringContactIds[] = new String[contactIds.length];
1601 for (int i = 0; i < contactIds.length; i++) {
1602 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1603 stringContactIds[i] = String.valueOf(contactIds[i]);
1604 if (contactIds[i] == -1) {
1605 return null;
1606 }
1607 if (i == contactIds.length -1) {
1608 break;
1609 }
1610 queryBuilder.append(" OR ");
1611 }
1612
Brian Attwell548f5c62015-01-27 17:46:46 -08001613 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1614 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001615 queryBuilder.toString(),
1616 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001617 if (c == null) {
1618 Log.e(TAG, "Unable to open Contacts DB cursor");
1619 showToast(R.string.contactSavedErrorToast);
1620 return null;
1621 }
Gary Mai7efa9942016-05-12 11:26:49 -07001622 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001623 try {
1624 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001625 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001626 return null;
1627 }
1628 rawContactIds = new long[c.getCount()];
1629 for (int i = 0; i < rawContactIds.length; i++) {
1630 c.moveToPosition(i);
1631 long rawContactId = c.getLong(JoinContactQuery._ID);
1632 rawContactIds[i] = rawContactId;
1633 }
1634 } finally {
1635 c.close();
1636 }
1637 return rawContactIds;
1638 }
1639
Brian Attwelld3946ca2015-03-03 11:13:49 -08001640 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1641 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1642 }
1643
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001644 /**
1645 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1646 */
1647 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1648 long rawContactId1, long rawContactId2) {
1649 Builder builder =
1650 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1651 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1652 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1653 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1654 operations.add(builder.build());
1655 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001656
1657 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001658 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} ContentProviderOperation.
Gary Mai7efa9942016-05-12 11:26:49 -07001659 */
1660 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
1661 long rawContactId1, long rawContactId2) {
1662 final Builder builder =
1663 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Mai53fe0d22016-07-26 17:23:53 -07001664 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001665 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1666 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1667 operations.add(builder.build());
1668 }
1669
Marcus Hagerott819214d2016-09-29 14:58:27 -07001670 public static Intent createImportFromSimIntent(Context context,
1671 ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
1672 return new Intent(context, ContactSaveService.class)
1673 .setAction(ACTION_IMPORT_FROM_SIM)
1674 .putExtra(EXTRA_SIM_CONTACTS, contacts)
1675 .putExtra(EXTRA_ACCOUNT, targetAccount);
1676 }
1677
1678 private void importFromSim(Intent intent) {
1679 final Intent result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
1680 .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, System.currentTimeMillis());
1681 try {
1682 final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
1683 final ArrayList<SimContact> contacts =
1684 intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
1685 mSimContactDao.importContacts(contacts, targetAccount);
1686 // notify success
1687 LocalBroadcastManager.getInstance(this).sendBroadcast(result
1688 .putExtra(EXTRA_RESULT_COUNT, contacts.size())
1689 .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS));
1690 if (Log.isLoggable(TAG, Log.DEBUG)) {
1691 Log.d(TAG, "importFromSim completed successfully");
1692 }
1693 } catch (RemoteException|OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001694 FeedbackHelper.sendFeedback(this, TAG, "Failed to import contacts from SIM card", e);
Marcus Hagerott819214d2016-09-29 14:58:27 -07001695 LocalBroadcastManager.getInstance(this).sendBroadcast(result
1696 .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE));
1697 }
1698 }
1699
Gary Mai7efa9942016-05-12 11:26:49 -07001700 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001701 * Shows a toast on the UI thread.
1702 */
1703 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001704 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001705
1706 @Override
1707 public void run() {
1708 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1709 }
1710 });
1711 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001712
1713 private void deliverCallback(final Intent callbackIntent) {
1714 mMainHandler.post(new Runnable() {
1715
1716 @Override
1717 public void run() {
1718 deliverCallbackOnUiThread(callbackIntent);
1719 }
1720 });
1721 }
1722
1723 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1724 // TODO: this assumes that if there are multiple instances of the same
1725 // activity registered, the last one registered is the one waiting for
1726 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001727 for (Listener listener : sListeners) {
1728 if (callbackIntent.getComponent().equals(
1729 ((Activity) listener).getIntent().getComponent())) {
1730 listener.onServiceCompleted(callbackIntent);
1731 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001732 }
1733 }
1734 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001735
1736 public interface GroupsDao {
1737 Uri create(String title, AccountWithDataSet account);
1738 int delete(Uri groupUri);
1739 Bundle captureDeletionUndoData(Uri groupUri);
1740 Uri undoDeletion(Bundle undoData);
1741 }
1742
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001743 public static class GroupsDaoImpl implements GroupsDao {
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001744 public static final String KEY_GROUP_DATA = "groupData";
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001745 public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
1746
1747 private static final String TAG = "GroupsDao";
1748 private final Context context;
1749 private final ContentResolver contentResolver;
1750
1751 public GroupsDaoImpl(Context context) {
1752 this(context, context.getContentResolver());
1753 }
1754
1755 public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
1756 this.context = context;
1757 this.contentResolver = contentResolver;
1758 }
1759
1760 public Bundle captureDeletionUndoData(Uri groupUri) {
1761 final long groupId = ContentUris.parseId(groupUri);
1762 final Bundle result = new Bundle();
1763
1764 final Cursor cursor = contentResolver.query(groupUri,
1765 new String[]{
1766 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
1767 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
1768 Groups.SHOULD_SYNC
1769 },
1770 Groups.DELETED + "=?", new String[] { "0" }, null);
1771 try {
1772 if (cursor.moveToFirst()) {
1773 final ContentValues groupValues = new ContentValues();
1774 DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
1775 result.putParcelable(KEY_GROUP_DATA, groupValues);
1776 } else {
1777 // Group doesn't exist.
1778 return result;
1779 }
1780 } finally {
1781 cursor.close();
1782 }
1783
1784 final Cursor membersCursor = contentResolver.query(
1785 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
1786 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1787 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
1788 final long[] memberIds = new long[membersCursor.getCount()];
1789 int i = 0;
1790 while (membersCursor.moveToNext()) {
1791 memberIds[i++] = membersCursor.getLong(0);
1792 }
1793 result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
1794 return result;
1795 }
1796
1797 public Uri undoDeletion(Bundle deletedGroupData) {
1798 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
1799 if (groupData == null) {
1800 return null;
1801 }
1802 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
1803 final long groupId = ContentUris.parseId(groupUri);
1804
1805 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
1806 if (memberIds == null) {
1807 return groupUri;
1808 }
1809 final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
1810 for (int i = 0; i < memberIds.length; i++) {
1811 memberInsertions[i] = new ContentValues();
1812 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
1813 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1814 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
1815 }
1816 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
1817 if (inserted != memberIds.length) {
1818 Log.e(TAG, "Could not recover some members for group deletion undo");
1819 }
1820
1821 return groupUri;
1822 }
1823
1824 public Uri create(String title, AccountWithDataSet account) {
1825 final ContentValues values = new ContentValues();
1826 values.put(Groups.TITLE, title);
1827 values.put(Groups.ACCOUNT_NAME, account.name);
1828 values.put(Groups.ACCOUNT_TYPE, account.type);
1829 values.put(Groups.DATA_SET, account.dataSet);
1830 return contentResolver.insert(Groups.CONTENT_URI, values);
1831 }
1832
1833 public int delete(Uri groupUri) {
1834 return contentResolver.delete(groupUri, null, null);
1835 }
1836 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001837}