blob: d2a65a868b3dc0e910df7127a563f856ef6bb427 [file] [log] [blame]
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080017package com.android.contacts;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070018
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080019import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070020import android.app.IntentService;
21import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080022import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070023import android.content.ContentProviderResult;
24import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080025import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070026import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080027import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070028import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080029import android.content.OperationApplicationException;
30import android.database.Cursor;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070031import android.database.DatabaseUtils;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070032import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080033import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080034import android.os.Handler;
35import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080036import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080037import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070038import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080039import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080040import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080041import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080042import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070043import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080044import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070045import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070046import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070047import android.provider.ContactsContract.RawContactsEntity;
Marcus Hagerott66e8b222016-10-23 15:41:55 -070048import android.support.annotation.NonNull;
49import android.support.annotation.Nullable;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070050import android.support.v4.content.LocalBroadcastManager;
Gary Mai7efa9942016-05-12 11:26:49 -070051import android.support.v4.os.ResultReceiver;
Marcus Hagerott7333c372016-11-07 09:40:20 -080052import android.telephony.SubscriptionInfo;
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;
Wenyi Wang67addcc2015-11-23 10:07:48 -080058import com.android.contacts.common.compat.CompatUtils;
Chiao Chengd7ca03e2012-10-24 15:14:08 -070059import com.android.contacts.common.database.ContactUpdateUtils;
Marcus Hagerott819214d2016-09-29 14:58:27 -070060import com.android.contacts.common.database.SimContactDao;
Chiao Cheng0d5588d2012-11-26 15:34:14 -080061import com.android.contacts.common.model.AccountTypeManager;
Wenyi Wang67addcc2015-11-23 10:07:48 -080062import com.android.contacts.common.model.CPOWrapper;
Yorke Leecd321f62013-10-28 15:20:15 -070063import com.android.contacts.common.model.RawContactDelta;
64import com.android.contacts.common.model.RawContactDeltaList;
65import com.android.contacts.common.model.RawContactModifier;
Marcus Hagerott7333c372016-11-07 09:40:20 -080066import com.android.contacts.common.model.SimCard;
Marcus Hagerott819214d2016-09-29 14:58:27 -070067import com.android.contacts.common.model.SimContact;
Chiao Cheng428f0082012-11-13 18:38:56 -080068import com.android.contacts.common.model.account.AccountWithDataSet;
James Laskeyf62b4882016-10-21 11:36:40 -070069import com.android.contacts.common.preference.ContactsPreferences;
70import com.android.contacts.common.util.ContactDisplayUtils;
Jay Shrauner615ed9c2015-07-29 11:27:56 -070071import com.android.contacts.common.util.PermissionsUtil;
Wenyi Wangaac0e662015-12-18 17:17:33 -080072import com.android.contacts.compat.PinnedPositionsCompat;
Yorke Lee637a38e2013-09-14 08:36:33 -070073import com.android.contacts.util.ContactPhotoUtils;
Walter Jang3a0b4832016-10-12 11:02:54 -070074import com.android.contactsbind.FeedbackHelper;
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070075import com.google.common.collect.Lists;
76import com.google.common.collect.Sets;
77
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080078import java.util.ArrayList;
Marcus Hagerott7333c372016-11-07 09:40:20 -080079import java.util.Collection;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080080import java.util.HashSet;
81import java.util.List;
82import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070083
Marcus Hagerott819214d2016-09-29 14:58:27 -070084import static android.Manifest.permission.WRITE_CONTACTS;
85
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";
136
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800137 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800138 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800139 public static final String EXTRA_CONTACT_ID1 = "contactId1";
140 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800141
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700142 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
143 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
144
145 public static final String ACTION_SET_RINGTONE = "setRingtone";
146 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
147
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700148 public static final String ACTION_UNDO = "undo";
149 public static final String EXTRA_UNDO_ACTION = "undoAction";
150 public static final String EXTRA_UNDO_DATA = "undoData";
151
Marcus Hagerott819214d2016-09-29 14:58:27 -0700152 public static final String ACTION_IMPORT_FROM_SIM = "importFromSim";
153 public static final String EXTRA_SIM_CONTACTS = "simContacts";
Marcus Hagerott7333c372016-11-07 09:40:20 -0800154 public static final String EXTRA_SIM_SUBSCRIPTION_ID = "simSubscriptionId";
155
156 // For debugging and testing what happens when requests are queued up.
157 public static final String ACTION_SLEEP = "sleep";
158 public static final String EXTRA_SLEEP_DURATION = "sleepDuration";
Marcus Hagerott819214d2016-09-29 14:58:27 -0700159
160 public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
161 public static final String BROADCAST_SIM_IMPORT_COMPLETE = "simImportComplete";
Marcus Hagerott7333c372016-11-07 09:40:20 -0800162
163 public static final String BROADCAST_SERVICE_STATE_CHANGED = "serviceStateChanged";
Marcus Hagerott819214d2016-09-29 14:58:27 -0700164
165 public static final String EXTRA_RESULT_CODE = "resultCode";
166 public static final String EXTRA_RESULT_COUNT = "count";
167 public static final String EXTRA_OPERATION_REQUESTED_AT_TIME = "requestedTime";
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700168
Gary Mai7efa9942016-05-12 11:26:49 -0700169 public static final int CP2_ERROR = 0;
170 public static final int CONTACTS_LINKED = 1;
171 public static final int CONTACTS_SPLIT = 2;
Gary Mai31d572e2016-06-03 14:04:32 -0700172 public static final int BAD_ARGUMENTS = 3;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700173 public static final int RESULT_UNKNOWN = 0;
174 public static final int RESULT_SUCCESS = 1;
175 public static final int RESULT_FAILURE = 2;
Gary Mai7efa9942016-05-12 11:26:49 -0700176
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700177 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
178 Data.MIMETYPE,
179 Data.IS_PRIMARY,
180 Data.DATA1,
181 Data.DATA2,
182 Data.DATA3,
183 Data.DATA4,
184 Data.DATA5,
185 Data.DATA6,
186 Data.DATA7,
187 Data.DATA8,
188 Data.DATA9,
189 Data.DATA10,
190 Data.DATA11,
191 Data.DATA12,
192 Data.DATA13,
193 Data.DATA14,
194 Data.DATA15
195 );
196
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800197 private static final int PERSIST_TRIES = 3;
198
Walter Jang0653de32015-07-24 12:12:40 -0700199 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
200
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800201 public interface Listener {
202 public void onServiceCompleted(Intent callbackIntent);
203 }
204
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100205 private static final CopyOnWriteArrayList<Listener> sListeners =
206 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800207
Marcus Hagerott7333c372016-11-07 09:40:20 -0800208 // Holds the current state of the service
209 private static final State sState = new State();
210
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800211 private Handler mMainHandler;
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700212 private GroupsDao mGroupsDao;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700213 private SimContactDao mSimContactDao;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800214
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700215 public ContactSaveService() {
216 super(TAG);
217 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800218 mMainHandler = new Handler(Looper.getMainLooper());
219 }
220
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700221 @Override
222 public void onCreate() {
223 super.onCreate();
224 mGroupsDao = new GroupsDaoImpl(this);
Marcus Hagerott66e8b222016-10-23 15:41:55 -0700225 mSimContactDao = SimContactDao.create(this);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700226 }
227
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800228 public static void registerListener(Listener listener) {
229 if (!(listener instanceof Activity)) {
230 throw new ClassCastException("Only activities can be registered to"
231 + " receive callback from " + ContactSaveService.class.getName());
232 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100233 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800234 }
235
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700236 public static boolean canUndo(Intent resultIntent) {
237 return resultIntent.hasExtra(EXTRA_UNDO_DATA);
238 }
239
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800240 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100241 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700242 }
243
Marcus Hagerott7333c372016-11-07 09:40:20 -0800244 public static State getState() {
245 return sState;
246 }
247
248 private void notifyStateChanged() {
249 LocalBroadcastManager.getInstance(this)
250 .sendBroadcast(new Intent(BROADCAST_SERVICE_STATE_CHANGED));
251 }
252
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800253 /**
254 * Returns true if the ContactSaveService was started successfully and false if an exception
255 * was thrown and a Toast error message was displayed.
256 */
257 public static boolean startService(Context context, Intent intent, int saveMode) {
258 try {
259 context.startService(intent);
260 } catch (Exception exception) {
261 final int resId;
262 switch (saveMode) {
Gary Mai363af602016-09-28 10:01:23 -0700263 case ContactEditorActivity.ContactEditor.SaveMode.SPLIT:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800264 resId = R.string.contactUnlinkErrorToast;
265 break;
Gary Mai363af602016-09-28 10:01:23 -0700266 case ContactEditorActivity.ContactEditor.SaveMode.RELOAD:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800267 resId = R.string.contactJoinErrorToast;
268 break;
Gary Mai363af602016-09-28 10:01:23 -0700269 case ContactEditorActivity.ContactEditor.SaveMode.CLOSE:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800270 resId = R.string.contactSavedErrorToast;
271 break;
272 default:
273 resId = R.string.contactGenericErrorToast;
274 }
275 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
276 return false;
277 }
278 return true;
279 }
280
281 /**
282 * Utility method that starts service and handles exception.
283 */
284 public static void startService(Context context, Intent intent) {
285 try {
286 context.startService(intent);
287 } catch (Exception exception) {
288 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
289 }
290 }
291
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700292 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800293 public Object getSystemService(String name) {
294 Object service = super.getSystemService(name);
295 if (service != null) {
296 return service;
297 }
298
299 return getApplicationContext().getSystemService(name);
300 }
301
Marcus Hagerott7333c372016-11-07 09:40:20 -0800302 // Parent classes Javadoc says not to override this method but we're doing it just to update
303 // our state which should be OK since we're still doing the work in onHandleIntent
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800304 @Override
Marcus Hagerott7333c372016-11-07 09:40:20 -0800305 public int onStartCommand(Intent intent, int flags, int startId) {
306 sState.onStart(intent);
307 notifyStateChanged();
308 return super.onStartCommand(intent, flags, startId);
309 }
310
311 @Override
312 protected void onHandleIntent(final Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800313 if (intent == null) {
314 Log.d(TAG, "onHandleIntent: could not handle null intent");
315 return;
316 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700317 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
318 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
319 // TODO: add more specific error string such as "Turn on Contacts
320 // permission to update your contacts"
321 showToast(R.string.contactSavedErrorToast);
322 return;
323 }
324
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700325 // Call an appropriate method. If we're sure it affects how incoming phone calls are
326 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700327 String action = intent.getAction();
328 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
329 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800330 } else if (ACTION_SAVE_CONTACT.equals(action)) {
331 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800332 } else if (ACTION_CREATE_GROUP.equals(action)) {
333 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800334 } else if (ACTION_RENAME_GROUP.equals(action)) {
335 renameGroup(intent);
336 } else if (ACTION_DELETE_GROUP.equals(action)) {
337 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700338 } else if (ACTION_UPDATE_GROUP.equals(action)) {
339 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800340 } else if (ACTION_SET_STARRED.equals(action)) {
341 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800342 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
343 setSuperPrimary(intent);
344 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
345 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800346 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
347 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800348 } else if (ACTION_DELETE_CONTACT.equals(action)) {
349 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700350 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
351 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800352 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
353 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800354 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
355 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700356 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
357 setSendToVoicemail(intent);
358 } else if (ACTION_SET_RINGTONE.equals(action)) {
359 setRingtone(intent);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700360 } else if (ACTION_UNDO.equals(action)) {
361 undo(intent);
Marcus Hagerott819214d2016-09-29 14:58:27 -0700362 } else if (ACTION_IMPORT_FROM_SIM.equals(action)) {
363 importFromSim(intent);
Marcus Hagerott7333c372016-11-07 09:40:20 -0800364 } else if (ACTION_SLEEP.equals(action)) {
365 sleepForDebugging(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700366 }
Marcus Hagerott7333c372016-11-07 09:40:20 -0800367
368 sState.onFinish(intent);
369 notifyStateChanged();
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700370 }
371
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800372 /**
373 * Creates an intent that can be sent to this service to create a new raw contact
374 * using data presented as a set of ContentValues.
375 */
376 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700377 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700378 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800379 Intent serviceIntent = new Intent(
380 context, ContactSaveService.class);
381 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
382 if (account != null) {
383 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
384 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700385 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800386 }
387 serviceIntent.putParcelableArrayListExtra(
388 ContactSaveService.EXTRA_CONTENT_VALUES, values);
389
390 // Callback intent will be invoked by the service once the new contact is
391 // created. The service will put the URI of the new contact as "data" on
392 // the callback intent.
393 Intent callbackIntent = new Intent(context, callbackActivity);
394 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800395 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
396 return serviceIntent;
397 }
398
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700399 private void createRawContact(Intent intent) {
400 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
401 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700402 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700403 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
404 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
405
406 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
407 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
408 .withValue(RawContacts.ACCOUNT_NAME, accountName)
409 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700410 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700411 .build());
412
413 int size = valueList.size();
414 for (int i = 0; i < size; i++) {
415 ContentValues values = valueList.get(i);
416 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
417 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
418 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
419 .withValues(values)
420 .build());
421 }
422
423 ContentResolver resolver = getContentResolver();
424 ContentProviderResult[] results;
425 try {
426 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
427 } catch (Exception e) {
428 throw new RuntimeException("Failed to store new contact", e);
429 }
430
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700431 Uri rawContactUri = results[0].uri;
432 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
433
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800434 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700435 }
436
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700437 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800438 * Creates an intent that can be sent to this service to create a new raw contact
439 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800440 * This variant is more convenient to use when there is only one photo that can
441 * possibly be updated, as in the Contact Details screen.
442 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
443 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800444 */
Maurice Chu851222a2012-06-21 11:43:08 -0700445 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700446 String saveModeExtraKey, int saveMode, boolean isProfile,
447 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700448 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800449 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700450 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800451 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700452 callbackActivity, callbackAction, bundle,
453 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800454 }
455
456 /**
457 * Creates an intent that can be sent to this service to create a new raw contact
458 * using data presented as a set of ContentValues.
459 * This variant is used when multiple contacts' photos may be updated, as in the
460 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700461 *
Josh Garguse692e012012-01-18 14:53:11 -0800462 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700463 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
464 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800465 */
Maurice Chu851222a2012-06-21 11:43:08 -0700466 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700467 String saveModeExtraKey, int saveMode, boolean isProfile,
468 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700469 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000470 Intent serviceIntent = new Intent(
471 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800472 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
473 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700474 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800475 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
476
Josh Garguse692e012012-01-18 14:53:11 -0800477 if (updatedPhotos != null) {
478 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
479 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800480
Josh Garguse5d3f892012-04-11 11:56:15 -0700481 if (callbackActivity != null) {
482 // Callback intent will be invoked by the service once the contact is
483 // saved. The service will put the URI of the new contact as "data" on
484 // the callback intent.
485 Intent callbackIntent = new Intent(context, callbackActivity);
486 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700487 if (joinContactIdExtraKey != null && joinContactId != null) {
488 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
489 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700490 callbackIntent.setAction(callbackAction);
491 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
492 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800493 return serviceIntent;
494 }
495
496 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700497 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700498 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800499 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800500
Jay Shrauner08099782015-03-25 14:17:11 -0700501 if (state == null) {
502 Log.e(TAG, "Invalid arguments for saveContact request");
503 return;
504 }
505
benny.lin3a4e7a22014-01-08 10:58:08 +0800506 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800507 // Trim any empty fields, and RawContacts, before persisting
508 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700509 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800510
511 Uri lookupUri = null;
512
513 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700514
Josh Garguse692e012012-01-18 14:53:11 -0800515 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800516
Josh Gargusef15c8e2012-01-30 16:42:02 -0800517 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
518 long insertedRawContactId = -1;
519
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800520 // Attempt to persist changes
521 int tries = 0;
522 while (tries++ < PERSIST_TRIES) {
523 try {
524 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800525 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
526
527 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
528
529 for (CPOWrapper cpoWrapper : diffWrapper) {
530 diff.add(cpoWrapper.getOperation());
531 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700532
Katherine Kuana007e442011-07-07 09:25:34 -0700533 if (DEBUG) {
534 Log.v(TAG, "Content Provider Operations:");
535 for (ContentProviderOperation operation : diff) {
536 Log.v(TAG, operation.toString());
537 }
538 }
539
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700540 int numberProcessed = 0;
541 boolean batchFailed = false;
542 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
543 while (numberProcessed < diff.size()) {
544 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
545 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700546 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700547 batchFailed = true;
548 break;
549 } else {
550 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700551 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800552 }
553
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700554 if (batchFailed) {
555 // Retry save
556 continue;
557 }
558
Wenyi Wang67addcc2015-11-23 10:07:48 -0800559 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800560 if (rawContactId == -1) {
561 throw new IllegalStateException("Could not determine RawContact ID after save");
562 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800563 // We don't have to check to see if the value is still -1. If we reach here,
564 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800565 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700566 if (isProfile) {
567 // Since the profile supports local raw contacts, which may have been completely
568 // removed if all information was removed, we need to do a special query to
569 // get the lookup URI for the profile contact (if it still exists).
570 Cursor c = resolver.query(Profile.CONTENT_URI,
571 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
572 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800573 if (c == null) {
574 continue;
575 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700576 try {
Erik162b7e32011-09-20 15:23:55 -0700577 if (c.moveToFirst()) {
578 final long contactId = c.getLong(0);
579 final String lookupKey = c.getString(1);
580 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
581 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700582 } finally {
583 c.close();
584 }
585 } else {
586 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
587 rawContactId);
588 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
589 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800590 if (lookupUri != null) {
591 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
592 }
Josh Garguse692e012012-01-18 14:53:11 -0800593
594 // We can change this back to false later, if we fail to save the contact photo.
595 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800596 break;
597
598 } catch (RemoteException e) {
599 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700600 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800601 break;
602
Jay Shrauner57fca182014-01-17 14:20:50 -0800603 } catch (IllegalArgumentException e) {
604 // This is thrown by applyBatch on malformed requests
Walter Jang3a0b4832016-10-12 11:02:54 -0700605 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Jay Shrauner57fca182014-01-17 14:20:50 -0800606 showToast(R.string.contactSavedErrorToast);
607 break;
608
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800609 } catch (OperationApplicationException e) {
610 // Version consistency failed, re-parent change and try again
611 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
612 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
613 boolean first = true;
614 final int count = state.size();
615 for (int i = 0; i < count; i++) {
616 Long rawContactId = state.getRawContactId(i);
617 if (rawContactId != null && rawContactId != -1) {
618 if (!first) {
619 sb.append(',');
620 }
621 sb.append(rawContactId);
622 first = false;
623 }
624 }
625 sb.append(")");
626
627 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800628 throw new IllegalStateException(
629 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800630 }
631
Maurice Chu851222a2012-06-21 11:43:08 -0700632 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700633 isProfile
634 ? RawContactsEntity.PROFILE_CONTENT_URI
635 : RawContactsEntity.CONTENT_URI,
636 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700637 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700638
639 // Update the new state to use profile URIs if appropriate.
640 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700641 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700642 delta.setProfileQueryUri();
643 }
644 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800645 }
646 }
647
Josh Garguse692e012012-01-18 14:53:11 -0800648 // Now save any updated photos. We do this at the end to ensure that
649 // the ContactProvider already knows about newly-created contacts.
650 if (updatedPhotos != null) {
651 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700652 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800653 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800654
655 // If the raw-contact ID is negative, we are saving a new raw-contact;
656 // replace the bogus ID with the new one that we actually saved the contact at.
657 if (rawContactId < 0) {
658 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800659 }
660
Jay Shrauner511561d2015-04-02 10:35:33 -0700661 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700662 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700663 succeeded = false;
664 }
Josh Garguse692e012012-01-18 14:53:11 -0800665 }
666 }
667
Josh Garguse5d3f892012-04-11 11:56:15 -0700668 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
669 if (callbackIntent != null) {
670 if (succeeded) {
671 // Mark the intent to indicate that the save was successful (even if the lookup URI
672 // is now null). For local contacts or the local profile, it's possible that the
673 // save triggered removal of the contact, so no lookup URI would exist..
674 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
675 }
676 callbackIntent.setData(lookupUri);
677 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800678 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800679 }
680
Josh Garguse692e012012-01-18 14:53:11 -0800681 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700682 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
683 * subsets, adds the returned array to "results".
684 *
685 * @return the size of the array, if not null; -1 when the array is null.
686 */
687 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
688 ContentProviderResult[] results, ContentResolver resolver)
689 throws RemoteException, OperationApplicationException {
690 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
691 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
692 subset.addAll(diff.subList(offset, offset + subsetCount));
693 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
694 .AUTHORITY, subset);
695 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
696 return -1;
697 }
698 for (ContentProviderResult c : subsetResult) {
699 results[offset++] = c;
700 }
701 return subsetResult.length;
702 }
703
704 /**
Josh Garguse692e012012-01-18 14:53:11 -0800705 * Save updated photo for the specified raw-contact.
706 * @return true for success, false for failure
707 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800708 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800709 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800710 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
711 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
712
benny.lin3a4e7a22014-01-08 10:58:08 +0800713 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800714 }
715
Josh Gargusef15c8e2012-01-30 16:42:02 -0800716 /**
717 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
718 */
Maurice Chu851222a2012-06-21 11:43:08 -0700719 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800720 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800721 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800722 long existingRawContactId = state.findRawContactId();
723 if (existingRawContactId != -1) {
724 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800725 }
726
Wenyi Wang67addcc2015-11-23 10:07:48 -0800727 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800728 }
729
730 /**
731 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
732 */
733 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800734 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800735 if (results == null) {
736 return -1;
737 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800738 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800739 final int numResults = results.length;
740 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800741 final CPOWrapper cpoWrapper = diffWrapper.get(i);
742 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
743 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
744 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800745 return ContentUris.parseId(results[i].uri);
746 }
747 }
748 return -1;
749 }
750
751 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700752 * Creates an intent that can be sent to this service to create a new group as
753 * well as add new members at the same time.
754 *
755 * @param context of the application
756 * @param account in which the group should be created
757 * @param label is the name of the group (cannot be null)
758 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
759 * should be added to the group
760 * @param callbackActivity is the activity to send the callback intent to
761 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700762 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700763 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700764 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700765 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800766 Intent serviceIntent = new Intent(context, ContactSaveService.class);
767 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
768 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
769 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700770 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800771 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700772 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700773
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800774 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700775 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800776 Intent callbackIntent = new Intent(context, callbackActivity);
777 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700778 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800779
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700780 return serviceIntent;
781 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800782
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800783 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700784 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
785 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
786 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
787 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700788 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800789
Katherine Kuan717e3432011-07-13 17:03:24 -0700790 // Create the new group
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700791 final Uri groupUri = mGroupsDao.create(label,
792 new AccountWithDataSet(accountName, accountType, dataSet));
793 final ContentResolver resolver = getContentResolver();
Katherine Kuan717e3432011-07-13 17:03:24 -0700794
795 // If there's no URI, then the insertion failed. Abort early because group members can't be
796 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800797 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700798 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800799 return;
800 }
801
Katherine Kuan717e3432011-07-13 17:03:24 -0700802 // Add new group members
803 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
804
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700805 ContentValues values = new ContentValues();
Katherine Kuan717e3432011-07-13 17:03:24 -0700806 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
Walter Jang8bac28b2016-08-30 10:34:55 -0700807 // with the way other intent extras that are passed to the
Gary Mai363af602016-09-28 10:01:23 -0700808 // {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800809 values.clear();
810 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
811 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
812
813 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700814 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700815 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800816 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800817 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800818 }
819
820 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800821 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800822 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700823 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700824 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800825 Intent serviceIntent = new Intent(context, ContactSaveService.class);
826 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
827 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
828 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700829
830 // Callback intent will be invoked by the service once the group is renamed.
831 Intent callbackIntent = new Intent(context, callbackActivity);
832 callbackIntent.setAction(callbackAction);
833 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
834
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800835 return serviceIntent;
836 }
837
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800838 private void renameGroup(Intent intent) {
839 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
840 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
841
842 if (groupId == -1) {
843 Log.e(TAG, "Invalid arguments for renameGroup request");
844 return;
845 }
846
847 ContentValues values = new ContentValues();
848 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700849 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
850 getContentResolver().update(groupUri, values, null, null);
851
852 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
853 callbackIntent.setData(groupUri);
854 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800855 }
856
857 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800858 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800859 */
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700860 public static Intent createGroupDeletionIntent(Context context, long groupId) {
861 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800862 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800863 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700864
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800865 return serviceIntent;
866 }
867
868 private void deleteGroup(Intent intent) {
869 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
870 if (groupId == -1) {
871 Log.e(TAG, "Invalid arguments for deleteGroup request");
872 return;
873 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700874 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800875
Marcus Hagerott819214d2016-09-29 14:58:27 -0700876 final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700877 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
878 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
879 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
Walter Jang72f99882016-05-26 09:01:31 -0700880
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700881 mGroupsDao.delete(groupUri);
882
883 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
884 }
885
886 public static Intent createUndoIntent(Context context, Intent resultIntent) {
887 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
888 serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
889 serviceIntent.putExtras(resultIntent);
890 return serviceIntent;
891 }
892
893 private void undo(Intent intent) {
894 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
895 if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
896 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
Walter Jang72f99882016-05-26 09:01:31 -0700897 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800898 }
899
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700900
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800901 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700902 * Creates an intent that can be sent to this service to rename a group as
903 * well as add and remove members from the group.
904 *
905 * @param context of the application
906 * @param groupId of the group that should be modified
907 * @param newLabel is the updated name of the group (can be null if the name
908 * should not be updated)
909 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
910 * should be added to the group
911 * @param rawContactsToRemove is an array of raw contact IDs for contacts
912 * that should be removed from the group
913 * @param callbackActivity is the activity to send the callback intent to
914 * @param callbackAction is the intent action for the callback intent
915 */
916 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
917 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700918 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700919 Intent serviceIntent = new Intent(context, ContactSaveService.class);
920 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
921 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
922 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
923 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
924 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
925 rawContactsToRemove);
926
927 // Callback intent will be invoked by the service once the group is updated
928 Intent callbackIntent = new Intent(context, callbackActivity);
929 callbackIntent.setAction(callbackAction);
930 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
931
932 return serviceIntent;
933 }
934
935 private void updateGroup(Intent intent) {
936 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
937 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
938 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
939 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
940
941 if (groupId == -1) {
942 Log.e(TAG, "Invalid arguments for updateGroup request");
943 return;
944 }
945
946 final ContentResolver resolver = getContentResolver();
947 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
948
949 // Update group name if necessary
950 if (label != null) {
951 ContentValues values = new ContentValues();
952 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700953 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700954 }
955
Katherine Kuan717e3432011-07-13 17:03:24 -0700956 // Add and remove members if necessary
957 addMembersToGroup(resolver, rawContactsToAdd, groupId);
958 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
959
960 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
961 callbackIntent.setData(groupUri);
962 deliverCallback(callbackIntent);
963 }
964
Walter Jang3a0b4832016-10-12 11:02:54 -0700965 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700966 long groupId) {
967 if (rawContactsToAdd == null) {
968 return;
969 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700970 for (long rawContactId : rawContactsToAdd) {
971 try {
972 final ArrayList<ContentProviderOperation> rawContactOperations =
973 new ArrayList<ContentProviderOperation>();
974
975 // Build an assert operation to ensure the contact is not already in the group
976 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
977 .newAssertQuery(Data.CONTENT_URI);
978 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
979 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
980 new String[] { String.valueOf(rawContactId),
981 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
982 assertBuilder.withExpectedCount(0);
983 rawContactOperations.add(assertBuilder.build());
984
985 // Build an insert operation to add the contact to the group
986 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
987 .newInsert(Data.CONTENT_URI);
988 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
989 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
990 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
991 rawContactOperations.add(insertBuilder.build());
992
993 if (DEBUG) {
994 for (ContentProviderOperation operation : rawContactOperations) {
995 Log.v(TAG, operation.toString());
996 }
997 }
998
999 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001000 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -08001001 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001002 }
1003 } catch (RemoteException e) {
1004 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -07001005 FeedbackHelper.sendFeedback(this, TAG,
1006 "Problem persisting user edits for raw contact ID " +
1007 String.valueOf(rawContactId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001008 } catch (OperationApplicationException e) {
1009 // The assert could have failed because the contact is already in the group,
1010 // just continue to the next contact
Walter Jang3a0b4832016-10-12 11:02:54 -07001011 FeedbackHelper.sendFeedback(this, TAG,
1012 "Assert failed in adding raw contact ID " +
1013 String.valueOf(rawContactId) + ". Already exists in group " +
1014 String.valueOf(groupId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001015 }
1016 }
Katherine Kuan717e3432011-07-13 17:03:24 -07001017 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001018
Daniel Lehmann18958a22012-02-28 17:45:25 -08001019 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -07001020 long groupId) {
1021 if (rawContactsToRemove == null) {
1022 return;
1023 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001024 for (long rawContactId : rawContactsToRemove) {
1025 // Apply the delete operation on the data row for the given raw contact's
1026 // membership in the given group. If no contact matches the provided selection, then
1027 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -08001028 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001029 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1030 new String[] { String.valueOf(rawContactId),
1031 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
1032 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001033 }
1034
1035 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001036 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001037 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001038 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
1039 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1040 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
1041 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1042 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
1043
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001044 return serviceIntent;
1045 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001046
1047 private void setStarred(Intent intent) {
1048 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1049 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
1050 if (contactUri == null) {
1051 Log.e(TAG, "Invalid arguments for setStarred request");
1052 return;
1053 }
1054
1055 final ContentValues values = new ContentValues(1);
1056 values.put(Contacts.STARRED, value);
1057 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -07001058
1059 // Undemote the contact if necessary
1060 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
1061 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -08001062 if (c == null) {
1063 return;
1064 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001065 try {
1066 if (c.moveToFirst()) {
1067 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -07001068
1069 // Don't bother undemoting if this contact is the user's profile.
1070 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -08001071 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -07001072 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001073 }
1074 } finally {
1075 c.close();
1076 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001077 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001078
1079 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -07001080 * Creates an intent that can be sent to this service to set the redirect to voicemail.
1081 */
1082 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
1083 boolean value) {
1084 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1085 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1086 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1087 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1088
1089 return serviceIntent;
1090 }
1091
1092 private void setSendToVoicemail(Intent intent) {
1093 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1094 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1095 if (contactUri == null) {
1096 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1097 return;
1098 }
1099
1100 final ContentValues values = new ContentValues(1);
1101 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1102 getContentResolver().update(contactUri, values, null, null);
1103 }
1104
1105 /**
1106 * Creates an intent that can be sent to this service to save the contact's ringtone.
1107 */
1108 public static Intent createSetRingtone(Context context, Uri contactUri,
1109 String value) {
1110 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1111 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1112 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1113 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1114
1115 return serviceIntent;
1116 }
1117
1118 private void setRingtone(Intent intent) {
1119 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1120 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1121 if (contactUri == null) {
1122 Log.e(TAG, "Invalid arguments for setRingtone");
1123 return;
1124 }
1125 ContentValues values = new ContentValues(1);
1126 values.put(Contacts.CUSTOM_RINGTONE, value);
1127 getContentResolver().update(contactUri, values, null, null);
1128 }
1129
1130 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001131 * Creates an intent that sets the selected data item as super primary (default)
1132 */
1133 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1134 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1135 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1136 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1137 return serviceIntent;
1138 }
1139
1140 private void setSuperPrimary(Intent intent) {
1141 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1142 if (dataId == -1) {
1143 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1144 return;
1145 }
1146
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001147 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001148 }
1149
1150 /**
1151 * Creates an intent that clears the primary flag of all data items that belong to the same
1152 * raw_contact as the given data item. Will only clear, if the data item was primary before
1153 * this call
1154 */
1155 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1156 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1157 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1158 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1159 return serviceIntent;
1160 }
1161
1162 private void clearPrimary(Intent intent) {
1163 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1164 if (dataId == -1) {
1165 Log.e(TAG, "Invalid arguments for clearPrimary request");
1166 return;
1167 }
1168
1169 // Update the primary values in the data record.
1170 ContentValues values = new ContentValues(1);
1171 values.put(Data.IS_SUPER_PRIMARY, 0);
1172 values.put(Data.IS_PRIMARY, 0);
1173
1174 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1175 values, null, null);
1176 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001177
1178 /**
1179 * Creates an intent that can be sent to this service to delete a contact.
1180 */
1181 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1182 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1183 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1184 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1185 return serviceIntent;
1186 }
1187
Brian Attwelld2962a32015-03-02 14:48:50 -08001188 /**
1189 * Creates an intent that can be sent to this service to delete multiple contacts.
1190 */
1191 public static Intent createDeleteMultipleContactsIntent(Context context,
James Laskeye5a140a2016-10-18 15:43:42 -07001192 long[] contactIds, final String[] names) {
Brian Attwelld2962a32015-03-02 14:48:50 -08001193 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1194 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1195 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
James Laskeye5a140a2016-10-18 15:43:42 -07001196 serviceIntent.putExtra(ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY, names);
Brian Attwelld2962a32015-03-02 14:48:50 -08001197 return serviceIntent;
1198 }
1199
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001200 private void deleteContact(Intent intent) {
1201 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1202 if (contactUri == null) {
1203 Log.e(TAG, "Invalid arguments for deleteContact request");
1204 return;
1205 }
1206
1207 getContentResolver().delete(contactUri, null, null);
1208 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001209
Brian Attwelld2962a32015-03-02 14:48:50 -08001210 private void deleteMultipleContacts(Intent intent) {
1211 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1212 if (contactIds == null) {
1213 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1214 return;
1215 }
1216 for (long contactId : contactIds) {
1217 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1218 getContentResolver().delete(contactUri, null, null);
1219 }
James Laskeye5a140a2016-10-18 15:43:42 -07001220 final String[] names = intent.getStringArrayExtra(
1221 ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY);
1222 final String deleteToastMessage;
1223 if (names.length == 0) {
1224 deleteToastMessage = getResources().getQuantityString(
1225 R.plurals.contacts_deleted_toast, contactIds.length);
1226 } else if (names.length == 1) {
1227 deleteToastMessage = getResources().getString(
1228 R.string.contacts_deleted_one_named_toast, names);
1229 } else if (names.length == 2) {
1230 deleteToastMessage = getResources().getString(
1231 R.string.contacts_deleted_two_named_toast, names);
1232 } else {
1233 deleteToastMessage = getResources().getString(
1234 R.string.contacts_deleted_many_named_toast, names);
1235 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001236 mMainHandler.post(new Runnable() {
1237 @Override
1238 public void run() {
1239 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1240 .show();
1241 }
1242 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001243 }
1244
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001245 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001246 * Creates an intent that can be sent to this service to split a contact into it's constituent
Gary Mai53fe0d22016-07-26 17:23:53 -07001247 * pieces. This will set the raw contact ids to TYPE_AUTOMATIC for AggregationExceptions so
1248 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001249 */
1250 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1251 ResultReceiver receiver) {
1252 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1253 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1254 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1255 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1256 return serviceIntent;
1257 }
1258
1259 private void splitContact(Intent intent) {
1260 final long rawContactIds[][] = (long[][]) intent
1261 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001262 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Mai7efa9942016-05-12 11:26:49 -07001263 if (rawContactIds == null) {
1264 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001265 if (receiver != null) {
1266 receiver.send(BAD_ARGUMENTS, new Bundle());
1267 }
Gary Mai7efa9942016-05-12 11:26:49 -07001268 return;
1269 }
1270 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1271 final ContentResolver resolver = getContentResolver();
1272 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001273 for (int i = 0; i < rawContactIds.length; i++) {
1274 for (int j = 0; j < rawContactIds.length; j++) {
1275 if (i != j) {
1276 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
1277 if (receiver != null) {
1278 receiver.send(CP2_ERROR, new Bundle());
1279 return;
1280 }
1281 }
1282 }
1283 }
1284 }
1285 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1286 if (receiver != null) {
1287 receiver.send(CP2_ERROR, new Bundle());
1288 }
1289 return;
1290 }
1291 if (receiver != null) {
1292 receiver.send(CONTACTS_SPLIT, new Bundle());
1293 } else {
1294 showToast(R.string.contactUnlinkedToast);
1295 }
1296 }
1297
1298 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001299 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001300 * and {@param rawContactIds2} to {@param operations}.
1301 * @return false if an error occurred, true otherwise.
1302 */
1303 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
1304 long[] rawContactIds1, long[] rawContactIds2) {
1305 if (rawContactIds1 == null || rawContactIds2 == null) {
1306 Log.e(TAG, "Invalid arguments for splitContact request");
1307 return false;
1308 }
1309 // For each pair of raw contacts, insert an aggregation exception
1310 final ContentResolver resolver = getContentResolver();
1311 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1312 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1313 for (int i = 0; i < rawContactIds1.length; i++) {
1314 for (int j = 0; j < rawContactIds2.length; j++) {
1315 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
1316 // Before we get to 500 we need to flush the operations list
1317 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1318 if (!applyOperations(resolver, operations)) {
1319 return false;
1320 }
1321 operations.clear();
1322 }
1323 }
1324 }
1325 return true;
1326 }
1327
1328 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001329 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001330 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001331 */
1332 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001333 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001334 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1335 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1336 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1337 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001338
1339 // Callback intent will be invoked by the service once the contacts are joined.
1340 Intent callbackIntent = new Intent(context, callbackActivity);
1341 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001342 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1343
1344 return serviceIntent;
1345 }
1346
Brian Attwelld3946ca2015-03-03 11:13:49 -08001347 /**
1348 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1349 * No special attention is paid to where the resulting contact's name is taken from.
1350 */
Gary Mai7efa9942016-05-12 11:26:49 -07001351 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1352 ResultReceiver receiver) {
1353 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001354 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1355 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001356 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001357 return serviceIntent;
1358 }
1359
Gary Mai7efa9942016-05-12 11:26:49 -07001360 /**
1361 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1362 * No special attention is paid to where the resulting contact's name is taken from.
1363 */
1364 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1365 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1366 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001367
1368 private interface JoinContactQuery {
1369 String[] PROJECTION = {
1370 RawContacts._ID,
1371 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001372 RawContacts.DISPLAY_NAME_SOURCE,
1373 };
1374
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001375 int _ID = 0;
1376 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001377 int DISPLAY_NAME_SOURCE = 2;
1378 }
1379
1380 private interface ContactEntityQuery {
1381 String[] PROJECTION = {
1382 Contacts.Entity.DATA_ID,
1383 Contacts.Entity.CONTACT_ID,
1384 Contacts.Entity.IS_SUPER_PRIMARY,
1385 };
1386 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1387 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1388 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1389 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1390
1391 int DATA_ID = 0;
1392 int CONTACT_ID = 1;
1393 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001394 }
1395
Brian Attwelld3946ca2015-03-03 11:13:49 -08001396 private void joinSeveralContacts(Intent intent) {
1397 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001398
Gary Mai7efa9942016-05-12 11:26:49 -07001399 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001400
Brian Attwelld3946ca2015-03-03 11:13:49 -08001401 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001402 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1403 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001404 if (rawContactIds == null) {
1405 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001406 if (receiver != null) {
1407 receiver.send(BAD_ARGUMENTS, new Bundle());
1408 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001409 return;
1410 }
1411
Brian Attwelld3946ca2015-03-03 11:13:49 -08001412 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001413 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001414 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1415 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1416 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001417 for (int i = 0; i < rawContactIds.length; i++) {
1418 for (int j = 0; j < rawContactIds.length; j++) {
1419 if (i != j) {
1420 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1421 }
Walter Jang0653de32015-07-24 12:12:40 -07001422 // Before we get to 500 we need to flush the operations list
1423 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001424 if (!applyOperations(resolver, operations)) {
1425 if (receiver != null) {
1426 receiver.send(CP2_ERROR, new Bundle());
1427 }
Walter Jang0653de32015-07-24 12:12:40 -07001428 return;
1429 }
1430 operations.clear();
1431 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001432 }
1433 }
Gary Mai7efa9942016-05-12 11:26:49 -07001434 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1435 if (receiver != null) {
1436 receiver.send(CP2_ERROR, new Bundle());
1437 }
Walter Jang0653de32015-07-24 12:12:40 -07001438 return;
1439 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001440
John Shaoa3c507a2016-09-13 14:26:17 -07001441
1442 final String name = queryNameOfLinkedContacts(contactIds);
1443 if (name != null) {
1444 if (receiver != null) {
1445 final Bundle result = new Bundle();
1446 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1447 result.putString(EXTRA_DISPLAY_NAME, name);
1448 receiver.send(CONTACTS_LINKED, result);
1449 } else {
James Laskeyf62b4882016-10-21 11:36:40 -07001450 if (TextUtils.isEmpty(name)) {
1451 showToast(R.string.contactsJoinedMessage);
1452 } else {
1453 showToast(R.string.contactsJoinedNamedMessage, name);
1454 }
John Shaoa3c507a2016-09-13 14:26:17 -07001455 }
Gary Mai7efa9942016-05-12 11:26:49 -07001456 } else {
John Shaoa3c507a2016-09-13 14:26:17 -07001457 if (receiver != null) {
1458 receiver.send(CP2_ERROR, new Bundle());
1459 }
1460 showToast(R.string.contactJoinErrorToast);
Gary Mai7efa9942016-05-12 11:26:49 -07001461 }
Walter Jang0653de32015-07-24 12:12:40 -07001462 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001463
John Shaoa3c507a2016-09-13 14:26:17 -07001464 /** Get the display name of the top-level contact after the contacts have been linked. */
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001465 private String queryNameOfLinkedContacts(long[] contactIds) {
1466 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1467 final String[] whereArgs = new String[contactIds.length];
1468 for (int i = 0; i < contactIds.length; i++) {
1469 whereArgs[i] = String.valueOf(contactIds[i]);
1470 whereBuilder.append("?,");
1471 }
1472 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1473 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
James Laskeyf62b4882016-10-21 11:36:40 -07001474 new String[]{Contacts._ID, Contacts.DISPLAY_NAME,
1475 Contacts.DISPLAY_NAME_ALTERNATIVE},
John Shaoa3c507a2016-09-13 14:26:17 -07001476 whereBuilder.toString(), whereArgs, null);
1477
1478 String name = null;
James Laskeyf62b4882016-10-21 11:36:40 -07001479 String nameAlt = null;
John Shaoa3c507a2016-09-13 14:26:17 -07001480 long contactId = 0;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001481 try {
1482 if (cursor.moveToFirst()) {
John Shaoa3c507a2016-09-13 14:26:17 -07001483 contactId = cursor.getLong(0);
1484 name = cursor.getString(1);
James Laskeyf62b4882016-10-21 11:36:40 -07001485 nameAlt = cursor.getString(2);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001486 }
John Shaoa3c507a2016-09-13 14:26:17 -07001487 while(cursor.moveToNext()) {
1488 if (cursor.getLong(0) != contactId) {
1489 return null;
1490 }
1491 }
James Laskeyf62b4882016-10-21 11:36:40 -07001492
1493 final String formattedName = ContactDisplayUtils.getPreferredDisplayName(name, nameAlt,
1494 new ContactsPreferences(getApplicationContext()));
1495 return formattedName == null ? "" : formattedName;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001496 } finally {
John Shaoa3c507a2016-09-13 14:26:17 -07001497 if (cursor != null) {
1498 cursor.close();
1499 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001500 }
1501 }
1502
Walter Jang0653de32015-07-24 12:12:40 -07001503 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001504 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001505 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001506 try {
John Shaoa3c507a2016-09-13 14:26:17 -07001507 final ContentProviderResult[] result =
1508 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1509 for (int i = 0; i < result.length; ++i) {
1510 // if no rows were modified in the operation then we count it as fail.
1511 if (result[i].count < 0) {
1512 throw new OperationApplicationException();
1513 }
1514 }
Walter Jang0653de32015-07-24 12:12:40 -07001515 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001516 } catch (RemoteException | OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001517 FeedbackHelper.sendFeedback(this, TAG,
1518 "Failed to apply aggregation exception batch", e);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001519 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001520 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001521 }
1522 }
1523
Brian Attwelld3946ca2015-03-03 11:13:49 -08001524 private void joinContacts(Intent intent) {
1525 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1526 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001527
1528 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001529 // in the join UIs.
1530 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1531 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001532 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001533 return;
1534 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001535
Brian Attwell548f5c62015-01-27 17:46:46 -08001536 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001537
1538 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001539 for (int i = 0; i < rawContactIds.length; i++) {
1540 for (int j = 0; j < rawContactIds.length; j++) {
1541 if (i != j) {
1542 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1543 }
1544 }
1545 }
1546
Brian Attwelld3946ca2015-03-03 11:13:49 -08001547 final ContentResolver resolver = getContentResolver();
1548
Brian Attwell548f5c62015-01-27 17:46:46 -08001549 // Use the name for contactId1 as the name for the newly aggregated contact.
1550 final Uri contactId1Uri = ContentUris.withAppendedId(
1551 Contacts.CONTENT_URI, contactId1);
1552 final Uri entityUri = Uri.withAppendedPath(
1553 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1554 Cursor c = resolver.query(entityUri,
1555 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1556 if (c == null) {
1557 Log.e(TAG, "Unable to open Contacts DB cursor");
1558 showToast(R.string.contactSavedErrorToast);
1559 return;
1560 }
1561 long dataIdToAddSuperPrimary = -1;
1562 try {
1563 if (c.moveToFirst()) {
1564 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1565 }
1566 } finally {
1567 c.close();
1568 }
1569
1570 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1571 // display name does not change as a result of the join.
1572 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001573 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001574 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1575 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1576 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001577 operations.add(builder.build());
1578 }
1579
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001580 // Apply all aggregation exceptions as one batch
John Shaoa3c507a2016-09-13 14:26:17 -07001581 final boolean success = applyOperations(resolver, operations);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001582
John Shaoa3c507a2016-09-13 14:26:17 -07001583 final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2});
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001584 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
John Shaoa3c507a2016-09-13 14:26:17 -07001585 if (success && name != null) {
James Laskeyf62b4882016-10-21 11:36:40 -07001586 if (TextUtils.isEmpty(name)) {
1587 showToast(R.string.contactsJoinedMessage);
1588 } else {
1589 showToast(R.string.contactsJoinedNamedMessage, name);
1590 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001591 Uri uri = RawContacts.getContactLookupUri(resolver,
1592 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1593 callbackIntent.setData(uri);
1594 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001595 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001596 }
1597
Gary Mai7efa9942016-05-12 11:26:49 -07001598 /**
1599 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1600 * array of the return value holds an array of raw contact ids for one contactId.
1601 * @param contactIds
1602 * @return
1603 */
1604 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1605 final long[][] rawContactIds = new long[contactIds.length][];
1606 for (int i = 0; i < contactIds.length; i++) {
1607 rawContactIds[i] = getRawContactIds(contactIds[i]);
1608 }
1609 return rawContactIds;
1610 }
1611
1612 /**
1613 * Gets the raw contact ids associated with {@param contactId}.
1614 * @param contactId
1615 * @return Array of raw contact ids.
1616 */
1617 private long[] getRawContactIds(long contactId) {
1618 final ContentResolver resolver = getContentResolver();
1619 long rawContactIds[];
1620
1621 final StringBuilder queryBuilder = new StringBuilder();
1622 queryBuilder.append(RawContacts.CONTACT_ID)
1623 .append("=")
1624 .append(String.valueOf(contactId));
1625
1626 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1627 JoinContactQuery.PROJECTION,
1628 queryBuilder.toString(),
1629 null, null);
1630 if (c == null) {
1631 Log.e(TAG, "Unable to open Contacts DB cursor");
1632 return null;
1633 }
1634 try {
1635 rawContactIds = new long[c.getCount()];
1636 for (int i = 0; i < rawContactIds.length; i++) {
1637 c.moveToPosition(i);
1638 final long rawContactId = c.getLong(JoinContactQuery._ID);
1639 rawContactIds[i] = rawContactId;
1640 }
1641 } finally {
1642 c.close();
1643 }
1644 return rawContactIds;
1645 }
1646
Brian Attwelld3946ca2015-03-03 11:13:49 -08001647 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1648 if (contactIds == null) {
1649 return null;
1650 }
1651
Brian Attwell548f5c62015-01-27 17:46:46 -08001652 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001653
1654 final StringBuilder queryBuilder = new StringBuilder();
1655 final String stringContactIds[] = new String[contactIds.length];
1656 for (int i = 0; i < contactIds.length; i++) {
1657 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1658 stringContactIds[i] = String.valueOf(contactIds[i]);
1659 if (contactIds[i] == -1) {
1660 return null;
1661 }
1662 if (i == contactIds.length -1) {
1663 break;
1664 }
1665 queryBuilder.append(" OR ");
1666 }
1667
Brian Attwell548f5c62015-01-27 17:46:46 -08001668 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1669 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001670 queryBuilder.toString(),
1671 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001672 if (c == null) {
1673 Log.e(TAG, "Unable to open Contacts DB cursor");
1674 showToast(R.string.contactSavedErrorToast);
1675 return null;
1676 }
Gary Mai7efa9942016-05-12 11:26:49 -07001677 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001678 try {
1679 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001680 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001681 return null;
1682 }
1683 rawContactIds = new long[c.getCount()];
1684 for (int i = 0; i < rawContactIds.length; i++) {
1685 c.moveToPosition(i);
1686 long rawContactId = c.getLong(JoinContactQuery._ID);
1687 rawContactIds[i] = rawContactId;
1688 }
1689 } finally {
1690 c.close();
1691 }
1692 return rawContactIds;
1693 }
1694
Brian Attwelld3946ca2015-03-03 11:13:49 -08001695 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1696 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1697 }
1698
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001699 /**
1700 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1701 */
1702 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1703 long rawContactId1, long rawContactId2) {
1704 Builder builder =
1705 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1706 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1707 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1708 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1709 operations.add(builder.build());
1710 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001711
1712 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001713 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} ContentProviderOperation.
Gary Mai7efa9942016-05-12 11:26:49 -07001714 */
1715 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
1716 long rawContactId1, long rawContactId2) {
1717 final Builder builder =
1718 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Mai53fe0d22016-07-26 17:23:53 -07001719 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001720 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1721 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1722 operations.add(builder.build());
1723 }
1724
Marcus Hagerott7333c372016-11-07 09:40:20 -08001725 /**
1726 * Returns an intent that can be used to import the contacts into targetAccount.
1727 *
1728 * @param context context to use for creating the intent
1729 * @param subscriptionId the subscriptionId of the SIM card that is being imported. See
1730 * {@link SubscriptionInfo#getSubscriptionId()}. Upon completion the
1731 * SIM for that subscription ID will be marked as imported
1732 * @param contacts the contacts to import
1733 * @param targetAccount the account import the contacts into
1734 */
1735 public static Intent createImportFromSimIntent(Context context, int subscriptionId,
1736 ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
Marcus Hagerott819214d2016-09-29 14:58:27 -07001737 return new Intent(context, ContactSaveService.class)
1738 .setAction(ACTION_IMPORT_FROM_SIM)
1739 .putExtra(EXTRA_SIM_CONTACTS, contacts)
Marcus Hagerott7333c372016-11-07 09:40:20 -08001740 .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId)
1741 .putExtra(EXTRA_ACCOUNT, targetAccount);
Marcus Hagerott819214d2016-09-29 14:58:27 -07001742 }
1743
1744 private void importFromSim(Intent intent) {
1745 final Intent result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
1746 .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, System.currentTimeMillis());
Marcus Hagerott7333c372016-11-07 09:40:20 -08001747 final int subscriptionId = intent.getIntExtra(EXTRA_SIM_SUBSCRIPTION_ID,
1748 SimCard.NO_SUBSCRIPTION_ID);
Marcus Hagerott819214d2016-09-29 14:58:27 -07001749 try {
1750 final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
1751 final ArrayList<SimContact> contacts =
1752 intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
1753 mSimContactDao.importContacts(contacts, targetAccount);
Marcus Hagerott7333c372016-11-07 09:40:20 -08001754
1755 // Update the imported state of the SIM card that was imported
1756 final SimCard sim = mSimContactDao.getSimBySubscriptionId(subscriptionId);
1757 if (sim != null) {
1758 mSimContactDao.persistSimState(sim.withImportedState(true));
1759 }
1760
Marcus Hagerott819214d2016-09-29 14:58:27 -07001761 // notify success
1762 LocalBroadcastManager.getInstance(this).sendBroadcast(result
1763 .putExtra(EXTRA_RESULT_COUNT, contacts.size())
Marcus Hagerott66e8b222016-10-23 15:41:55 -07001764 .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS)
Marcus Hagerott7333c372016-11-07 09:40:20 -08001765 .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId));
Marcus Hagerott819214d2016-09-29 14:58:27 -07001766 if (Log.isLoggable(TAG, Log.DEBUG)) {
1767 Log.d(TAG, "importFromSim completed successfully");
1768 }
1769 } catch (RemoteException|OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001770 FeedbackHelper.sendFeedback(this, TAG, "Failed to import contacts from SIM card", e);
Marcus Hagerott819214d2016-09-29 14:58:27 -07001771 LocalBroadcastManager.getInstance(this).sendBroadcast(result
Marcus Hagerott7333c372016-11-07 09:40:20 -08001772 .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE)
1773 .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId));
1774 }
1775 }
1776
1777 /**
1778 * Returns an intent that can start this service and cause it to sleep for the specified time.
1779 *
1780 * This exists purely for debugging and manual testing. Since this service uses a single thread
1781 * it is useful to have a way to test behavior when work is queued up and most of the other
1782 * operations complete too quickly to simulate that under normal conditions.
1783 */
1784 public static Intent createSleepIntent(Context context, long millis) {
1785 return new Intent(context, ContactSaveService.class).setAction(ACTION_SLEEP)
1786 .putExtra(EXTRA_SLEEP_DURATION, millis);
1787 }
1788
1789 private void sleepForDebugging(Intent intent) {
1790 long duration = intent.getLongExtra(EXTRA_SLEEP_DURATION, 1000);
1791 if (Log.isLoggable(TAG, Log.DEBUG)) {
1792 Log.d(TAG, "sleeping for " + duration + "ms");
1793 }
1794 try {
1795 Thread.sleep(duration);
1796 } catch (InterruptedException e) {
1797 e.printStackTrace();
1798 }
1799 if (Log.isLoggable(TAG, Log.DEBUG)) {
1800 Log.d(TAG, "finished sleeping");
Marcus Hagerott819214d2016-09-29 14:58:27 -07001801 }
1802 }
1803
Gary Mai7efa9942016-05-12 11:26:49 -07001804 /**
James Laskeyf62b4882016-10-21 11:36:40 -07001805 * Shows a toast on the UI thread by formatting messageId using args.
1806 * @param messageId id of message string
1807 * @param args args to format string
1808 */
1809 private void showToast(final int messageId, final Object... args) {
1810 final String message = getResources().getString(messageId, args);
1811 mMainHandler.post(new Runnable() {
1812 @Override
1813 public void run() {
1814 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1815 }
1816 });
1817 }
1818
1819
1820 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001821 * Shows a toast on the UI thread.
1822 */
1823 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001824 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001825
1826 @Override
1827 public void run() {
1828 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1829 }
1830 });
1831 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001832
1833 private void deliverCallback(final Intent callbackIntent) {
1834 mMainHandler.post(new Runnable() {
1835
1836 @Override
1837 public void run() {
1838 deliverCallbackOnUiThread(callbackIntent);
1839 }
1840 });
1841 }
1842
1843 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1844 // TODO: this assumes that if there are multiple instances of the same
1845 // activity registered, the last one registered is the one waiting for
1846 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001847 for (Listener listener : sListeners) {
1848 if (callbackIntent.getComponent().equals(
1849 ((Activity) listener).getIntent().getComponent())) {
1850 listener.onServiceCompleted(callbackIntent);
1851 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001852 }
1853 }
1854 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001855
1856 public interface GroupsDao {
1857 Uri create(String title, AccountWithDataSet account);
1858 int delete(Uri groupUri);
1859 Bundle captureDeletionUndoData(Uri groupUri);
1860 Uri undoDeletion(Bundle undoData);
1861 }
1862
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001863 public static class GroupsDaoImpl implements GroupsDao {
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001864 public static final String KEY_GROUP_DATA = "groupData";
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001865 public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
1866
1867 private static final String TAG = "GroupsDao";
1868 private final Context context;
1869 private final ContentResolver contentResolver;
1870
1871 public GroupsDaoImpl(Context context) {
1872 this(context, context.getContentResolver());
1873 }
1874
1875 public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
1876 this.context = context;
1877 this.contentResolver = contentResolver;
1878 }
1879
1880 public Bundle captureDeletionUndoData(Uri groupUri) {
1881 final long groupId = ContentUris.parseId(groupUri);
1882 final Bundle result = new Bundle();
1883
1884 final Cursor cursor = contentResolver.query(groupUri,
1885 new String[]{
1886 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
1887 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
1888 Groups.SHOULD_SYNC
1889 },
1890 Groups.DELETED + "=?", new String[] { "0" }, null);
1891 try {
1892 if (cursor.moveToFirst()) {
1893 final ContentValues groupValues = new ContentValues();
1894 DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
1895 result.putParcelable(KEY_GROUP_DATA, groupValues);
1896 } else {
1897 // Group doesn't exist.
1898 return result;
1899 }
1900 } finally {
1901 cursor.close();
1902 }
1903
1904 final Cursor membersCursor = contentResolver.query(
1905 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
1906 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1907 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
1908 final long[] memberIds = new long[membersCursor.getCount()];
1909 int i = 0;
1910 while (membersCursor.moveToNext()) {
1911 memberIds[i++] = membersCursor.getLong(0);
1912 }
1913 result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
1914 return result;
1915 }
1916
1917 public Uri undoDeletion(Bundle deletedGroupData) {
1918 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
1919 if (groupData == null) {
1920 return null;
1921 }
1922 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
1923 final long groupId = ContentUris.parseId(groupUri);
1924
1925 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
1926 if (memberIds == null) {
1927 return groupUri;
1928 }
1929 final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
1930 for (int i = 0; i < memberIds.length; i++) {
1931 memberInsertions[i] = new ContentValues();
1932 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
1933 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1934 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
1935 }
1936 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
1937 if (inserted != memberIds.length) {
1938 Log.e(TAG, "Could not recover some members for group deletion undo");
1939 }
1940
1941 return groupUri;
1942 }
1943
1944 public Uri create(String title, AccountWithDataSet account) {
1945 final ContentValues values = new ContentValues();
1946 values.put(Groups.TITLE, title);
1947 values.put(Groups.ACCOUNT_NAME, account.name);
1948 values.put(Groups.ACCOUNT_TYPE, account.type);
1949 values.put(Groups.DATA_SET, account.dataSet);
1950 return contentResolver.insert(Groups.CONTENT_URI, values);
1951 }
1952
1953 public int delete(Uri groupUri) {
1954 return contentResolver.delete(groupUri, null, null);
1955 }
1956 }
Marcus Hagerott7333c372016-11-07 09:40:20 -08001957
1958 /**
1959 * Keeps track of which operations have been requested but have not yet finished for this
1960 * service.
1961 */
1962 public static class State {
1963 private final CopyOnWriteArrayList<Intent> mPending;
1964
1965 public State() {
1966 mPending = new CopyOnWriteArrayList<>();
1967 }
1968
1969 public State(Collection<Intent> pendingActions) {
1970 mPending = new CopyOnWriteArrayList<>(pendingActions);
1971 }
1972
1973 public boolean isIdle() {
1974 return mPending.isEmpty();
1975 }
1976
1977 public Intent getCurrentIntent() {
1978 return mPending.isEmpty() ? null : mPending.get(0);
1979 }
1980
1981 /**
1982 * Returns the first intent requested that has the specified action or null if no intent
1983 * with that action has been requested.
1984 */
1985 public Intent getNextIntentWithAction(String action) {
1986 for (Intent intent : mPending) {
1987 if (action.equals(intent.getAction())) {
1988 return intent;
1989 }
1990 }
1991 return null;
1992 }
1993
1994 public boolean isActionPending(String action) {
1995 return getNextIntentWithAction(action) != null;
1996 }
1997
1998 private void onFinish(Intent intent) {
1999 if (mPending.isEmpty()) {
2000 return;
2001 }
2002 final String action = mPending.get(0).getAction();
2003 if (action.equals(intent.getAction())) {
2004 mPending.remove(0);
2005 }
2006 }
2007
2008 private void onStart(Intent intent) {
2009 if (intent.getAction() == null) {
2010 return;
2011 }
2012 mPending.add(intent);
2013 }
2014 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07002015}