blob: 853e676780d9231b84d875974ddff29cc632ea32 [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 Hagerott66e8b222016-10-23 15:41:55 -070048import android.support.annotation.NonNull;
49import android.support.annotation.Nullable;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070050import android.support.v4.content.LocalBroadcastManager;
Gary Mai7efa9942016-05-12 11:26:49 -070051import android.support.v4.os.ResultReceiver;
James Laskeyf62b4882016-10-21 11:36:40 -070052import android.text.TextUtils;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070053import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080054import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070055
Gary Mai363af602016-09-28 10:01:23 -070056import com.android.contacts.activities.ContactEditorActivity;
Wenyi Wang67addcc2015-11-23 10:07:48 -080057import com.android.contacts.common.compat.CompatUtils;
Chiao Chengd7ca03e2012-10-24 15:14:08 -070058import com.android.contacts.common.database.ContactUpdateUtils;
Marcus Hagerott819214d2016-09-29 14:58:27 -070059import com.android.contacts.common.database.SimContactDao;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080060import com.android.contacts.common.model.AccountTypeManager;
Wenyi Wang67addcc2015-11-23 10:07:48 -080061import com.android.contacts.common.model.CPOWrapper;
Yorke Leecd321f62013-10-28 15:20:15 -070062import com.android.contacts.common.model.RawContactDelta;
63import com.android.contacts.common.model.RawContactDeltaList;
64import com.android.contacts.common.model.RawContactModifier;
Marcus Hagerott819214d2016-09-29 14:58:27 -070065import com.android.contacts.common.model.SimContact;
Chiao Cheng428f0082012-11-13 18:38:56 -080066import com.android.contacts.common.model.account.AccountWithDataSet;
James Laskeyf62b4882016-10-21 11:36:40 -070067import com.android.contacts.common.preference.ContactsPreferences;
68import com.android.contacts.common.util.ContactDisplayUtils;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070069import com.android.contacts.common.util.PermissionsUtil;
Wenyi Wangaac0e662015-12-18 17:17:33 -080070import com.android.contacts.compat.PinnedPositionsCompat;
Yorke Lee637a38e2013-09-14 08:36:33 -070071import com.android.contacts.util.ContactPhotoUtils;
Walter Jang3a0b4832016-10-12 11:02:54 -070072import com.android.contactsbind.FeedbackHelper;
73
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070074import com.google.common.collect.Lists;
75import com.google.common.collect.Sets;
76
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080077import java.util.ArrayList;
78import java.util.HashSet;
79import java.util.List;
80import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070081
Marcus Hagerott819214d2016-09-29 14:58:27 -070082import static android.Manifest.permission.WRITE_CONTACTS;
83
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080084/**
85 * A service responsible for saving changes to the content provider.
86 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070087public class ContactSaveService extends IntentService {
88 private static final String TAG = "ContactSaveService";
89
Katherine Kuana007e442011-07-07 09:25:34 -070090 /** Set to true in order to view logs on content provider operations */
91 private static final boolean DEBUG = false;
92
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070093 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
94
95 public static final String EXTRA_ACCOUNT_NAME = "accountName";
96 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070097 public static final String EXTRA_DATA_SET = "dataSet";
Marcus Hagerott819214d2016-09-29 14:58:27 -070098 public static final String EXTRA_ACCOUNT = "account";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070099 public static final String EXTRA_CONTENT_VALUES = "contentValues";
100 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
Gary Mai7efa9942016-05-12 11:26:49 -0700101 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
102 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700103
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800104 public static final String ACTION_SAVE_CONTACT = "saveContact";
105 public static final String EXTRA_CONTACT_STATE = "state";
106 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700107 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -0700108 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -0800109 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700110
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800111 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800112 public static final String ACTION_RENAME_GROUP = "renameGroup";
113 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700114 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800115 public static final String EXTRA_GROUP_ID = "groupId";
116 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700117 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
118 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800119
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800120 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800121 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800122 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800123 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800124 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800125 public static final String EXTRA_STARRED_FLAG = "starred";
Marcus Hagerott3bb85142016-07-29 10:46:36 -0700126 public static final String EXTRA_DISPLAY_NAME = "extraDisplayName";
James Laskeye5a140a2016-10-18 15:43:42 -0700127 public static final String EXTRA_DISPLAY_NAME_ARRAY = "extraDisplayNameArray";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800128
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800129 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
130 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
131 public static final String EXTRA_DATA_ID = "dataId";
132
Gary Mai7efa9942016-05-12 11:26:49 -0700133 public static final String ACTION_SPLIT_CONTACT = "splitContact";
134
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800135 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800136 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800137 public static final String EXTRA_CONTACT_ID1 = "contactId1";
138 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800139
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700140 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
141 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
142
143 public static final String ACTION_SET_RINGTONE = "setRingtone";
144 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
145
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700146 public static final String ACTION_UNDO = "undo";
147 public static final String EXTRA_UNDO_ACTION = "undoAction";
148 public static final String EXTRA_UNDO_DATA = "undoData";
149
Marcus Hagerott819214d2016-09-29 14:58:27 -0700150 public static final String ACTION_IMPORT_FROM_SIM = "importFromSim";
151 public static final String EXTRA_SIM_CONTACTS = "simContacts";
152
153 public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
154 public static final String BROADCAST_SIM_IMPORT_COMPLETE = "simImportComplete";
Marcus Hagerott66e8b222016-10-23 15:41:55 -0700155 public static final String EXTRA_CALLBACK_DATA = "extraCallbackData";
Marcus Hagerott819214d2016-09-29 14:58:27 -0700156
157 public static final String EXTRA_RESULT_CODE = "resultCode";
158 public static final String EXTRA_RESULT_COUNT = "count";
159 public static final String EXTRA_OPERATION_REQUESTED_AT_TIME = "requestedTime";
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700160
Gary Mai7efa9942016-05-12 11:26:49 -0700161 public static final int CP2_ERROR = 0;
162 public static final int CONTACTS_LINKED = 1;
163 public static final int CONTACTS_SPLIT = 2;
Gary Mai31d572e2016-06-03 14:04:32 -0700164 public static final int BAD_ARGUMENTS = 3;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700165 public static final int RESULT_UNKNOWN = 0;
166 public static final int RESULT_SUCCESS = 1;
167 public static final int RESULT_FAILURE = 2;
Gary Mai7efa9942016-05-12 11:26:49 -0700168
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700169 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
170 Data.MIMETYPE,
171 Data.IS_PRIMARY,
172 Data.DATA1,
173 Data.DATA2,
174 Data.DATA3,
175 Data.DATA4,
176 Data.DATA5,
177 Data.DATA6,
178 Data.DATA7,
179 Data.DATA8,
180 Data.DATA9,
181 Data.DATA10,
182 Data.DATA11,
183 Data.DATA12,
184 Data.DATA13,
185 Data.DATA14,
186 Data.DATA15
187 );
188
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800189 private static final int PERSIST_TRIES = 3;
190
Walter Jang0653de32015-07-24 12:12:40 -0700191 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
192
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800193 public interface Listener {
194 public void onServiceCompleted(Intent callbackIntent);
195 }
196
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100197 private static final CopyOnWriteArrayList<Listener> sListeners =
198 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800199
200 private Handler mMainHandler;
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700201 private GroupsDao mGroupsDao;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700202 private SimContactDao mSimContactDao;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800203
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700204 public ContactSaveService() {
205 super(TAG);
206 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800207 mMainHandler = new Handler(Looper.getMainLooper());
208 }
209
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700210 @Override
211 public void onCreate() {
212 super.onCreate();
213 mGroupsDao = new GroupsDaoImpl(this);
Marcus Hagerott66e8b222016-10-23 15:41:55 -0700214 mSimContactDao = SimContactDao.create(this);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700215 }
216
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800217 public static void registerListener(Listener listener) {
218 if (!(listener instanceof Activity)) {
219 throw new ClassCastException("Only activities can be registered to"
220 + " receive callback from " + ContactSaveService.class.getName());
221 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100222 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800223 }
224
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700225 public static boolean canUndo(Intent resultIntent) {
226 return resultIntent.hasExtra(EXTRA_UNDO_DATA);
227 }
228
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800229 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100230 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700231 }
232
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800233 /**
234 * Returns true if the ContactSaveService was started successfully and false if an exception
235 * was thrown and a Toast error message was displayed.
236 */
237 public static boolean startService(Context context, Intent intent, int saveMode) {
238 try {
239 context.startService(intent);
240 } catch (Exception exception) {
241 final int resId;
242 switch (saveMode) {
Gary Mai363af602016-09-28 10:01:23 -0700243 case ContactEditorActivity.ContactEditor.SaveMode.SPLIT:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800244 resId = R.string.contactUnlinkErrorToast;
245 break;
Gary Mai363af602016-09-28 10:01:23 -0700246 case ContactEditorActivity.ContactEditor.SaveMode.RELOAD:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800247 resId = R.string.contactJoinErrorToast;
248 break;
Gary Mai363af602016-09-28 10:01:23 -0700249 case ContactEditorActivity.ContactEditor.SaveMode.CLOSE:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800250 resId = R.string.contactSavedErrorToast;
251 break;
252 default:
253 resId = R.string.contactGenericErrorToast;
254 }
255 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
256 return false;
257 }
258 return true;
259 }
260
261 /**
262 * Utility method that starts service and handles exception.
263 */
264 public static void startService(Context context, Intent intent) {
265 try {
266 context.startService(intent);
267 } catch (Exception exception) {
268 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
269 }
270 }
271
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700272 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800273 public Object getSystemService(String name) {
274 Object service = super.getSystemService(name);
275 if (service != null) {
276 return service;
277 }
278
279 return getApplicationContext().getSystemService(name);
280 }
281
282 @Override
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700283 protected void onHandleIntent(Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800284 if (intent == null) {
285 Log.d(TAG, "onHandleIntent: could not handle null intent");
286 return;
287 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700288 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
289 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
290 // TODO: add more specific error string such as "Turn on Contacts
291 // permission to update your contacts"
292 showToast(R.string.contactSavedErrorToast);
293 return;
294 }
295
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700296 // Call an appropriate method. If we're sure it affects how incoming phone calls are
297 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700298 String action = intent.getAction();
299 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
300 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800301 } else if (ACTION_SAVE_CONTACT.equals(action)) {
302 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800303 } else if (ACTION_CREATE_GROUP.equals(action)) {
304 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800305 } else if (ACTION_RENAME_GROUP.equals(action)) {
306 renameGroup(intent);
307 } else if (ACTION_DELETE_GROUP.equals(action)) {
308 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700309 } else if (ACTION_UPDATE_GROUP.equals(action)) {
310 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800311 } else if (ACTION_SET_STARRED.equals(action)) {
312 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800313 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
314 setSuperPrimary(intent);
315 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
316 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800317 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
318 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800319 } else if (ACTION_DELETE_CONTACT.equals(action)) {
320 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700321 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
322 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800323 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
324 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800325 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
326 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700327 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
328 setSendToVoicemail(intent);
329 } else if (ACTION_SET_RINGTONE.equals(action)) {
330 setRingtone(intent);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700331 } else if (ACTION_UNDO.equals(action)) {
332 undo(intent);
Marcus Hagerott819214d2016-09-29 14:58:27 -0700333 } else if (ACTION_IMPORT_FROM_SIM.equals(action)) {
334 importFromSim(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700335 }
336 }
337
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800338 /**
339 * Creates an intent that can be sent to this service to create a new raw contact
340 * using data presented as a set of ContentValues.
341 */
342 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700343 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700344 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800345 Intent serviceIntent = new Intent(
346 context, ContactSaveService.class);
347 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
348 if (account != null) {
349 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
350 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700351 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800352 }
353 serviceIntent.putParcelableArrayListExtra(
354 ContactSaveService.EXTRA_CONTENT_VALUES, values);
355
356 // Callback intent will be invoked by the service once the new contact is
357 // created. The service will put the URI of the new contact as "data" on
358 // the callback intent.
359 Intent callbackIntent = new Intent(context, callbackActivity);
360 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800361 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
362 return serviceIntent;
363 }
364
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700365 private void createRawContact(Intent intent) {
366 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
367 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700368 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700369 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
370 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
371
372 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
373 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
374 .withValue(RawContacts.ACCOUNT_NAME, accountName)
375 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700376 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700377 .build());
378
379 int size = valueList.size();
380 for (int i = 0; i < size; i++) {
381 ContentValues values = valueList.get(i);
382 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
383 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
384 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
385 .withValues(values)
386 .build());
387 }
388
389 ContentResolver resolver = getContentResolver();
390 ContentProviderResult[] results;
391 try {
392 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
393 } catch (Exception e) {
394 throw new RuntimeException("Failed to store new contact", e);
395 }
396
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700397 Uri rawContactUri = results[0].uri;
398 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
399
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800400 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700401 }
402
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700403 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800404 * Creates an intent that can be sent to this service to create a new raw contact
405 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800406 * This variant is more convenient to use when there is only one photo that can
407 * possibly be updated, as in the Contact Details screen.
408 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
409 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800410 */
Maurice Chu851222a2012-06-21 11:43:08 -0700411 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700412 String saveModeExtraKey, int saveMode, boolean isProfile,
413 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700414 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800415 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700416 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800417 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700418 callbackActivity, callbackAction, bundle,
419 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800420 }
421
422 /**
423 * Creates an intent that can be sent to this service to create a new raw contact
424 * using data presented as a set of ContentValues.
425 * This variant is used when multiple contacts' photos may be updated, as in the
426 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700427 *
Josh Garguse692e012012-01-18 14:53:11 -0800428 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700429 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
430 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800431 */
Maurice Chu851222a2012-06-21 11:43:08 -0700432 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700433 String saveModeExtraKey, int saveMode, boolean isProfile,
434 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700435 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000436 Intent serviceIntent = new Intent(
437 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800438 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
439 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700440 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800441 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
442
Josh Garguse692e012012-01-18 14:53:11 -0800443 if (updatedPhotos != null) {
444 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
445 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800446
Josh Garguse5d3f892012-04-11 11:56:15 -0700447 if (callbackActivity != null) {
448 // Callback intent will be invoked by the service once the contact is
449 // saved. The service will put the URI of the new contact as "data" on
450 // the callback intent.
451 Intent callbackIntent = new Intent(context, callbackActivity);
452 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700453 if (joinContactIdExtraKey != null && joinContactId != null) {
454 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
455 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700456 callbackIntent.setAction(callbackAction);
457 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
458 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800459 return serviceIntent;
460 }
461
462 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700463 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700464 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800465 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800466
Jay Shrauner08099782015-03-25 14:17:11 -0700467 if (state == null) {
468 Log.e(TAG, "Invalid arguments for saveContact request");
469 return;
470 }
471
benny.lin3a4e7a22014-01-08 10:58:08 +0800472 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800473 // Trim any empty fields, and RawContacts, before persisting
474 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700475 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800476
477 Uri lookupUri = null;
478
479 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700480
Josh Garguse692e012012-01-18 14:53:11 -0800481 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800482
Josh Gargusef15c8e2012-01-30 16:42:02 -0800483 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
484 long insertedRawContactId = -1;
485
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800486 // Attempt to persist changes
487 int tries = 0;
488 while (tries++ < PERSIST_TRIES) {
489 try {
490 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800491 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
492
493 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
494
495 for (CPOWrapper cpoWrapper : diffWrapper) {
496 diff.add(cpoWrapper.getOperation());
497 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700498
Katherine Kuana007e442011-07-07 09:25:34 -0700499 if (DEBUG) {
500 Log.v(TAG, "Content Provider Operations:");
501 for (ContentProviderOperation operation : diff) {
502 Log.v(TAG, operation.toString());
503 }
504 }
505
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700506 int numberProcessed = 0;
507 boolean batchFailed = false;
508 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
509 while (numberProcessed < diff.size()) {
510 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
511 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700512 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700513 batchFailed = true;
514 break;
515 } else {
516 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700517 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800518 }
519
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700520 if (batchFailed) {
521 // Retry save
522 continue;
523 }
524
Wenyi Wang67addcc2015-11-23 10:07:48 -0800525 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800526 if (rawContactId == -1) {
527 throw new IllegalStateException("Could not determine RawContact ID after save");
528 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800529 // We don't have to check to see if the value is still -1. If we reach here,
530 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800531 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700532 if (isProfile) {
533 // Since the profile supports local raw contacts, which may have been completely
534 // removed if all information was removed, we need to do a special query to
535 // get the lookup URI for the profile contact (if it still exists).
536 Cursor c = resolver.query(Profile.CONTENT_URI,
537 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
538 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800539 if (c == null) {
540 continue;
541 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700542 try {
Erik162b7e32011-09-20 15:23:55 -0700543 if (c.moveToFirst()) {
544 final long contactId = c.getLong(0);
545 final String lookupKey = c.getString(1);
546 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
547 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700548 } finally {
549 c.close();
550 }
551 } else {
552 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
553 rawContactId);
554 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
555 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800556 if (lookupUri != null) {
557 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
558 }
Josh Garguse692e012012-01-18 14:53:11 -0800559
560 // We can change this back to false later, if we fail to save the contact photo.
561 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800562 break;
563
564 } catch (RemoteException e) {
565 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700566 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800567 break;
568
Jay Shrauner57fca182014-01-17 14:20:50 -0800569 } catch (IllegalArgumentException e) {
570 // This is thrown by applyBatch on malformed requests
Walter Jang3a0b4832016-10-12 11:02:54 -0700571 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Jay Shrauner57fca182014-01-17 14:20:50 -0800572 showToast(R.string.contactSavedErrorToast);
573 break;
574
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800575 } catch (OperationApplicationException e) {
576 // Version consistency failed, re-parent change and try again
577 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
578 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
579 boolean first = true;
580 final int count = state.size();
581 for (int i = 0; i < count; i++) {
582 Long rawContactId = state.getRawContactId(i);
583 if (rawContactId != null && rawContactId != -1) {
584 if (!first) {
585 sb.append(',');
586 }
587 sb.append(rawContactId);
588 first = false;
589 }
590 }
591 sb.append(")");
592
593 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800594 throw new IllegalStateException(
595 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800596 }
597
Maurice Chu851222a2012-06-21 11:43:08 -0700598 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700599 isProfile
600 ? RawContactsEntity.PROFILE_CONTENT_URI
601 : RawContactsEntity.CONTENT_URI,
602 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700603 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700604
605 // Update the new state to use profile URIs if appropriate.
606 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700607 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700608 delta.setProfileQueryUri();
609 }
610 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800611 }
612 }
613
Josh Garguse692e012012-01-18 14:53:11 -0800614 // Now save any updated photos. We do this at the end to ensure that
615 // the ContactProvider already knows about newly-created contacts.
616 if (updatedPhotos != null) {
617 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700618 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800619 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800620
621 // If the raw-contact ID is negative, we are saving a new raw-contact;
622 // replace the bogus ID with the new one that we actually saved the contact at.
623 if (rawContactId < 0) {
624 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800625 }
626
Jay Shrauner511561d2015-04-02 10:35:33 -0700627 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700628 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700629 succeeded = false;
630 }
Josh Garguse692e012012-01-18 14:53:11 -0800631 }
632 }
633
Josh Garguse5d3f892012-04-11 11:56:15 -0700634 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
635 if (callbackIntent != null) {
636 if (succeeded) {
637 // Mark the intent to indicate that the save was successful (even if the lookup URI
638 // is now null). For local contacts or the local profile, it's possible that the
639 // save triggered removal of the contact, so no lookup URI would exist..
640 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
641 }
642 callbackIntent.setData(lookupUri);
643 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800644 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800645 }
646
Josh Garguse692e012012-01-18 14:53:11 -0800647 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700648 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
649 * subsets, adds the returned array to "results".
650 *
651 * @return the size of the array, if not null; -1 when the array is null.
652 */
653 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
654 ContentProviderResult[] results, ContentResolver resolver)
655 throws RemoteException, OperationApplicationException {
656 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
657 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
658 subset.addAll(diff.subList(offset, offset + subsetCount));
659 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
660 .AUTHORITY, subset);
661 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
662 return -1;
663 }
664 for (ContentProviderResult c : subsetResult) {
665 results[offset++] = c;
666 }
667 return subsetResult.length;
668 }
669
670 /**
Josh Garguse692e012012-01-18 14:53:11 -0800671 * Save updated photo for the specified raw-contact.
672 * @return true for success, false for failure
673 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800674 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800675 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800676 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
677 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
678
benny.lin3a4e7a22014-01-08 10:58:08 +0800679 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800680 }
681
Josh Gargusef15c8e2012-01-30 16:42:02 -0800682 /**
683 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
684 */
Maurice Chu851222a2012-06-21 11:43:08 -0700685 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800686 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800687 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800688 long existingRawContactId = state.findRawContactId();
689 if (existingRawContactId != -1) {
690 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800691 }
692
Wenyi Wang67addcc2015-11-23 10:07:48 -0800693 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800694 }
695
696 /**
697 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
698 */
699 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800700 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800701 if (results == null) {
702 return -1;
703 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800704 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800705 final int numResults = results.length;
706 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800707 final CPOWrapper cpoWrapper = diffWrapper.get(i);
708 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
709 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
710 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800711 return ContentUris.parseId(results[i].uri);
712 }
713 }
714 return -1;
715 }
716
717 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700718 * Creates an intent that can be sent to this service to create a new group as
719 * well as add new members at the same time.
720 *
721 * @param context of the application
722 * @param account in which the group should be created
723 * @param label is the name of the group (cannot be null)
724 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
725 * should be added to the group
726 * @param callbackActivity is the activity to send the callback intent to
727 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700728 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700729 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700730 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700731 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800732 Intent serviceIntent = new Intent(context, ContactSaveService.class);
733 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
734 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
735 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700736 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800737 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700738 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700739
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800740 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700741 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800742 Intent callbackIntent = new Intent(context, callbackActivity);
743 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700744 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800745
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700746 return serviceIntent;
747 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800748
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800749 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700750 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
751 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
752 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
753 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700754 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800755
Katherine Kuan717e3432011-07-13 17:03:24 -0700756 // Create the new group
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700757 final Uri groupUri = mGroupsDao.create(label,
758 new AccountWithDataSet(accountName, accountType, dataSet));
759 final ContentResolver resolver = getContentResolver();
Katherine Kuan717e3432011-07-13 17:03:24 -0700760
761 // If there's no URI, then the insertion failed. Abort early because group members can't be
762 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800763 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700764 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800765 return;
766 }
767
Katherine Kuan717e3432011-07-13 17:03:24 -0700768 // Add new group members
769 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
770
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700771 ContentValues values = new ContentValues();
Katherine Kuan717e3432011-07-13 17:03:24 -0700772 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
Walter Jang8bac28b2016-08-30 10:34:55 -0700773 // with the way other intent extras that are passed to the
Gary Mai363af602016-09-28 10:01:23 -0700774 // {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800775 values.clear();
776 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
777 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
778
779 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700780 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700781 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800782 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800783 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800784 }
785
786 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800787 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800788 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700789 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700790 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800791 Intent serviceIntent = new Intent(context, ContactSaveService.class);
792 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
793 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
794 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700795
796 // Callback intent will be invoked by the service once the group is renamed.
797 Intent callbackIntent = new Intent(context, callbackActivity);
798 callbackIntent.setAction(callbackAction);
799 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
800
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800801 return serviceIntent;
802 }
803
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800804 private void renameGroup(Intent intent) {
805 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
806 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
807
808 if (groupId == -1) {
809 Log.e(TAG, "Invalid arguments for renameGroup request");
810 return;
811 }
812
813 ContentValues values = new ContentValues();
814 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700815 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
816 getContentResolver().update(groupUri, values, null, null);
817
818 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
819 callbackIntent.setData(groupUri);
820 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800821 }
822
823 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800824 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800825 */
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700826 public static Intent createGroupDeletionIntent(Context context, long groupId) {
827 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800828 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800829 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700830
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800831 return serviceIntent;
832 }
833
834 private void deleteGroup(Intent intent) {
835 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
836 if (groupId == -1) {
837 Log.e(TAG, "Invalid arguments for deleteGroup request");
838 return;
839 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700840 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800841
Marcus Hagerott819214d2016-09-29 14:58:27 -0700842 final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700843 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
844 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
845 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
Walter Jang72f99882016-05-26 09:01:31 -0700846
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700847 mGroupsDao.delete(groupUri);
848
849 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
850 }
851
852 public static Intent createUndoIntent(Context context, Intent resultIntent) {
853 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
854 serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
855 serviceIntent.putExtras(resultIntent);
856 return serviceIntent;
857 }
858
859 private void undo(Intent intent) {
860 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
861 if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
862 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
Walter Jang72f99882016-05-26 09:01:31 -0700863 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800864 }
865
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700866
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800867 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700868 * Creates an intent that can be sent to this service to rename a group as
869 * well as add and remove members from the group.
870 *
871 * @param context of the application
872 * @param groupId of the group that should be modified
873 * @param newLabel is the updated name of the group (can be null if the name
874 * should not be updated)
875 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
876 * should be added to the group
877 * @param rawContactsToRemove is an array of raw contact IDs for contacts
878 * that should be removed from the group
879 * @param callbackActivity is the activity to send the callback intent to
880 * @param callbackAction is the intent action for the callback intent
881 */
882 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
883 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700884 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700885 Intent serviceIntent = new Intent(context, ContactSaveService.class);
886 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
887 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
888 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
889 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
890 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
891 rawContactsToRemove);
892
893 // Callback intent will be invoked by the service once the group is updated
894 Intent callbackIntent = new Intent(context, callbackActivity);
895 callbackIntent.setAction(callbackAction);
896 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
897
898 return serviceIntent;
899 }
900
901 private void updateGroup(Intent intent) {
902 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
903 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
904 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
905 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
906
907 if (groupId == -1) {
908 Log.e(TAG, "Invalid arguments for updateGroup request");
909 return;
910 }
911
912 final ContentResolver resolver = getContentResolver();
913 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
914
915 // Update group name if necessary
916 if (label != null) {
917 ContentValues values = new ContentValues();
918 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700919 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700920 }
921
Katherine Kuan717e3432011-07-13 17:03:24 -0700922 // Add and remove members if necessary
923 addMembersToGroup(resolver, rawContactsToAdd, groupId);
924 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
925
926 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
927 callbackIntent.setData(groupUri);
928 deliverCallback(callbackIntent);
929 }
930
Walter Jang3a0b4832016-10-12 11:02:54 -0700931 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700932 long groupId) {
933 if (rawContactsToAdd == null) {
934 return;
935 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700936 for (long rawContactId : rawContactsToAdd) {
937 try {
938 final ArrayList<ContentProviderOperation> rawContactOperations =
939 new ArrayList<ContentProviderOperation>();
940
941 // Build an assert operation to ensure the contact is not already in the group
942 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
943 .newAssertQuery(Data.CONTENT_URI);
944 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
945 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
946 new String[] { String.valueOf(rawContactId),
947 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
948 assertBuilder.withExpectedCount(0);
949 rawContactOperations.add(assertBuilder.build());
950
951 // Build an insert operation to add the contact to the group
952 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
953 .newInsert(Data.CONTENT_URI);
954 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
955 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
956 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
957 rawContactOperations.add(insertBuilder.build());
958
959 if (DEBUG) {
960 for (ContentProviderOperation operation : rawContactOperations) {
961 Log.v(TAG, operation.toString());
962 }
963 }
964
965 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700966 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800967 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700968 }
969 } catch (RemoteException e) {
970 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700971 FeedbackHelper.sendFeedback(this, TAG,
972 "Problem persisting user edits for raw contact ID " +
973 String.valueOf(rawContactId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700974 } catch (OperationApplicationException e) {
975 // The assert could have failed because the contact is already in the group,
976 // just continue to the next contact
Walter Jang3a0b4832016-10-12 11:02:54 -0700977 FeedbackHelper.sendFeedback(this, TAG,
978 "Assert failed in adding raw contact ID " +
979 String.valueOf(rawContactId) + ". Already exists in group " +
980 String.valueOf(groupId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700981 }
982 }
Katherine Kuan717e3432011-07-13 17:03:24 -0700983 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700984
Daniel Lehmann18958a22012-02-28 17:45:25 -0800985 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -0700986 long groupId) {
987 if (rawContactsToRemove == null) {
988 return;
989 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700990 for (long rawContactId : rawContactsToRemove) {
991 // Apply the delete operation on the data row for the given raw contact's
992 // membership in the given group. If no contact matches the provided selection, then
993 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -0800994 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700995 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
996 new String[] { String.valueOf(rawContactId),
997 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
998 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700999 }
1000
1001 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001002 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001003 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001004 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
1005 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1006 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
1007 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1008 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
1009
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001010 return serviceIntent;
1011 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001012
1013 private void setStarred(Intent intent) {
1014 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1015 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
1016 if (contactUri == null) {
1017 Log.e(TAG, "Invalid arguments for setStarred request");
1018 return;
1019 }
1020
1021 final ContentValues values = new ContentValues(1);
1022 values.put(Contacts.STARRED, value);
1023 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -07001024
1025 // Undemote the contact if necessary
1026 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
1027 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -08001028 if (c == null) {
1029 return;
1030 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001031 try {
1032 if (c.moveToFirst()) {
1033 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -07001034
1035 // Don't bother undemoting if this contact is the user's profile.
1036 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -08001037 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -07001038 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001039 }
1040 } finally {
1041 c.close();
1042 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001043 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001044
1045 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -07001046 * Creates an intent that can be sent to this service to set the redirect to voicemail.
1047 */
1048 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
1049 boolean value) {
1050 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1051 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1052 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1053 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1054
1055 return serviceIntent;
1056 }
1057
1058 private void setSendToVoicemail(Intent intent) {
1059 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1060 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1061 if (contactUri == null) {
1062 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1063 return;
1064 }
1065
1066 final ContentValues values = new ContentValues(1);
1067 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1068 getContentResolver().update(contactUri, values, null, null);
1069 }
1070
1071 /**
1072 * Creates an intent that can be sent to this service to save the contact's ringtone.
1073 */
1074 public static Intent createSetRingtone(Context context, Uri contactUri,
1075 String value) {
1076 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1077 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1078 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1079 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1080
1081 return serviceIntent;
1082 }
1083
1084 private void setRingtone(Intent intent) {
1085 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1086 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1087 if (contactUri == null) {
1088 Log.e(TAG, "Invalid arguments for setRingtone");
1089 return;
1090 }
1091 ContentValues values = new ContentValues(1);
1092 values.put(Contacts.CUSTOM_RINGTONE, value);
1093 getContentResolver().update(contactUri, values, null, null);
1094 }
1095
1096 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001097 * Creates an intent that sets the selected data item as super primary (default)
1098 */
1099 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1100 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1101 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1102 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1103 return serviceIntent;
1104 }
1105
1106 private void setSuperPrimary(Intent intent) {
1107 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1108 if (dataId == -1) {
1109 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1110 return;
1111 }
1112
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001113 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001114 }
1115
1116 /**
1117 * Creates an intent that clears the primary flag of all data items that belong to the same
1118 * raw_contact as the given data item. Will only clear, if the data item was primary before
1119 * this call
1120 */
1121 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1122 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1123 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1124 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1125 return serviceIntent;
1126 }
1127
1128 private void clearPrimary(Intent intent) {
1129 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1130 if (dataId == -1) {
1131 Log.e(TAG, "Invalid arguments for clearPrimary request");
1132 return;
1133 }
1134
1135 // Update the primary values in the data record.
1136 ContentValues values = new ContentValues(1);
1137 values.put(Data.IS_SUPER_PRIMARY, 0);
1138 values.put(Data.IS_PRIMARY, 0);
1139
1140 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1141 values, null, null);
1142 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001143
1144 /**
1145 * Creates an intent that can be sent to this service to delete a contact.
1146 */
1147 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1148 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1149 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1150 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1151 return serviceIntent;
1152 }
1153
Brian Attwelld2962a32015-03-02 14:48:50 -08001154 /**
1155 * Creates an intent that can be sent to this service to delete multiple contacts.
1156 */
1157 public static Intent createDeleteMultipleContactsIntent(Context context,
James Laskeye5a140a2016-10-18 15:43:42 -07001158 long[] contactIds, final String[] names) {
Brian Attwelld2962a32015-03-02 14:48:50 -08001159 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1160 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1161 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
James Laskeye5a140a2016-10-18 15:43:42 -07001162 serviceIntent.putExtra(ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY, names);
Brian Attwelld2962a32015-03-02 14:48:50 -08001163 return serviceIntent;
1164 }
1165
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001166 private void deleteContact(Intent intent) {
1167 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1168 if (contactUri == null) {
1169 Log.e(TAG, "Invalid arguments for deleteContact request");
1170 return;
1171 }
1172
1173 getContentResolver().delete(contactUri, null, null);
1174 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001175
Brian Attwelld2962a32015-03-02 14:48:50 -08001176 private void deleteMultipleContacts(Intent intent) {
1177 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1178 if (contactIds == null) {
1179 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1180 return;
1181 }
1182 for (long contactId : contactIds) {
1183 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1184 getContentResolver().delete(contactUri, null, null);
1185 }
James Laskeye5a140a2016-10-18 15:43:42 -07001186 final String[] names = intent.getStringArrayExtra(
1187 ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY);
1188 final String deleteToastMessage;
1189 if (names.length == 0) {
1190 deleteToastMessage = getResources().getQuantityString(
1191 R.plurals.contacts_deleted_toast, contactIds.length);
1192 } else if (names.length == 1) {
1193 deleteToastMessage = getResources().getString(
1194 R.string.contacts_deleted_one_named_toast, names);
1195 } else if (names.length == 2) {
1196 deleteToastMessage = getResources().getString(
1197 R.string.contacts_deleted_two_named_toast, names);
1198 } else {
1199 deleteToastMessage = getResources().getString(
1200 R.string.contacts_deleted_many_named_toast, names);
1201 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001202 mMainHandler.post(new Runnable() {
1203 @Override
1204 public void run() {
1205 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1206 .show();
1207 }
1208 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001209 }
1210
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001211 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001212 * 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 -07001213 * pieces. This will set the raw contact ids to TYPE_AUTOMATIC for AggregationExceptions so
1214 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001215 */
1216 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1217 ResultReceiver receiver) {
1218 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1219 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1220 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1221 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1222 return serviceIntent;
1223 }
1224
1225 private void splitContact(Intent intent) {
1226 final long rawContactIds[][] = (long[][]) intent
1227 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001228 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Mai7efa9942016-05-12 11:26:49 -07001229 if (rawContactIds == null) {
1230 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001231 if (receiver != null) {
1232 receiver.send(BAD_ARGUMENTS, new Bundle());
1233 }
Gary Mai7efa9942016-05-12 11:26:49 -07001234 return;
1235 }
1236 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1237 final ContentResolver resolver = getContentResolver();
1238 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001239 for (int i = 0; i < rawContactIds.length; i++) {
1240 for (int j = 0; j < rawContactIds.length; j++) {
1241 if (i != j) {
1242 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
1243 if (receiver != null) {
1244 receiver.send(CP2_ERROR, new Bundle());
1245 return;
1246 }
1247 }
1248 }
1249 }
1250 }
1251 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1252 if (receiver != null) {
1253 receiver.send(CP2_ERROR, new Bundle());
1254 }
1255 return;
1256 }
1257 if (receiver != null) {
1258 receiver.send(CONTACTS_SPLIT, new Bundle());
1259 } else {
1260 showToast(R.string.contactUnlinkedToast);
1261 }
1262 }
1263
1264 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001265 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001266 * and {@param rawContactIds2} to {@param operations}.
1267 * @return false if an error occurred, true otherwise.
1268 */
1269 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
1270 long[] rawContactIds1, long[] rawContactIds2) {
1271 if (rawContactIds1 == null || rawContactIds2 == null) {
1272 Log.e(TAG, "Invalid arguments for splitContact request");
1273 return false;
1274 }
1275 // For each pair of raw contacts, insert an aggregation exception
1276 final ContentResolver resolver = getContentResolver();
1277 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1278 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1279 for (int i = 0; i < rawContactIds1.length; i++) {
1280 for (int j = 0; j < rawContactIds2.length; j++) {
1281 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
1282 // Before we get to 500 we need to flush the operations list
1283 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1284 if (!applyOperations(resolver, operations)) {
1285 return false;
1286 }
1287 operations.clear();
1288 }
1289 }
1290 }
1291 return true;
1292 }
1293
1294 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001295 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001296 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001297 */
1298 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001299 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001300 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1301 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1302 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1303 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001304
1305 // Callback intent will be invoked by the service once the contacts are joined.
1306 Intent callbackIntent = new Intent(context, callbackActivity);
1307 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001308 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1309
1310 return serviceIntent;
1311 }
1312
Brian Attwelld3946ca2015-03-03 11:13:49 -08001313 /**
1314 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1315 * No special attention is paid to where the resulting contact's name is taken from.
1316 */
Gary Mai7efa9942016-05-12 11:26:49 -07001317 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1318 ResultReceiver receiver) {
1319 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001320 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1321 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001322 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001323 return serviceIntent;
1324 }
1325
Gary Mai7efa9942016-05-12 11:26:49 -07001326 /**
1327 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1328 * No special attention is paid to where the resulting contact's name is taken from.
1329 */
1330 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1331 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1332 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001333
1334 private interface JoinContactQuery {
1335 String[] PROJECTION = {
1336 RawContacts._ID,
1337 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001338 RawContacts.DISPLAY_NAME_SOURCE,
1339 };
1340
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001341 int _ID = 0;
1342 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001343 int DISPLAY_NAME_SOURCE = 2;
1344 }
1345
1346 private interface ContactEntityQuery {
1347 String[] PROJECTION = {
1348 Contacts.Entity.DATA_ID,
1349 Contacts.Entity.CONTACT_ID,
1350 Contacts.Entity.IS_SUPER_PRIMARY,
1351 };
1352 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1353 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1354 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1355 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1356
1357 int DATA_ID = 0;
1358 int CONTACT_ID = 1;
1359 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001360 }
1361
Brian Attwelld3946ca2015-03-03 11:13:49 -08001362 private void joinSeveralContacts(Intent intent) {
1363 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001364
Gary Mai7efa9942016-05-12 11:26:49 -07001365 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001366
Brian Attwelld3946ca2015-03-03 11:13:49 -08001367 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001368 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1369 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001370 if (rawContactIds == null) {
1371 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001372 if (receiver != null) {
1373 receiver.send(BAD_ARGUMENTS, new Bundle());
1374 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001375 return;
1376 }
1377
Brian Attwelld3946ca2015-03-03 11:13:49 -08001378 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001379 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001380 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1381 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1382 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001383 for (int i = 0; i < rawContactIds.length; i++) {
1384 for (int j = 0; j < rawContactIds.length; j++) {
1385 if (i != j) {
1386 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1387 }
Walter Jang0653de32015-07-24 12:12:40 -07001388 // Before we get to 500 we need to flush the operations list
1389 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001390 if (!applyOperations(resolver, operations)) {
1391 if (receiver != null) {
1392 receiver.send(CP2_ERROR, new Bundle());
1393 }
Walter Jang0653de32015-07-24 12:12:40 -07001394 return;
1395 }
1396 operations.clear();
1397 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001398 }
1399 }
Gary Mai7efa9942016-05-12 11:26:49 -07001400 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1401 if (receiver != null) {
1402 receiver.send(CP2_ERROR, new Bundle());
1403 }
Walter Jang0653de32015-07-24 12:12:40 -07001404 return;
1405 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001406
John Shaoa3c507a2016-09-13 14:26:17 -07001407
1408 final String name = queryNameOfLinkedContacts(contactIds);
1409 if (name != null) {
1410 if (receiver != null) {
1411 final Bundle result = new Bundle();
1412 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1413 result.putString(EXTRA_DISPLAY_NAME, name);
1414 receiver.send(CONTACTS_LINKED, result);
1415 } else {
James Laskeyf62b4882016-10-21 11:36:40 -07001416 if (TextUtils.isEmpty(name)) {
1417 showToast(R.string.contactsJoinedMessage);
1418 } else {
1419 showToast(R.string.contactsJoinedNamedMessage, name);
1420 }
John Shaoa3c507a2016-09-13 14:26:17 -07001421 }
Gary Mai7efa9942016-05-12 11:26:49 -07001422 } else {
John Shaoa3c507a2016-09-13 14:26:17 -07001423 if (receiver != null) {
1424 receiver.send(CP2_ERROR, new Bundle());
1425 }
1426 showToast(R.string.contactJoinErrorToast);
Gary Mai7efa9942016-05-12 11:26:49 -07001427 }
Walter Jang0653de32015-07-24 12:12:40 -07001428 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001429
John Shaoa3c507a2016-09-13 14:26:17 -07001430 /** Get the display name of the top-level contact after the contacts have been linked. */
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001431 private String queryNameOfLinkedContacts(long[] contactIds) {
1432 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1433 final String[] whereArgs = new String[contactIds.length];
1434 for (int i = 0; i < contactIds.length; i++) {
1435 whereArgs[i] = String.valueOf(contactIds[i]);
1436 whereBuilder.append("?,");
1437 }
1438 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1439 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
James Laskeyf62b4882016-10-21 11:36:40 -07001440 new String[]{Contacts._ID, Contacts.DISPLAY_NAME,
1441 Contacts.DISPLAY_NAME_ALTERNATIVE},
John Shaoa3c507a2016-09-13 14:26:17 -07001442 whereBuilder.toString(), whereArgs, null);
1443
1444 String name = null;
James Laskeyf62b4882016-10-21 11:36:40 -07001445 String nameAlt = null;
John Shaoa3c507a2016-09-13 14:26:17 -07001446 long contactId = 0;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001447 try {
1448 if (cursor.moveToFirst()) {
John Shaoa3c507a2016-09-13 14:26:17 -07001449 contactId = cursor.getLong(0);
1450 name = cursor.getString(1);
James Laskeyf62b4882016-10-21 11:36:40 -07001451 nameAlt = cursor.getString(2);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001452 }
John Shaoa3c507a2016-09-13 14:26:17 -07001453 while(cursor.moveToNext()) {
1454 if (cursor.getLong(0) != contactId) {
1455 return null;
1456 }
1457 }
James Laskeyf62b4882016-10-21 11:36:40 -07001458
1459 final String formattedName = ContactDisplayUtils.getPreferredDisplayName(name, nameAlt,
1460 new ContactsPreferences(getApplicationContext()));
1461 return formattedName == null ? "" : formattedName;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001462 } finally {
John Shaoa3c507a2016-09-13 14:26:17 -07001463 if (cursor != null) {
1464 cursor.close();
1465 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001466 }
1467 }
1468
Walter Jang0653de32015-07-24 12:12:40 -07001469 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001470 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001471 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001472 try {
John Shaoa3c507a2016-09-13 14:26:17 -07001473 final ContentProviderResult[] result =
1474 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1475 for (int i = 0; i < result.length; ++i) {
1476 // if no rows were modified in the operation then we count it as fail.
1477 if (result[i].count < 0) {
1478 throw new OperationApplicationException();
1479 }
1480 }
Walter Jang0653de32015-07-24 12:12:40 -07001481 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001482 } catch (RemoteException | OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001483 FeedbackHelper.sendFeedback(this, TAG,
1484 "Failed to apply aggregation exception batch", e);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001485 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001486 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001487 }
1488 }
1489
Brian Attwelld3946ca2015-03-03 11:13:49 -08001490 private void joinContacts(Intent intent) {
1491 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1492 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001493
1494 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001495 // in the join UIs.
1496 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1497 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001498 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001499 return;
1500 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001501
Brian Attwell548f5c62015-01-27 17:46:46 -08001502 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001503
1504 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001505 for (int i = 0; i < rawContactIds.length; i++) {
1506 for (int j = 0; j < rawContactIds.length; j++) {
1507 if (i != j) {
1508 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1509 }
1510 }
1511 }
1512
Brian Attwelld3946ca2015-03-03 11:13:49 -08001513 final ContentResolver resolver = getContentResolver();
1514
Brian Attwell548f5c62015-01-27 17:46:46 -08001515 // Use the name for contactId1 as the name for the newly aggregated contact.
1516 final Uri contactId1Uri = ContentUris.withAppendedId(
1517 Contacts.CONTENT_URI, contactId1);
1518 final Uri entityUri = Uri.withAppendedPath(
1519 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1520 Cursor c = resolver.query(entityUri,
1521 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1522 if (c == null) {
1523 Log.e(TAG, "Unable to open Contacts DB cursor");
1524 showToast(R.string.contactSavedErrorToast);
1525 return;
1526 }
1527 long dataIdToAddSuperPrimary = -1;
1528 try {
1529 if (c.moveToFirst()) {
1530 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1531 }
1532 } finally {
1533 c.close();
1534 }
1535
1536 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1537 // display name does not change as a result of the join.
1538 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001539 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001540 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1541 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1542 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001543 operations.add(builder.build());
1544 }
1545
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001546 // Apply all aggregation exceptions as one batch
John Shaoa3c507a2016-09-13 14:26:17 -07001547 final boolean success = applyOperations(resolver, operations);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001548
John Shaoa3c507a2016-09-13 14:26:17 -07001549 final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2});
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001550 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
John Shaoa3c507a2016-09-13 14:26:17 -07001551 if (success && name != null) {
James Laskeyf62b4882016-10-21 11:36:40 -07001552 if (TextUtils.isEmpty(name)) {
1553 showToast(R.string.contactsJoinedMessage);
1554 } else {
1555 showToast(R.string.contactsJoinedNamedMessage, name);
1556 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001557 Uri uri = RawContacts.getContactLookupUri(resolver,
1558 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1559 callbackIntent.setData(uri);
1560 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001561 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001562 }
1563
Gary Mai7efa9942016-05-12 11:26:49 -07001564 /**
1565 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1566 * array of the return value holds an array of raw contact ids for one contactId.
1567 * @param contactIds
1568 * @return
1569 */
1570 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1571 final long[][] rawContactIds = new long[contactIds.length][];
1572 for (int i = 0; i < contactIds.length; i++) {
1573 rawContactIds[i] = getRawContactIds(contactIds[i]);
1574 }
1575 return rawContactIds;
1576 }
1577
1578 /**
1579 * Gets the raw contact ids associated with {@param contactId}.
1580 * @param contactId
1581 * @return Array of raw contact ids.
1582 */
1583 private long[] getRawContactIds(long contactId) {
1584 final ContentResolver resolver = getContentResolver();
1585 long rawContactIds[];
1586
1587 final StringBuilder queryBuilder = new StringBuilder();
1588 queryBuilder.append(RawContacts.CONTACT_ID)
1589 .append("=")
1590 .append(String.valueOf(contactId));
1591
1592 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1593 JoinContactQuery.PROJECTION,
1594 queryBuilder.toString(),
1595 null, null);
1596 if (c == null) {
1597 Log.e(TAG, "Unable to open Contacts DB cursor");
1598 return null;
1599 }
1600 try {
1601 rawContactIds = new long[c.getCount()];
1602 for (int i = 0; i < rawContactIds.length; i++) {
1603 c.moveToPosition(i);
1604 final long rawContactId = c.getLong(JoinContactQuery._ID);
1605 rawContactIds[i] = rawContactId;
1606 }
1607 } finally {
1608 c.close();
1609 }
1610 return rawContactIds;
1611 }
1612
Brian Attwelld3946ca2015-03-03 11:13:49 -08001613 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1614 if (contactIds == null) {
1615 return null;
1616 }
1617
Brian Attwell548f5c62015-01-27 17:46:46 -08001618 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001619
1620 final StringBuilder queryBuilder = new StringBuilder();
1621 final String stringContactIds[] = new String[contactIds.length];
1622 for (int i = 0; i < contactIds.length; i++) {
1623 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1624 stringContactIds[i] = String.valueOf(contactIds[i]);
1625 if (contactIds[i] == -1) {
1626 return null;
1627 }
1628 if (i == contactIds.length -1) {
1629 break;
1630 }
1631 queryBuilder.append(" OR ");
1632 }
1633
Brian Attwell548f5c62015-01-27 17:46:46 -08001634 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1635 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001636 queryBuilder.toString(),
1637 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001638 if (c == null) {
1639 Log.e(TAG, "Unable to open Contacts DB cursor");
1640 showToast(R.string.contactSavedErrorToast);
1641 return null;
1642 }
Gary Mai7efa9942016-05-12 11:26:49 -07001643 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001644 try {
1645 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001646 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001647 return null;
1648 }
1649 rawContactIds = new long[c.getCount()];
1650 for (int i = 0; i < rawContactIds.length; i++) {
1651 c.moveToPosition(i);
1652 long rawContactId = c.getLong(JoinContactQuery._ID);
1653 rawContactIds[i] = rawContactId;
1654 }
1655 } finally {
1656 c.close();
1657 }
1658 return rawContactIds;
1659 }
1660
Brian Attwelld3946ca2015-03-03 11:13:49 -08001661 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1662 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1663 }
1664
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001665 /**
1666 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1667 */
1668 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1669 long rawContactId1, long rawContactId2) {
1670 Builder builder =
1671 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1672 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1673 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1674 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1675 operations.add(builder.build());
1676 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001677
1678 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001679 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} ContentProviderOperation.
Gary Mai7efa9942016-05-12 11:26:49 -07001680 */
1681 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
1682 long rawContactId1, long rawContactId2) {
1683 final Builder builder =
1684 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Mai53fe0d22016-07-26 17:23:53 -07001685 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001686 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1687 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1688 operations.add(builder.build());
1689 }
1690
Marcus Hagerott66e8b222016-10-23 15:41:55 -07001691 public static Intent createImportFromSimIntent(@NonNull Context context,
1692 @NonNull ArrayList<SimContact> contacts, @NonNull AccountWithDataSet targetAccount,
1693 @Nullable Bundle callbackData) {
Marcus Hagerott819214d2016-09-29 14:58:27 -07001694 return new Intent(context, ContactSaveService.class)
1695 .setAction(ACTION_IMPORT_FROM_SIM)
1696 .putExtra(EXTRA_SIM_CONTACTS, contacts)
Marcus Hagerott66e8b222016-10-23 15:41:55 -07001697 .putExtra(EXTRA_ACCOUNT, targetAccount)
1698 .putExtra(EXTRA_CALLBACK_DATA, callbackData);
Marcus Hagerott819214d2016-09-29 14:58:27 -07001699 }
1700
1701 private void importFromSim(Intent intent) {
1702 final Intent result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
1703 .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, System.currentTimeMillis());
1704 try {
1705 final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
1706 final ArrayList<SimContact> contacts =
1707 intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
1708 mSimContactDao.importContacts(contacts, targetAccount);
1709 // notify success
1710 LocalBroadcastManager.getInstance(this).sendBroadcast(result
1711 .putExtra(EXTRA_RESULT_COUNT, contacts.size())
Marcus Hagerott66e8b222016-10-23 15:41:55 -07001712 .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS)
1713 .putExtra(EXTRA_CALLBACK_DATA, intent.getBundleExtra(EXTRA_CALLBACK_DATA)));
Marcus Hagerott819214d2016-09-29 14:58:27 -07001714 if (Log.isLoggable(TAG, Log.DEBUG)) {
1715 Log.d(TAG, "importFromSim completed successfully");
1716 }
1717 } catch (RemoteException|OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001718 FeedbackHelper.sendFeedback(this, TAG, "Failed to import contacts from SIM card", e);
Marcus Hagerott819214d2016-09-29 14:58:27 -07001719 LocalBroadcastManager.getInstance(this).sendBroadcast(result
1720 .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE));
1721 }
1722 }
1723
Gary Mai7efa9942016-05-12 11:26:49 -07001724 /**
James Laskeyf62b4882016-10-21 11:36:40 -07001725 * Shows a toast on the UI thread by formatting messageId using args.
1726 * @param messageId id of message string
1727 * @param args args to format string
1728 */
1729 private void showToast(final int messageId, final Object... args) {
1730 final String message = getResources().getString(messageId, args);
1731 mMainHandler.post(new Runnable() {
1732 @Override
1733 public void run() {
1734 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1735 }
1736 });
1737 }
1738
1739
1740 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001741 * Shows a toast on the UI thread.
1742 */
1743 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001744 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001745
1746 @Override
1747 public void run() {
1748 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1749 }
1750 });
1751 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001752
1753 private void deliverCallback(final Intent callbackIntent) {
1754 mMainHandler.post(new Runnable() {
1755
1756 @Override
1757 public void run() {
1758 deliverCallbackOnUiThread(callbackIntent);
1759 }
1760 });
1761 }
1762
1763 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1764 // TODO: this assumes that if there are multiple instances of the same
1765 // activity registered, the last one registered is the one waiting for
1766 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001767 for (Listener listener : sListeners) {
1768 if (callbackIntent.getComponent().equals(
1769 ((Activity) listener).getIntent().getComponent())) {
1770 listener.onServiceCompleted(callbackIntent);
1771 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001772 }
1773 }
1774 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001775
1776 public interface GroupsDao {
1777 Uri create(String title, AccountWithDataSet account);
1778 int delete(Uri groupUri);
1779 Bundle captureDeletionUndoData(Uri groupUri);
1780 Uri undoDeletion(Bundle undoData);
1781 }
1782
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001783 public static class GroupsDaoImpl implements GroupsDao {
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001784 public static final String KEY_GROUP_DATA = "groupData";
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001785 public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
1786
1787 private static final String TAG = "GroupsDao";
1788 private final Context context;
1789 private final ContentResolver contentResolver;
1790
1791 public GroupsDaoImpl(Context context) {
1792 this(context, context.getContentResolver());
1793 }
1794
1795 public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
1796 this.context = context;
1797 this.contentResolver = contentResolver;
1798 }
1799
1800 public Bundle captureDeletionUndoData(Uri groupUri) {
1801 final long groupId = ContentUris.parseId(groupUri);
1802 final Bundle result = new Bundle();
1803
1804 final Cursor cursor = contentResolver.query(groupUri,
1805 new String[]{
1806 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
1807 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
1808 Groups.SHOULD_SYNC
1809 },
1810 Groups.DELETED + "=?", new String[] { "0" }, null);
1811 try {
1812 if (cursor.moveToFirst()) {
1813 final ContentValues groupValues = new ContentValues();
1814 DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
1815 result.putParcelable(KEY_GROUP_DATA, groupValues);
1816 } else {
1817 // Group doesn't exist.
1818 return result;
1819 }
1820 } finally {
1821 cursor.close();
1822 }
1823
1824 final Cursor membersCursor = contentResolver.query(
1825 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
1826 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1827 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
1828 final long[] memberIds = new long[membersCursor.getCount()];
1829 int i = 0;
1830 while (membersCursor.moveToNext()) {
1831 memberIds[i++] = membersCursor.getLong(0);
1832 }
1833 result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
1834 return result;
1835 }
1836
1837 public Uri undoDeletion(Bundle deletedGroupData) {
1838 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
1839 if (groupData == null) {
1840 return null;
1841 }
1842 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
1843 final long groupId = ContentUris.parseId(groupUri);
1844
1845 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
1846 if (memberIds == null) {
1847 return groupUri;
1848 }
1849 final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
1850 for (int i = 0; i < memberIds.length; i++) {
1851 memberInsertions[i] = new ContentValues();
1852 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
1853 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1854 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
1855 }
1856 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
1857 if (inserted != memberIds.length) {
1858 Log.e(TAG, "Could not recover some members for group deletion undo");
1859 }
1860
1861 return groupUri;
1862 }
1863
1864 public Uri create(String title, AccountWithDataSet account) {
1865 final ContentValues values = new ContentValues();
1866 values.put(Groups.TITLE, title);
1867 values.put(Groups.ACCOUNT_NAME, account.name);
1868 values.put(Groups.ACCOUNT_TYPE, account.type);
1869 values.put(Groups.DATA_SET, account.dataSet);
1870 return contentResolver.insert(Groups.CONTENT_URI, values);
1871 }
1872
1873 public int delete(Uri groupUri) {
1874 return contentResolver.delete(groupUri, null, null);
1875 }
1876 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001877}