blob: 30e4f820dffd99d2d25bbbb280be59da1947be10 [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
Gary Mai0a49afa2016-12-05 15:53:58 -080019import static android.Manifest.permission.WRITE_CONTACTS;
20
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080021import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070022import android.app.IntentService;
23import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080024import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070025import android.content.ContentProviderResult;
26import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080027import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070028import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080029import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070030import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080031import android.content.OperationApplicationException;
32import android.database.Cursor;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070033import android.database.DatabaseUtils;
John Shaod32c4e92021-10-12 20:53:55 +000034import android.icu.text.MessageFormat;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070035import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080036import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080037import android.os.Handler;
38import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080039import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080040import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070041import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080042import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080043import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080044import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080045import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070046import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080047import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070048import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070049import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070050import android.provider.ContactsContract.RawContactsEntity;
Aravind Sreekumar71212852018-04-06 15:47:45 -070051import androidx.localbroadcastmanager.content.LocalBroadcastManager;
Gary Mai7efa9942016-05-12 11:26:49 -070052import android.support.v4.os.ResultReceiver;
James Laskeyf62b4882016-10-21 11:36:40 -070053import android.text.TextUtils;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070054import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080055import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070056
Gary Mai363af602016-09-28 10:01:23 -070057import com.android.contacts.activities.ContactEditorActivity;
Gary Mai69c182a2016-12-05 13:07:03 -080058import com.android.contacts.compat.CompatUtils;
Gary Mai0a49afa2016-12-05 15:53:58 -080059import com.android.contacts.compat.PinnedPositionsCompat;
Gary Mai69c182a2016-12-05 13:07:03 -080060import com.android.contacts.database.ContactUpdateUtils;
61import com.android.contacts.database.SimContactDao;
62import com.android.contacts.model.AccountTypeManager;
63import com.android.contacts.model.CPOWrapper;
64import com.android.contacts.model.RawContactDelta;
65import com.android.contacts.model.RawContactDeltaList;
66import com.android.contacts.model.RawContactModifier;
67import com.android.contacts.model.account.AccountWithDataSet;
68import com.android.contacts.preference.ContactsPreferences;
69import com.android.contacts.util.ContactDisplayUtils;
Yorke Lee637a38e2013-09-14 08:36:33 -070070import com.android.contacts.util.ContactPhotoUtils;
Gary Mai0a49afa2016-12-05 15:53:58 -080071import com.android.contacts.util.PermissionsUtil;
Walter Jang3a0b4832016-10-12 11:02:54 -070072import com.android.contactsbind.FeedbackHelper;
Gary Mai0a49afa2016-12-05 15:53:58 -080073
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;
Marcus Hagerott7333c372016-11-07 09:40:20 -080078import java.util.Collection;
John Shaod32c4e92021-10-12 20:53:55 +000079import java.util.HashMap;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080080import java.util.HashSet;
81import java.util.List;
John Shaod32c4e92021-10-12 20:53:55 +000082import java.util.Locale;
83import java.util.Map;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080084import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070085
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080086/**
87 * A service responsible for saving changes to the content provider.
88 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070089public class ContactSaveService extends IntentService {
90 private static final String TAG = "ContactSaveService";
91
Katherine Kuana007e442011-07-07 09:25:34 -070092 /** Set to true in order to view logs on content provider operations */
93 private static final boolean DEBUG = false;
94
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070095 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
96
97 public static final String EXTRA_ACCOUNT_NAME = "accountName";
98 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070099 public static final String EXTRA_DATA_SET = "dataSet";
Marcus Hagerott819214d2016-09-29 14:58:27 -0700100 public static final String EXTRA_ACCOUNT = "account";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700101 public static final String EXTRA_CONTENT_VALUES = "contentValues";
102 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
Gary Mai7efa9942016-05-12 11:26:49 -0700103 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
104 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700105
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800106 public static final String ACTION_SAVE_CONTACT = "saveContact";
107 public static final String EXTRA_CONTACT_STATE = "state";
108 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700109 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -0700110 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -0800111 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700112
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800113 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800114 public static final String ACTION_RENAME_GROUP = "renameGroup";
115 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700116 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800117 public static final String EXTRA_GROUP_ID = "groupId";
118 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700119 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
120 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800121
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800122 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800123 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800124 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800125 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800126 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800127 public static final String EXTRA_STARRED_FLAG = "starred";
Marcus Hagerott3bb85142016-07-29 10:46:36 -0700128 public static final String EXTRA_DISPLAY_NAME = "extraDisplayName";
James Laskeye5a140a2016-10-18 15:43:42 -0700129 public static final String EXTRA_DISPLAY_NAME_ARRAY = "extraDisplayNameArray";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800130
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800131 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
132 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
133 public static final String EXTRA_DATA_ID = "dataId";
134
Gary Mai7efa9942016-05-12 11:26:49 -0700135 public static final String ACTION_SPLIT_CONTACT = "splitContact";
Gary Maib9065dd2016-11-08 10:49:00 -0800136 public static final String EXTRA_HARD_SPLIT = "extraHardSplit";
Gary Mai7efa9942016-05-12 11:26:49 -0700137
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800138 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800139 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800140 public static final String EXTRA_CONTACT_ID1 = "contactId1";
141 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800142
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700143 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
144 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
145
146 public static final String ACTION_SET_RINGTONE = "setRingtone";
147 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
148
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700149 public static final String ACTION_UNDO = "undo";
150 public static final String EXTRA_UNDO_ACTION = "undoAction";
151 public static final String EXTRA_UNDO_DATA = "undoData";
152
Marcus Hagerott7333c372016-11-07 09:40:20 -0800153 // For debugging and testing what happens when requests are queued up.
154 public static final String ACTION_SLEEP = "sleep";
155 public static final String EXTRA_SLEEP_DURATION = "sleepDuration";
Marcus Hagerott819214d2016-09-29 14:58:27 -0700156
157 public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
Gary Maib9065dd2016-11-08 10:49:00 -0800158 public static final String BROADCAST_LINK_COMPLETE = "linkComplete";
159 public static final String BROADCAST_UNLINK_COMPLETE = "unlinkComplete";
Marcus Hagerott7333c372016-11-07 09:40:20 -0800160
161 public static final String BROADCAST_SERVICE_STATE_CHANGED = "serviceStateChanged";
Marcus Hagerott819214d2016-09-29 14:58:27 -0700162
163 public static final String EXTRA_RESULT_CODE = "resultCode";
164 public static final String EXTRA_RESULT_COUNT = "count";
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700165
Gary Mai7efa9942016-05-12 11:26:49 -0700166 public static final int CP2_ERROR = 0;
167 public static final int CONTACTS_LINKED = 1;
168 public static final int CONTACTS_SPLIT = 2;
Gary Mai31d572e2016-06-03 14:04:32 -0700169 public static final int BAD_ARGUMENTS = 3;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700170 public static final int RESULT_UNKNOWN = 0;
171 public static final int RESULT_SUCCESS = 1;
172 public static final int RESULT_FAILURE = 2;
Gary Mai7efa9942016-05-12 11:26:49 -0700173
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700174 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
175 Data.MIMETYPE,
176 Data.IS_PRIMARY,
177 Data.DATA1,
178 Data.DATA2,
179 Data.DATA3,
180 Data.DATA4,
181 Data.DATA5,
182 Data.DATA6,
183 Data.DATA7,
184 Data.DATA8,
185 Data.DATA9,
186 Data.DATA10,
187 Data.DATA11,
188 Data.DATA12,
189 Data.DATA13,
190 Data.DATA14,
191 Data.DATA15
192 );
193
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800194 private static final int PERSIST_TRIES = 3;
195
Walter Jang0653de32015-07-24 12:12:40 -0700196 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
197
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800198 public interface Listener {
199 public void onServiceCompleted(Intent callbackIntent);
200 }
201
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100202 private static final CopyOnWriteArrayList<Listener> sListeners =
203 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800204
Marcus Hagerott7333c372016-11-07 09:40:20 -0800205 // Holds the current state of the service
206 private static final State sState = new State();
207
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800208 private Handler mMainHandler;
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700209 private GroupsDao mGroupsDao;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700210 private SimContactDao mSimContactDao;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800211
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700212 public ContactSaveService() {
213 super(TAG);
214 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800215 mMainHandler = new Handler(Looper.getMainLooper());
216 }
217
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700218 @Override
219 public void onCreate() {
220 super.onCreate();
221 mGroupsDao = new GroupsDaoImpl(this);
Marcus Hagerott66e8b222016-10-23 15:41:55 -0700222 mSimContactDao = SimContactDao.create(this);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700223 }
224
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800225 public static void registerListener(Listener listener) {
226 if (!(listener instanceof Activity)) {
227 throw new ClassCastException("Only activities can be registered to"
228 + " receive callback from " + ContactSaveService.class.getName());
229 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100230 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800231 }
232
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700233 public static boolean canUndo(Intent resultIntent) {
234 return resultIntent.hasExtra(EXTRA_UNDO_DATA);
235 }
236
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800237 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100238 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700239 }
240
Marcus Hagerott7333c372016-11-07 09:40:20 -0800241 public static State getState() {
242 return sState;
243 }
244
245 private void notifyStateChanged() {
246 LocalBroadcastManager.getInstance(this)
247 .sendBroadcast(new Intent(BROADCAST_SERVICE_STATE_CHANGED));
248 }
249
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800250 /**
251 * Returns true if the ContactSaveService was started successfully and false if an exception
252 * was thrown and a Toast error message was displayed.
253 */
254 public static boolean startService(Context context, Intent intent, int saveMode) {
255 try {
256 context.startService(intent);
257 } catch (Exception exception) {
258 final int resId;
259 switch (saveMode) {
Gary Mai363af602016-09-28 10:01:23 -0700260 case ContactEditorActivity.ContactEditor.SaveMode.SPLIT:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800261 resId = R.string.contactUnlinkErrorToast;
262 break;
Gary Mai363af602016-09-28 10:01:23 -0700263 case ContactEditorActivity.ContactEditor.SaveMode.RELOAD:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800264 resId = R.string.contactJoinErrorToast;
265 break;
Gary Mai363af602016-09-28 10:01:23 -0700266 case ContactEditorActivity.ContactEditor.SaveMode.CLOSE:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800267 resId = R.string.contactSavedErrorToast;
268 break;
269 default:
270 resId = R.string.contactGenericErrorToast;
271 }
272 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
273 return false;
274 }
275 return true;
276 }
277
278 /**
279 * Utility method that starts service and handles exception.
280 */
281 public static void startService(Context context, Intent intent) {
282 try {
283 context.startService(intent);
284 } catch (Exception exception) {
285 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
286 }
287 }
288
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700289 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800290 public Object getSystemService(String name) {
291 Object service = super.getSystemService(name);
292 if (service != null) {
293 return service;
294 }
295
296 return getApplicationContext().getSystemService(name);
297 }
298
Marcus Hagerott7333c372016-11-07 09:40:20 -0800299 // Parent classes Javadoc says not to override this method but we're doing it just to update
300 // our state which should be OK since we're still doing the work in onHandleIntent
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800301 @Override
Marcus Hagerott7333c372016-11-07 09:40:20 -0800302 public int onStartCommand(Intent intent, int flags, int startId) {
303 sState.onStart(intent);
304 notifyStateChanged();
305 return super.onStartCommand(intent, flags, startId);
306 }
307
308 @Override
309 protected void onHandleIntent(final Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800310 if (intent == null) {
Wenyi Wang57a0e982017-03-24 16:02:44 -0700311 if (Log.isLoggable(TAG, Log.DEBUG)) {
312 Log.d(TAG, "onHandleIntent: could not handle null intent");
313 }
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800314 return;
315 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700316 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
317 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
318 // TODO: add more specific error string such as "Turn on Contacts
319 // permission to update your contacts"
320 showToast(R.string.contactSavedErrorToast);
321 return;
322 }
323
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700324 // Call an appropriate method. If we're sure it affects how incoming phone calls are
325 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700326 String action = intent.getAction();
327 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
328 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800329 } else if (ACTION_SAVE_CONTACT.equals(action)) {
330 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800331 } else if (ACTION_CREATE_GROUP.equals(action)) {
332 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800333 } else if (ACTION_RENAME_GROUP.equals(action)) {
334 renameGroup(intent);
335 } else if (ACTION_DELETE_GROUP.equals(action)) {
336 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700337 } else if (ACTION_UPDATE_GROUP.equals(action)) {
338 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800339 } else if (ACTION_SET_STARRED.equals(action)) {
340 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800341 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
342 setSuperPrimary(intent);
343 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
344 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800345 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
346 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800347 } else if (ACTION_DELETE_CONTACT.equals(action)) {
348 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700349 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
350 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800351 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
352 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800353 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
354 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700355 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
356 setSendToVoicemail(intent);
357 } else if (ACTION_SET_RINGTONE.equals(action)) {
358 setRingtone(intent);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700359 } else if (ACTION_UNDO.equals(action)) {
360 undo(intent);
Marcus Hagerott7333c372016-11-07 09:40:20 -0800361 } else if (ACTION_SLEEP.equals(action)) {
362 sleepForDebugging(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700363 }
Marcus Hagerott7333c372016-11-07 09:40:20 -0800364
365 sState.onFinish(intent);
366 notifyStateChanged();
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700367 }
368
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800369 /**
370 * Creates an intent that can be sent to this service to create a new raw contact
371 * using data presented as a set of ContentValues.
372 */
373 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700374 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700375 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800376 Intent serviceIntent = new Intent(
377 context, ContactSaveService.class);
378 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
379 if (account != null) {
380 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
381 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700382 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800383 }
384 serviceIntent.putParcelableArrayListExtra(
385 ContactSaveService.EXTRA_CONTENT_VALUES, values);
386
387 // Callback intent will be invoked by the service once the new contact is
388 // created. The service will put the URI of the new contact as "data" on
389 // the callback intent.
390 Intent callbackIntent = new Intent(context, callbackActivity);
391 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800392 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
393 return serviceIntent;
394 }
395
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700396 private void createRawContact(Intent intent) {
397 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
398 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700399 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700400 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
401 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
402
403 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
404 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
405 .withValue(RawContacts.ACCOUNT_NAME, accountName)
406 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700407 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700408 .build());
409
410 int size = valueList.size();
411 for (int i = 0; i < size; i++) {
412 ContentValues values = valueList.get(i);
413 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
414 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
415 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
416 .withValues(values)
417 .build());
418 }
419
420 ContentResolver resolver = getContentResolver();
421 ContentProviderResult[] results;
422 try {
423 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
424 } catch (Exception e) {
425 throw new RuntimeException("Failed to store new contact", e);
426 }
427
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700428 Uri rawContactUri = results[0].uri;
429 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
430
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800431 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700432 }
433
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700434 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800435 * Creates an intent that can be sent to this service to create a new raw contact
436 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800437 * This variant is more convenient to use when there is only one photo that can
438 * possibly be updated, as in the Contact Details screen.
439 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
440 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800441 */
Maurice Chu851222a2012-06-21 11:43:08 -0700442 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700443 String saveModeExtraKey, int saveMode, boolean isProfile,
444 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700445 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800446 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700447 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800448 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700449 callbackActivity, callbackAction, bundle,
450 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800451 }
452
453 /**
454 * Creates an intent that can be sent to this service to create a new raw contact
455 * using data presented as a set of ContentValues.
456 * This variant is used when multiple contacts' photos may be updated, as in the
457 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700458 *
Josh Garguse692e012012-01-18 14:53:11 -0800459 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700460 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
461 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800462 */
Maurice Chu851222a2012-06-21 11:43:08 -0700463 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700464 String saveModeExtraKey, int saveMode, boolean isProfile,
465 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700466 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000467 Intent serviceIntent = new Intent(
468 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800469 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
470 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700471 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800472 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
473
Josh Garguse692e012012-01-18 14:53:11 -0800474 if (updatedPhotos != null) {
475 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
476 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800477
Josh Garguse5d3f892012-04-11 11:56:15 -0700478 if (callbackActivity != null) {
479 // Callback intent will be invoked by the service once the contact is
480 // saved. The service will put the URI of the new contact as "data" on
481 // the callback intent.
482 Intent callbackIntent = new Intent(context, callbackActivity);
483 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700484 if (joinContactIdExtraKey != null && joinContactId != null) {
485 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
486 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700487 callbackIntent.setAction(callbackAction);
488 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
489 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800490 return serviceIntent;
491 }
492
493 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700494 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700495 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800496 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800497
Jay Shrauner08099782015-03-25 14:17:11 -0700498 if (state == null) {
499 Log.e(TAG, "Invalid arguments for saveContact request");
500 return;
501 }
502
benny.lin3a4e7a22014-01-08 10:58:08 +0800503 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800504 // Trim any empty fields, and RawContacts, before persisting
505 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700506 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800507
508 Uri lookupUri = null;
509
510 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700511
Josh Garguse692e012012-01-18 14:53:11 -0800512 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800513
Josh Gargusef15c8e2012-01-30 16:42:02 -0800514 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
515 long insertedRawContactId = -1;
516
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800517 // Attempt to persist changes
518 int tries = 0;
519 while (tries++ < PERSIST_TRIES) {
520 try {
521 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800522 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
523
524 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
525
526 for (CPOWrapper cpoWrapper : diffWrapper) {
527 diff.add(cpoWrapper.getOperation());
528 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700529
Katherine Kuana007e442011-07-07 09:25:34 -0700530 if (DEBUG) {
531 Log.v(TAG, "Content Provider Operations:");
532 for (ContentProviderOperation operation : diff) {
533 Log.v(TAG, operation.toString());
534 }
535 }
536
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700537 int numberProcessed = 0;
538 boolean batchFailed = false;
539 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
540 while (numberProcessed < diff.size()) {
541 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
542 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700543 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700544 batchFailed = true;
545 break;
546 } else {
547 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700548 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800549 }
550
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700551 if (batchFailed) {
552 // Retry save
553 continue;
554 }
555
Wenyi Wang67addcc2015-11-23 10:07:48 -0800556 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800557 if (rawContactId == -1) {
558 throw new IllegalStateException("Could not determine RawContact ID after save");
559 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800560 // We don't have to check to see if the value is still -1. If we reach here,
561 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800562 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700563 if (isProfile) {
564 // Since the profile supports local raw contacts, which may have been completely
565 // removed if all information was removed, we need to do a special query to
566 // get the lookup URI for the profile contact (if it still exists).
567 Cursor c = resolver.query(Profile.CONTENT_URI,
568 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
569 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800570 if (c == null) {
571 continue;
572 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700573 try {
Erik162b7e32011-09-20 15:23:55 -0700574 if (c.moveToFirst()) {
575 final long contactId = c.getLong(0);
576 final String lookupKey = c.getString(1);
577 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
578 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700579 } finally {
580 c.close();
581 }
582 } else {
583 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
584 rawContactId);
585 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
586 }
Wenyi Wang57a0e982017-03-24 16:02:44 -0700587 if (lookupUri != null && Log.isLoggable(TAG, Log.VERBOSE)) {
Jay Shraunere320c0b2015-03-05 12:45:18 -0800588 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
589 }
Josh Garguse692e012012-01-18 14:53:11 -0800590
591 // We can change this back to false later, if we fail to save the contact photo.
592 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800593 break;
594
595 } catch (RemoteException e) {
596 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700597 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800598 break;
599
Jay Shrauner57fca182014-01-17 14:20:50 -0800600 } catch (IllegalArgumentException e) {
601 // This is thrown by applyBatch on malformed requests
Walter Jang3a0b4832016-10-12 11:02:54 -0700602 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Jay Shrauner57fca182014-01-17 14:20:50 -0800603 showToast(R.string.contactSavedErrorToast);
604 break;
605
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800606 } catch (OperationApplicationException e) {
607 // Version consistency failed, re-parent change and try again
608 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
609 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
610 boolean first = true;
611 final int count = state.size();
612 for (int i = 0; i < count; i++) {
613 Long rawContactId = state.getRawContactId(i);
614 if (rawContactId != null && rawContactId != -1) {
615 if (!first) {
616 sb.append(',');
617 }
618 sb.append(rawContactId);
619 first = false;
620 }
621 }
622 sb.append(")");
623
624 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800625 throw new IllegalStateException(
626 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800627 }
628
Maurice Chu851222a2012-06-21 11:43:08 -0700629 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700630 isProfile
631 ? RawContactsEntity.PROFILE_CONTENT_URI
632 : RawContactsEntity.CONTENT_URI,
633 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700634 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700635
636 // Update the new state to use profile URIs if appropriate.
637 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700638 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700639 delta.setProfileQueryUri();
640 }
641 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800642 }
643 }
644
Josh Garguse692e012012-01-18 14:53:11 -0800645 // Now save any updated photos. We do this at the end to ensure that
646 // the ContactProvider already knows about newly-created contacts.
647 if (updatedPhotos != null) {
648 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700649 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800650 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800651
652 // If the raw-contact ID is negative, we are saving a new raw-contact;
653 // replace the bogus ID with the new one that we actually saved the contact at.
654 if (rawContactId < 0) {
655 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800656 }
657
Jay Shrauner511561d2015-04-02 10:35:33 -0700658 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700659 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700660 succeeded = false;
661 }
Josh Garguse692e012012-01-18 14:53:11 -0800662 }
663 }
664
Josh Garguse5d3f892012-04-11 11:56:15 -0700665 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
666 if (callbackIntent != null) {
667 if (succeeded) {
668 // Mark the intent to indicate that the save was successful (even if the lookup URI
669 // is now null). For local contacts or the local profile, it's possible that the
670 // save triggered removal of the contact, so no lookup URI would exist..
671 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
672 }
673 callbackIntent.setData(lookupUri);
674 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800675 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800676 }
677
Josh Garguse692e012012-01-18 14:53:11 -0800678 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700679 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
680 * subsets, adds the returned array to "results".
681 *
682 * @return the size of the array, if not null; -1 when the array is null.
683 */
684 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
685 ContentProviderResult[] results, ContentResolver resolver)
686 throws RemoteException, OperationApplicationException {
687 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
688 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
689 subset.addAll(diff.subList(offset, offset + subsetCount));
690 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
691 .AUTHORITY, subset);
692 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
693 return -1;
694 }
695 for (ContentProviderResult c : subsetResult) {
696 results[offset++] = c;
697 }
698 return subsetResult.length;
699 }
700
701 /**
Josh Garguse692e012012-01-18 14:53:11 -0800702 * Save updated photo for the specified raw-contact.
703 * @return true for success, false for failure
704 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800705 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800706 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800707 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
708 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
709
benny.lin3a4e7a22014-01-08 10:58:08 +0800710 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800711 }
712
Josh Gargusef15c8e2012-01-30 16:42:02 -0800713 /**
714 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
715 */
Maurice Chu851222a2012-06-21 11:43:08 -0700716 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800717 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800718 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800719 long existingRawContactId = state.findRawContactId();
720 if (existingRawContactId != -1) {
721 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800722 }
723
Wenyi Wang67addcc2015-11-23 10:07:48 -0800724 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800725 }
726
727 /**
728 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
729 */
730 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800731 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800732 if (results == null) {
733 return -1;
734 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800735 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800736 final int numResults = results.length;
737 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800738 final CPOWrapper cpoWrapper = diffWrapper.get(i);
739 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
740 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
741 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800742 return ContentUris.parseId(results[i].uri);
743 }
744 }
745 return -1;
746 }
747
748 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700749 * Creates an intent that can be sent to this service to create a new group as
750 * well as add new members at the same time.
751 *
752 * @param context of the application
753 * @param account in which the group should be created
754 * @param label is the name of the group (cannot be null)
755 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
756 * should be added to the group
757 * @param callbackActivity is the activity to send the callback intent to
758 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700759 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700760 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700761 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700762 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800763 Intent serviceIntent = new Intent(context, ContactSaveService.class);
764 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
765 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
766 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700767 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800768 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700769 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700770
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800771 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700772 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800773 Intent callbackIntent = new Intent(context, callbackActivity);
774 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700775 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800776
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700777 return serviceIntent;
778 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800779
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800780 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700781 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
782 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
783 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
784 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700785 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800786
Katherine Kuan717e3432011-07-13 17:03:24 -0700787 // Create the new group
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700788 final Uri groupUri = mGroupsDao.create(label,
789 new AccountWithDataSet(accountName, accountType, dataSet));
790 final ContentResolver resolver = getContentResolver();
Katherine Kuan717e3432011-07-13 17:03:24 -0700791
792 // If there's no URI, then the insertion failed. Abort early because group members can't be
793 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800794 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700795 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800796 return;
797 }
798
Katherine Kuan717e3432011-07-13 17:03:24 -0700799 // Add new group members
800 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
801
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700802 ContentValues values = new ContentValues();
Katherine Kuan717e3432011-07-13 17:03:24 -0700803 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
Walter Jang8bac28b2016-08-30 10:34:55 -0700804 // with the way other intent extras that are passed to the
Gary Mai363af602016-09-28 10:01:23 -0700805 // {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800806 values.clear();
807 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
808 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
809
810 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700811 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700812 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800813 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800814 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800815 }
816
817 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800818 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800819 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700820 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700821 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800822 Intent serviceIntent = new Intent(context, ContactSaveService.class);
823 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
824 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
825 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700826
827 // Callback intent will be invoked by the service once the group is renamed.
828 Intent callbackIntent = new Intent(context, callbackActivity);
829 callbackIntent.setAction(callbackAction);
830 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
831
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800832 return serviceIntent;
833 }
834
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800835 private void renameGroup(Intent intent) {
836 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
837 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
838
839 if (groupId == -1) {
840 Log.e(TAG, "Invalid arguments for renameGroup request");
841 return;
842 }
843
844 ContentValues values = new ContentValues();
845 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700846 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
847 getContentResolver().update(groupUri, values, null, null);
848
849 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
850 callbackIntent.setData(groupUri);
851 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800852 }
853
854 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800855 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800856 */
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700857 public static Intent createGroupDeletionIntent(Context context, long groupId) {
858 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800859 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800860 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700861
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800862 return serviceIntent;
863 }
864
865 private void deleteGroup(Intent intent) {
866 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
867 if (groupId == -1) {
868 Log.e(TAG, "Invalid arguments for deleteGroup request");
869 return;
870 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700871 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800872
Marcus Hagerott819214d2016-09-29 14:58:27 -0700873 final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700874 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
875 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
876 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
Walter Jang72f99882016-05-26 09:01:31 -0700877
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700878 mGroupsDao.delete(groupUri);
879
880 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
881 }
882
883 public static Intent createUndoIntent(Context context, Intent resultIntent) {
884 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
885 serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
886 serviceIntent.putExtras(resultIntent);
887 return serviceIntent;
888 }
889
890 private void undo(Intent intent) {
891 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
892 if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
893 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
Walter Jang72f99882016-05-26 09:01:31 -0700894 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800895 }
896
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700897
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800898 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700899 * Creates an intent that can be sent to this service to rename a group as
900 * well as add and remove members from the group.
901 *
902 * @param context of the application
903 * @param groupId of the group that should be modified
904 * @param newLabel is the updated name of the group (can be null if the name
905 * should not be updated)
906 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
907 * should be added to the group
908 * @param rawContactsToRemove is an array of raw contact IDs for contacts
909 * that should be removed from the group
910 * @param callbackActivity is the activity to send the callback intent to
911 * @param callbackAction is the intent action for the callback intent
912 */
913 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
914 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700915 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700916 Intent serviceIntent = new Intent(context, ContactSaveService.class);
917 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
918 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
919 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
920 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
921 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
922 rawContactsToRemove);
923
924 // Callback intent will be invoked by the service once the group is updated
925 Intent callbackIntent = new Intent(context, callbackActivity);
926 callbackIntent.setAction(callbackAction);
927 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
928
929 return serviceIntent;
930 }
931
932 private void updateGroup(Intent intent) {
933 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
934 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
935 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
936 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
937
938 if (groupId == -1) {
939 Log.e(TAG, "Invalid arguments for updateGroup request");
940 return;
941 }
942
943 final ContentResolver resolver = getContentResolver();
944 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
945
946 // Update group name if necessary
947 if (label != null) {
948 ContentValues values = new ContentValues();
949 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700950 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700951 }
952
Katherine Kuan717e3432011-07-13 17:03:24 -0700953 // Add and remove members if necessary
954 addMembersToGroup(resolver, rawContactsToAdd, groupId);
955 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
956
957 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
958 callbackIntent.setData(groupUri);
959 deliverCallback(callbackIntent);
960 }
961
Walter Jang3a0b4832016-10-12 11:02:54 -0700962 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700963 long groupId) {
964 if (rawContactsToAdd == null) {
965 return;
966 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700967 for (long rawContactId : rawContactsToAdd) {
968 try {
969 final ArrayList<ContentProviderOperation> rawContactOperations =
970 new ArrayList<ContentProviderOperation>();
971
972 // Build an assert operation to ensure the contact is not already in the group
973 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
974 .newAssertQuery(Data.CONTENT_URI);
975 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
976 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
977 new String[] { String.valueOf(rawContactId),
978 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
979 assertBuilder.withExpectedCount(0);
980 rawContactOperations.add(assertBuilder.build());
981
982 // Build an insert operation to add the contact to the group
983 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
984 .newInsert(Data.CONTENT_URI);
985 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
986 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
987 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
988 rawContactOperations.add(insertBuilder.build());
989
990 if (DEBUG) {
991 for (ContentProviderOperation operation : rawContactOperations) {
992 Log.v(TAG, operation.toString());
993 }
994 }
995
996 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700997 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800998 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700999 }
1000 } catch (RemoteException e) {
1001 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -07001002 FeedbackHelper.sendFeedback(this, TAG,
1003 "Problem persisting user edits for raw contact ID " +
1004 String.valueOf(rawContactId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001005 } catch (OperationApplicationException e) {
1006 // The assert could have failed because the contact is already in the group,
1007 // just continue to the next contact
Walter Jang3a0b4832016-10-12 11:02:54 -07001008 FeedbackHelper.sendFeedback(this, TAG,
1009 "Assert failed in adding raw contact ID " +
1010 String.valueOf(rawContactId) + ". Already exists in group " +
1011 String.valueOf(groupId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001012 }
1013 }
Katherine Kuan717e3432011-07-13 17:03:24 -07001014 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001015
Daniel Lehmann18958a22012-02-28 17:45:25 -08001016 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -07001017 long groupId) {
1018 if (rawContactsToRemove == null) {
1019 return;
1020 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001021 for (long rawContactId : rawContactsToRemove) {
1022 // Apply the delete operation on the data row for the given raw contact's
1023 // membership in the given group. If no contact matches the provided selection, then
1024 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -08001025 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001026 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1027 new String[] { String.valueOf(rawContactId),
1028 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
1029 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001030 }
1031
1032 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001033 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001034 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001035 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
1036 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1037 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
1038 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1039 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
1040
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001041 return serviceIntent;
1042 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001043
1044 private void setStarred(Intent intent) {
1045 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1046 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
1047 if (contactUri == null) {
1048 Log.e(TAG, "Invalid arguments for setStarred request");
1049 return;
1050 }
1051
1052 final ContentValues values = new ContentValues(1);
1053 values.put(Contacts.STARRED, value);
1054 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -07001055
1056 // Undemote the contact if necessary
1057 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
1058 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -08001059 if (c == null) {
1060 return;
1061 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001062 try {
1063 if (c.moveToFirst()) {
1064 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -07001065
1066 // Don't bother undemoting if this contact is the user's profile.
1067 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -08001068 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -07001069 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001070 }
1071 } finally {
1072 c.close();
1073 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001074 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001075
1076 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -07001077 * Creates an intent that can be sent to this service to set the redirect to voicemail.
1078 */
1079 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
1080 boolean value) {
1081 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1082 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1083 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1084 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1085
1086 return serviceIntent;
1087 }
1088
1089 private void setSendToVoicemail(Intent intent) {
1090 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1091 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1092 if (contactUri == null) {
1093 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1094 return;
1095 }
1096
1097 final ContentValues values = new ContentValues(1);
1098 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1099 getContentResolver().update(contactUri, values, null, null);
1100 }
1101
1102 /**
1103 * Creates an intent that can be sent to this service to save the contact's ringtone.
1104 */
1105 public static Intent createSetRingtone(Context context, Uri contactUri,
1106 String value) {
1107 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1108 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1109 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1110 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1111
1112 return serviceIntent;
1113 }
1114
1115 private void setRingtone(Intent intent) {
1116 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1117 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1118 if (contactUri == null) {
1119 Log.e(TAG, "Invalid arguments for setRingtone");
1120 return;
1121 }
1122 ContentValues values = new ContentValues(1);
1123 values.put(Contacts.CUSTOM_RINGTONE, value);
1124 getContentResolver().update(contactUri, values, null, null);
1125 }
1126
1127 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001128 * Creates an intent that sets the selected data item as super primary (default)
1129 */
1130 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1131 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1132 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1133 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1134 return serviceIntent;
1135 }
1136
1137 private void setSuperPrimary(Intent intent) {
1138 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1139 if (dataId == -1) {
1140 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1141 return;
1142 }
1143
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001144 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001145 }
1146
1147 /**
1148 * Creates an intent that clears the primary flag of all data items that belong to the same
1149 * raw_contact as the given data item. Will only clear, if the data item was primary before
1150 * this call
1151 */
1152 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1153 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1154 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1155 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1156 return serviceIntent;
1157 }
1158
1159 private void clearPrimary(Intent intent) {
1160 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1161 if (dataId == -1) {
1162 Log.e(TAG, "Invalid arguments for clearPrimary request");
1163 return;
1164 }
1165
1166 // Update the primary values in the data record.
1167 ContentValues values = new ContentValues(1);
1168 values.put(Data.IS_SUPER_PRIMARY, 0);
1169 values.put(Data.IS_PRIMARY, 0);
1170
1171 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1172 values, null, null);
1173 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001174
1175 /**
1176 * Creates an intent that can be sent to this service to delete a contact.
1177 */
1178 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1179 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1180 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1181 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1182 return serviceIntent;
1183 }
1184
Brian Attwelld2962a32015-03-02 14:48:50 -08001185 /**
1186 * Creates an intent that can be sent to this service to delete multiple contacts.
1187 */
1188 public static Intent createDeleteMultipleContactsIntent(Context context,
James Laskeye5a140a2016-10-18 15:43:42 -07001189 long[] contactIds, final String[] names) {
Brian Attwelld2962a32015-03-02 14:48:50 -08001190 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1191 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1192 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
James Laskeye5a140a2016-10-18 15:43:42 -07001193 serviceIntent.putExtra(ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY, names);
Brian Attwelld2962a32015-03-02 14:48:50 -08001194 return serviceIntent;
1195 }
1196
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001197 private void deleteContact(Intent intent) {
1198 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1199 if (contactUri == null) {
1200 Log.e(TAG, "Invalid arguments for deleteContact request");
1201 return;
1202 }
1203
1204 getContentResolver().delete(contactUri, null, null);
1205 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001206
Brian Attwelld2962a32015-03-02 14:48:50 -08001207 private void deleteMultipleContacts(Intent intent) {
1208 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1209 if (contactIds == null) {
1210 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1211 return;
1212 }
1213 for (long contactId : contactIds) {
1214 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1215 getContentResolver().delete(contactUri, null, null);
1216 }
James Laskeye5a140a2016-10-18 15:43:42 -07001217 final String[] names = intent.getStringArrayExtra(
1218 ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY);
1219 final String deleteToastMessage;
James Laskey56019ad2016-11-14 16:38:35 -08001220 if (contactIds.length != names.length || names.length == 0) {
John Shaod32c4e92021-10-12 20:53:55 +00001221 MessageFormat msgFormat = new MessageFormat(
1222 getResources().getString(R.string.contacts_deleted_toast),
1223 Locale.getDefault());
1224 Map<String, Object> arguments = new HashMap<>();
1225 arguments.put("count", contactIds.length);
1226 deleteToastMessage = msgFormat.format(arguments);
James Laskeye5a140a2016-10-18 15:43:42 -07001227 } else if (names.length == 1) {
1228 deleteToastMessage = getResources().getString(
Aurimas Liutikas3ccadf22018-10-09 23:51:48 +00001229 R.string.contacts_deleted_one_named_toast, (Object[]) names);
James Laskeye5a140a2016-10-18 15:43:42 -07001230 } else if (names.length == 2) {
1231 deleteToastMessage = getResources().getString(
Aurimas Liutikas3ccadf22018-10-09 23:51:48 +00001232 R.string.contacts_deleted_two_named_toast, (Object[]) names);
James Laskeye5a140a2016-10-18 15:43:42 -07001233 } else {
1234 deleteToastMessage = getResources().getString(
Aurimas Liutikas3ccadf22018-10-09 23:51:48 +00001235 R.string.contacts_deleted_many_named_toast, (Object[]) names);
James Laskeye5a140a2016-10-18 15:43:42 -07001236 }
James Laskey56019ad2016-11-14 16:38:35 -08001237
Wenyi Wang687d2182015-10-28 17:03:18 -07001238 mMainHandler.post(new Runnable() {
1239 @Override
1240 public void run() {
1241 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1242 .show();
1243 }
1244 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001245 }
1246
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001247 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001248 * Creates an intent that can be sent to this service to split a contact into it's constituent
Gary Maib9065dd2016-11-08 10:49:00 -08001249 * pieces. This will set the raw contact ids to {@link AggregationExceptions#TYPE_AUTOMATIC} so
Gary Mai53fe0d22016-07-26 17:23:53 -07001250 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001251 */
1252 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1253 ResultReceiver receiver) {
1254 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1255 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1256 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1257 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1258 return serviceIntent;
1259 }
1260
Gary Maib9065dd2016-11-08 10:49:00 -08001261 /**
1262 * Creates an intent that can be sent to this service to split a contact into it's constituent
1263 * pieces. This will explicitly set the raw contact ids to
1264 * {@link AggregationExceptions#TYPE_KEEP_SEPARATE}.
1265 */
1266 public static Intent createHardSplitContactIntent(Context context, long[][] rawContactIds) {
1267 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1268 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1269 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1270 serviceIntent.putExtra(ContactSaveService.EXTRA_HARD_SPLIT, true);
1271 return serviceIntent;
1272 }
1273
Gary Mai7efa9942016-05-12 11:26:49 -07001274 private void splitContact(Intent intent) {
1275 final long rawContactIds[][] = (long[][]) intent
1276 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001277 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Maib9065dd2016-11-08 10:49:00 -08001278 final boolean hardSplit = intent.getBooleanExtra(EXTRA_HARD_SPLIT, false);
Gary Mai7efa9942016-05-12 11:26:49 -07001279 if (rawContactIds == null) {
1280 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001281 if (receiver != null) {
1282 receiver.send(BAD_ARGUMENTS, new Bundle());
1283 }
Gary Mai7efa9942016-05-12 11:26:49 -07001284 return;
1285 }
1286 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1287 final ContentResolver resolver = getContentResolver();
1288 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001289 for (int i = 0; i < rawContactIds.length; i++) {
1290 for (int j = 0; j < rawContactIds.length; j++) {
1291 if (i != j) {
Gary Maib9065dd2016-11-08 10:49:00 -08001292 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j],
1293 hardSplit)) {
Gary Mai7efa9942016-05-12 11:26:49 -07001294 if (receiver != null) {
1295 receiver.send(CP2_ERROR, new Bundle());
1296 return;
1297 }
1298 }
1299 }
1300 }
1301 }
1302 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1303 if (receiver != null) {
1304 receiver.send(CP2_ERROR, new Bundle());
1305 }
1306 return;
1307 }
Gary Maib9065dd2016-11-08 10:49:00 -08001308 LocalBroadcastManager.getInstance(this)
1309 .sendBroadcast(new Intent(BROADCAST_UNLINK_COMPLETE));
Gary Mai7efa9942016-05-12 11:26:49 -07001310 if (receiver != null) {
1311 receiver.send(CONTACTS_SPLIT, new Bundle());
1312 } else {
1313 showToast(R.string.contactUnlinkedToast);
1314 }
1315 }
1316
1317 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001318 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001319 * and {@param rawContactIds2} to {@param operations}.
1320 * @return false if an error occurred, true otherwise.
1321 */
1322 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
Gary Maib9065dd2016-11-08 10:49:00 -08001323 long[] rawContactIds1, long[] rawContactIds2, boolean hardSplit) {
Gary Mai7efa9942016-05-12 11:26:49 -07001324 if (rawContactIds1 == null || rawContactIds2 == null) {
1325 Log.e(TAG, "Invalid arguments for splitContact request");
1326 return false;
1327 }
1328 // For each pair of raw contacts, insert an aggregation exception
1329 final ContentResolver resolver = getContentResolver();
1330 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1331 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1332 for (int i = 0; i < rawContactIds1.length; i++) {
1333 for (int j = 0; j < rawContactIds2.length; j++) {
Gary Maib9065dd2016-11-08 10:49:00 -08001334 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j], hardSplit);
Gary Mai7efa9942016-05-12 11:26:49 -07001335 // Before we get to 500 we need to flush the operations list
1336 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1337 if (!applyOperations(resolver, operations)) {
1338 return false;
1339 }
1340 operations.clear();
1341 }
1342 }
1343 }
1344 return true;
1345 }
1346
1347 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001348 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001349 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001350 */
1351 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001352 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001353 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1354 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1355 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1356 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001357
1358 // Callback intent will be invoked by the service once the contacts are joined.
1359 Intent callbackIntent = new Intent(context, callbackActivity);
1360 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001361 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1362
1363 return serviceIntent;
1364 }
1365
Brian Attwelld3946ca2015-03-03 11:13:49 -08001366 /**
1367 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1368 * No special attention is paid to where the resulting contact's name is taken from.
1369 */
Gary Mai7efa9942016-05-12 11:26:49 -07001370 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1371 ResultReceiver receiver) {
1372 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001373 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1374 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001375 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001376 return serviceIntent;
1377 }
1378
Gary Mai7efa9942016-05-12 11:26:49 -07001379 /**
1380 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1381 * No special attention is paid to where the resulting contact's name is taken from.
1382 */
1383 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1384 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1385 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001386
1387 private interface JoinContactQuery {
1388 String[] PROJECTION = {
1389 RawContacts._ID,
1390 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001391 RawContacts.DISPLAY_NAME_SOURCE,
1392 };
1393
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001394 int _ID = 0;
1395 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001396 int DISPLAY_NAME_SOURCE = 2;
1397 }
1398
1399 private interface ContactEntityQuery {
1400 String[] PROJECTION = {
1401 Contacts.Entity.DATA_ID,
1402 Contacts.Entity.CONTACT_ID,
1403 Contacts.Entity.IS_SUPER_PRIMARY,
1404 };
1405 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1406 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1407 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1408 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1409
1410 int DATA_ID = 0;
1411 int CONTACT_ID = 1;
1412 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001413 }
1414
Brian Attwelld3946ca2015-03-03 11:13:49 -08001415 private void joinSeveralContacts(Intent intent) {
1416 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001417
Gary Mai7efa9942016-05-12 11:26:49 -07001418 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001419
Brian Attwelld3946ca2015-03-03 11:13:49 -08001420 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001421 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1422 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001423 if (rawContactIds == null) {
1424 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001425 if (receiver != null) {
1426 receiver.send(BAD_ARGUMENTS, new Bundle());
1427 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001428 return;
1429 }
1430
Brian Attwelld3946ca2015-03-03 11:13:49 -08001431 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001432 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001433 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1434 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1435 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001436 for (int i = 0; i < rawContactIds.length; i++) {
1437 for (int j = 0; j < rawContactIds.length; j++) {
1438 if (i != j) {
1439 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1440 }
Walter Jang0653de32015-07-24 12:12:40 -07001441 // Before we get to 500 we need to flush the operations list
1442 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001443 if (!applyOperations(resolver, operations)) {
1444 if (receiver != null) {
1445 receiver.send(CP2_ERROR, new Bundle());
1446 }
Walter Jang0653de32015-07-24 12:12:40 -07001447 return;
1448 }
1449 operations.clear();
1450 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001451 }
1452 }
Gary Mai7efa9942016-05-12 11:26:49 -07001453 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1454 if (receiver != null) {
1455 receiver.send(CP2_ERROR, new Bundle());
1456 }
Walter Jang0653de32015-07-24 12:12:40 -07001457 return;
1458 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001459
John Shaoa3c507a2016-09-13 14:26:17 -07001460
1461 final String name = queryNameOfLinkedContacts(contactIds);
1462 if (name != null) {
1463 if (receiver != null) {
1464 final Bundle result = new Bundle();
1465 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1466 result.putString(EXTRA_DISPLAY_NAME, name);
1467 receiver.send(CONTACTS_LINKED, result);
1468 } else {
James Laskeyf62b4882016-10-21 11:36:40 -07001469 if (TextUtils.isEmpty(name)) {
1470 showToast(R.string.contactsJoinedMessage);
1471 } else {
1472 showToast(R.string.contactsJoinedNamedMessage, name);
1473 }
John Shaoa3c507a2016-09-13 14:26:17 -07001474 }
Gary Maib9065dd2016-11-08 10:49:00 -08001475 LocalBroadcastManager.getInstance(this)
1476 .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
Gary Mai7efa9942016-05-12 11:26:49 -07001477 } else {
John Shaoa3c507a2016-09-13 14:26:17 -07001478 if (receiver != null) {
1479 receiver.send(CP2_ERROR, new Bundle());
1480 }
1481 showToast(R.string.contactJoinErrorToast);
Gary Mai7efa9942016-05-12 11:26:49 -07001482 }
Walter Jang0653de32015-07-24 12:12:40 -07001483 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001484
John Shaoa3c507a2016-09-13 14:26:17 -07001485 /** Get the display name of the top-level contact after the contacts have been linked. */
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001486 private String queryNameOfLinkedContacts(long[] contactIds) {
1487 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1488 final String[] whereArgs = new String[contactIds.length];
1489 for (int i = 0; i < contactIds.length; i++) {
1490 whereArgs[i] = String.valueOf(contactIds[i]);
1491 whereBuilder.append("?,");
1492 }
1493 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1494 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
James Laskeyf62b4882016-10-21 11:36:40 -07001495 new String[]{Contacts._ID, Contacts.DISPLAY_NAME,
1496 Contacts.DISPLAY_NAME_ALTERNATIVE},
John Shaoa3c507a2016-09-13 14:26:17 -07001497 whereBuilder.toString(), whereArgs, null);
1498
1499 String name = null;
James Laskeyf62b4882016-10-21 11:36:40 -07001500 String nameAlt = null;
John Shaoa3c507a2016-09-13 14:26:17 -07001501 long contactId = 0;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001502 try {
1503 if (cursor.moveToFirst()) {
John Shaoa3c507a2016-09-13 14:26:17 -07001504 contactId = cursor.getLong(0);
1505 name = cursor.getString(1);
James Laskeyf62b4882016-10-21 11:36:40 -07001506 nameAlt = cursor.getString(2);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001507 }
John Shaoa3c507a2016-09-13 14:26:17 -07001508 while(cursor.moveToNext()) {
1509 if (cursor.getLong(0) != contactId) {
1510 return null;
1511 }
1512 }
James Laskeyf62b4882016-10-21 11:36:40 -07001513
1514 final String formattedName = ContactDisplayUtils.getPreferredDisplayName(name, nameAlt,
1515 new ContactsPreferences(getApplicationContext()));
1516 return formattedName == null ? "" : formattedName;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001517 } finally {
John Shaoa3c507a2016-09-13 14:26:17 -07001518 if (cursor != null) {
1519 cursor.close();
1520 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001521 }
1522 }
1523
Walter Jang0653de32015-07-24 12:12:40 -07001524 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001525 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001526 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001527 try {
John Shaoa3c507a2016-09-13 14:26:17 -07001528 final ContentProviderResult[] result =
1529 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1530 for (int i = 0; i < result.length; ++i) {
1531 // if no rows were modified in the operation then we count it as fail.
1532 if (result[i].count < 0) {
1533 throw new OperationApplicationException();
1534 }
1535 }
Walter Jang0653de32015-07-24 12:12:40 -07001536 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001537 } catch (RemoteException | OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001538 FeedbackHelper.sendFeedback(this, TAG,
1539 "Failed to apply aggregation exception batch", e);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001540 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001541 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001542 }
1543 }
1544
Brian Attwelld3946ca2015-03-03 11:13:49 -08001545 private void joinContacts(Intent intent) {
1546 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1547 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001548
1549 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001550 // in the join UIs.
1551 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1552 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001553 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001554 return;
1555 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001556
Brian Attwell548f5c62015-01-27 17:46:46 -08001557 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001558
1559 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001560 for (int i = 0; i < rawContactIds.length; i++) {
1561 for (int j = 0; j < rawContactIds.length; j++) {
1562 if (i != j) {
1563 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1564 }
1565 }
1566 }
1567
Brian Attwelld3946ca2015-03-03 11:13:49 -08001568 final ContentResolver resolver = getContentResolver();
1569
Brian Attwell548f5c62015-01-27 17:46:46 -08001570 // Use the name for contactId1 as the name for the newly aggregated contact.
1571 final Uri contactId1Uri = ContentUris.withAppendedId(
1572 Contacts.CONTENT_URI, contactId1);
1573 final Uri entityUri = Uri.withAppendedPath(
1574 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1575 Cursor c = resolver.query(entityUri,
1576 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1577 if (c == null) {
1578 Log.e(TAG, "Unable to open Contacts DB cursor");
1579 showToast(R.string.contactSavedErrorToast);
1580 return;
1581 }
1582 long dataIdToAddSuperPrimary = -1;
1583 try {
1584 if (c.moveToFirst()) {
1585 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1586 }
1587 } finally {
1588 c.close();
1589 }
1590
1591 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1592 // display name does not change as a result of the join.
1593 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001594 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001595 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1596 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1597 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001598 operations.add(builder.build());
1599 }
1600
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001601 // Apply all aggregation exceptions as one batch
John Shaoa3c507a2016-09-13 14:26:17 -07001602 final boolean success = applyOperations(resolver, operations);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001603
John Shaoa3c507a2016-09-13 14:26:17 -07001604 final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2});
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001605 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
John Shaoa3c507a2016-09-13 14:26:17 -07001606 if (success && name != null) {
James Laskeyf62b4882016-10-21 11:36:40 -07001607 if (TextUtils.isEmpty(name)) {
1608 showToast(R.string.contactsJoinedMessage);
1609 } else {
1610 showToast(R.string.contactsJoinedNamedMessage, name);
1611 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001612 Uri uri = RawContacts.getContactLookupUri(resolver,
1613 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1614 callbackIntent.setData(uri);
Gary Maib9065dd2016-11-08 10:49:00 -08001615 LocalBroadcastManager.getInstance(this)
1616 .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001617 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001618 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001619 }
1620
Gary Mai7efa9942016-05-12 11:26:49 -07001621 /**
1622 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1623 * array of the return value holds an array of raw contact ids for one contactId.
1624 * @param contactIds
1625 * @return
1626 */
1627 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1628 final long[][] rawContactIds = new long[contactIds.length][];
1629 for (int i = 0; i < contactIds.length; i++) {
1630 rawContactIds[i] = getRawContactIds(contactIds[i]);
1631 }
1632 return rawContactIds;
1633 }
1634
1635 /**
1636 * Gets the raw contact ids associated with {@param contactId}.
1637 * @param contactId
1638 * @return Array of raw contact ids.
1639 */
1640 private long[] getRawContactIds(long contactId) {
1641 final ContentResolver resolver = getContentResolver();
1642 long rawContactIds[];
1643
1644 final StringBuilder queryBuilder = new StringBuilder();
1645 queryBuilder.append(RawContacts.CONTACT_ID)
1646 .append("=")
1647 .append(String.valueOf(contactId));
1648
1649 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1650 JoinContactQuery.PROJECTION,
1651 queryBuilder.toString(),
1652 null, null);
1653 if (c == null) {
1654 Log.e(TAG, "Unable to open Contacts DB cursor");
1655 return null;
1656 }
1657 try {
1658 rawContactIds = new long[c.getCount()];
1659 for (int i = 0; i < rawContactIds.length; i++) {
1660 c.moveToPosition(i);
1661 final long rawContactId = c.getLong(JoinContactQuery._ID);
1662 rawContactIds[i] = rawContactId;
1663 }
1664 } finally {
1665 c.close();
1666 }
1667 return rawContactIds;
1668 }
1669
Brian Attwelld3946ca2015-03-03 11:13:49 -08001670 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1671 if (contactIds == null) {
1672 return null;
1673 }
1674
Brian Attwell548f5c62015-01-27 17:46:46 -08001675 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001676
1677 final StringBuilder queryBuilder = new StringBuilder();
1678 final String stringContactIds[] = new String[contactIds.length];
1679 for (int i = 0; i < contactIds.length; i++) {
1680 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1681 stringContactIds[i] = String.valueOf(contactIds[i]);
1682 if (contactIds[i] == -1) {
1683 return null;
1684 }
1685 if (i == contactIds.length -1) {
1686 break;
1687 }
1688 queryBuilder.append(" OR ");
1689 }
1690
Brian Attwell548f5c62015-01-27 17:46:46 -08001691 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1692 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001693 queryBuilder.toString(),
1694 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001695 if (c == null) {
1696 Log.e(TAG, "Unable to open Contacts DB cursor");
1697 showToast(R.string.contactSavedErrorToast);
1698 return null;
1699 }
Gary Mai7efa9942016-05-12 11:26:49 -07001700 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001701 try {
1702 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001703 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001704 return null;
1705 }
1706 rawContactIds = new long[c.getCount()];
1707 for (int i = 0; i < rawContactIds.length; i++) {
1708 c.moveToPosition(i);
1709 long rawContactId = c.getLong(JoinContactQuery._ID);
1710 rawContactIds[i] = rawContactId;
1711 }
1712 } finally {
1713 c.close();
1714 }
1715 return rawContactIds;
1716 }
1717
Brian Attwelld3946ca2015-03-03 11:13:49 -08001718 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1719 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1720 }
1721
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001722 /**
1723 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1724 */
1725 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1726 long rawContactId1, long rawContactId2) {
1727 Builder builder =
1728 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1729 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1730 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1731 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1732 operations.add(builder.build());
1733 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001734
1735 /**
Gary Maib9065dd2016-11-08 10:49:00 -08001736 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} or a
1737 * {@link AggregationExceptions#TYPE_KEEP_SEPARATE} ContentProviderOperation if a hard split is
1738 * requested.
Gary Mai7efa9942016-05-12 11:26:49 -07001739 */
1740 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
Gary Maib9065dd2016-11-08 10:49:00 -08001741 long rawContactId1, long rawContactId2, boolean hardSplit) {
Gary Mai7efa9942016-05-12 11:26:49 -07001742 final Builder builder =
1743 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Maib9065dd2016-11-08 10:49:00 -08001744 builder.withValue(AggregationExceptions.TYPE,
1745 hardSplit
1746 ? AggregationExceptions.TYPE_KEEP_SEPARATE
1747 : AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001748 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1749 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1750 operations.add(builder.build());
1751 }
1752
Marcus Hagerott7333c372016-11-07 09:40:20 -08001753 /**
Marcus Hagerott7333c372016-11-07 09:40:20 -08001754 * Returns an intent that can start this service and cause it to sleep for the specified time.
1755 *
1756 * This exists purely for debugging and manual testing. Since this service uses a single thread
1757 * it is useful to have a way to test behavior when work is queued up and most of the other
1758 * operations complete too quickly to simulate that under normal conditions.
1759 */
1760 public static Intent createSleepIntent(Context context, long millis) {
1761 return new Intent(context, ContactSaveService.class).setAction(ACTION_SLEEP)
1762 .putExtra(EXTRA_SLEEP_DURATION, millis);
1763 }
1764
1765 private void sleepForDebugging(Intent intent) {
1766 long duration = intent.getLongExtra(EXTRA_SLEEP_DURATION, 1000);
1767 if (Log.isLoggable(TAG, Log.DEBUG)) {
1768 Log.d(TAG, "sleeping for " + duration + "ms");
1769 }
1770 try {
1771 Thread.sleep(duration);
1772 } catch (InterruptedException e) {
1773 e.printStackTrace();
1774 }
1775 if (Log.isLoggable(TAG, Log.DEBUG)) {
1776 Log.d(TAG, "finished sleeping");
Marcus Hagerott819214d2016-09-29 14:58:27 -07001777 }
1778 }
1779
Gary Mai7efa9942016-05-12 11:26:49 -07001780 /**
James Laskeyf62b4882016-10-21 11:36:40 -07001781 * Shows a toast on the UI thread by formatting messageId using args.
1782 * @param messageId id of message string
1783 * @param args args to format string
1784 */
1785 private void showToast(final int messageId, final Object... args) {
1786 final String message = getResources().getString(messageId, args);
1787 mMainHandler.post(new Runnable() {
1788 @Override
1789 public void run() {
1790 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1791 }
1792 });
1793 }
1794
1795
1796 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001797 * Shows a toast on the UI thread.
1798 */
1799 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001800 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001801
1802 @Override
1803 public void run() {
1804 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1805 }
1806 });
1807 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001808
1809 private void deliverCallback(final Intent callbackIntent) {
1810 mMainHandler.post(new Runnable() {
1811
1812 @Override
1813 public void run() {
1814 deliverCallbackOnUiThread(callbackIntent);
1815 }
1816 });
1817 }
1818
1819 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1820 // TODO: this assumes that if there are multiple instances of the same
1821 // activity registered, the last one registered is the one waiting for
1822 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001823 for (Listener listener : sListeners) {
1824 if (callbackIntent.getComponent().equals(
1825 ((Activity) listener).getIntent().getComponent())) {
1826 listener.onServiceCompleted(callbackIntent);
1827 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001828 }
1829 }
1830 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001831
1832 public interface GroupsDao {
1833 Uri create(String title, AccountWithDataSet account);
1834 int delete(Uri groupUri);
1835 Bundle captureDeletionUndoData(Uri groupUri);
1836 Uri undoDeletion(Bundle undoData);
1837 }
1838
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001839 public static class GroupsDaoImpl implements GroupsDao {
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001840 public static final String KEY_GROUP_DATA = "groupData";
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001841 public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
1842
1843 private static final String TAG = "GroupsDao";
1844 private final Context context;
1845 private final ContentResolver contentResolver;
1846
1847 public GroupsDaoImpl(Context context) {
1848 this(context, context.getContentResolver());
1849 }
1850
1851 public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
1852 this.context = context;
1853 this.contentResolver = contentResolver;
1854 }
1855
1856 public Bundle captureDeletionUndoData(Uri groupUri) {
1857 final long groupId = ContentUris.parseId(groupUri);
1858 final Bundle result = new Bundle();
1859
1860 final Cursor cursor = contentResolver.query(groupUri,
1861 new String[]{
1862 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
1863 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
1864 Groups.SHOULD_SYNC
1865 },
1866 Groups.DELETED + "=?", new String[] { "0" }, null);
1867 try {
1868 if (cursor.moveToFirst()) {
1869 final ContentValues groupValues = new ContentValues();
1870 DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
1871 result.putParcelable(KEY_GROUP_DATA, groupValues);
1872 } else {
1873 // Group doesn't exist.
1874 return result;
1875 }
1876 } finally {
1877 cursor.close();
1878 }
1879
1880 final Cursor membersCursor = contentResolver.query(
1881 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
1882 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1883 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
1884 final long[] memberIds = new long[membersCursor.getCount()];
1885 int i = 0;
1886 while (membersCursor.moveToNext()) {
1887 memberIds[i++] = membersCursor.getLong(0);
1888 }
1889 result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
1890 return result;
1891 }
1892
1893 public Uri undoDeletion(Bundle deletedGroupData) {
1894 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
1895 if (groupData == null) {
1896 return null;
1897 }
1898 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
1899 final long groupId = ContentUris.parseId(groupUri);
1900
1901 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
1902 if (memberIds == null) {
1903 return groupUri;
1904 }
1905 final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
1906 for (int i = 0; i < memberIds.length; i++) {
1907 memberInsertions[i] = new ContentValues();
1908 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
1909 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1910 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
1911 }
1912 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
1913 if (inserted != memberIds.length) {
1914 Log.e(TAG, "Could not recover some members for group deletion undo");
1915 }
1916
1917 return groupUri;
1918 }
1919
1920 public Uri create(String title, AccountWithDataSet account) {
1921 final ContentValues values = new ContentValues();
1922 values.put(Groups.TITLE, title);
1923 values.put(Groups.ACCOUNT_NAME, account.name);
1924 values.put(Groups.ACCOUNT_TYPE, account.type);
1925 values.put(Groups.DATA_SET, account.dataSet);
1926 return contentResolver.insert(Groups.CONTENT_URI, values);
1927 }
1928
1929 public int delete(Uri groupUri) {
1930 return contentResolver.delete(groupUri, null, null);
1931 }
1932 }
Marcus Hagerott7333c372016-11-07 09:40:20 -08001933
1934 /**
1935 * Keeps track of which operations have been requested but have not yet finished for this
1936 * service.
1937 */
1938 public static class State {
1939 private final CopyOnWriteArrayList<Intent> mPending;
1940
1941 public State() {
1942 mPending = new CopyOnWriteArrayList<>();
1943 }
1944
1945 public State(Collection<Intent> pendingActions) {
1946 mPending = new CopyOnWriteArrayList<>(pendingActions);
1947 }
1948
1949 public boolean isIdle() {
1950 return mPending.isEmpty();
1951 }
1952
1953 public Intent getCurrentIntent() {
1954 return mPending.isEmpty() ? null : mPending.get(0);
1955 }
1956
1957 /**
1958 * Returns the first intent requested that has the specified action or null if no intent
1959 * with that action has been requested.
1960 */
1961 public Intent getNextIntentWithAction(String action) {
1962 for (Intent intent : mPending) {
1963 if (action.equals(intent.getAction())) {
1964 return intent;
1965 }
1966 }
1967 return null;
1968 }
1969
1970 public boolean isActionPending(String action) {
1971 return getNextIntentWithAction(action) != null;
1972 }
1973
1974 private void onFinish(Intent intent) {
1975 if (mPending.isEmpty()) {
1976 return;
1977 }
1978 final String action = mPending.get(0).getAction();
1979 if (action.equals(intent.getAction())) {
1980 mPending.remove(0);
1981 }
1982 }
1983
1984 private void onStart(Intent intent) {
1985 if (intent.getAction() == null) {
1986 return;
1987 }
1988 mPending.add(intent);
1989 }
1990 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001991}