blob: 24bcbfd97999db2010b53b5ec398987b204bca61 [file] [log] [blame]
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080017package com.android.contacts;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070018
Gary Maib9065dd2016-11-08 10:49:00 -080019import static android.Manifest.permission.WRITE_CONTACTS;
20
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080021import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070022import android.app.IntentService;
23import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080024import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070025import android.content.ContentProviderResult;
26import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080027import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070028import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080029import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070030import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080031import android.content.OperationApplicationException;
32import android.database.Cursor;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070033import android.database.DatabaseUtils;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070034import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080035import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080036import android.os.Handler;
37import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080038import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080039import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070040import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080041import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080042import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080043import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080044import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070045import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080046import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070047import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070048import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070049import android.provider.ContactsContract.RawContactsEntity;
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;
Gary Maib9065dd2016-11-08 10:49:00 -080075
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070076import com.google.common.collect.Lists;
77import com.google.common.collect.Sets;
78
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080079import java.util.ArrayList;
Marcus Hagerott7333c372016-11-07 09:40:20 -080080import java.util.Collection;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080081import java.util.HashSet;
82import java.util.List;
83import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070084
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080085/**
86 * A service responsible for saving changes to the content provider.
87 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070088public class ContactSaveService extends IntentService {
89 private static final String TAG = "ContactSaveService";
90
Katherine Kuana007e442011-07-07 09:25:34 -070091 /** Set to true in order to view logs on content provider operations */
92 private static final boolean DEBUG = false;
93
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070094 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
95
96 public static final String EXTRA_ACCOUNT_NAME = "accountName";
97 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070098 public static final String EXTRA_DATA_SET = "dataSet";
Marcus Hagerott819214d2016-09-29 14:58:27 -070099 public static final String EXTRA_ACCOUNT = "account";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700100 public static final String EXTRA_CONTENT_VALUES = "contentValues";
101 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
Gary Mai7efa9942016-05-12 11:26:49 -0700102 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
103 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700104
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800105 public static final String ACTION_SAVE_CONTACT = "saveContact";
106 public static final String EXTRA_CONTACT_STATE = "state";
107 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700108 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -0700109 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -0800110 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700111
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800112 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800113 public static final String ACTION_RENAME_GROUP = "renameGroup";
114 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700115 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800116 public static final String EXTRA_GROUP_ID = "groupId";
117 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700118 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
119 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800120
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800121 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800122 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800123 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800124 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800125 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800126 public static final String EXTRA_STARRED_FLAG = "starred";
Marcus Hagerott3bb85142016-07-29 10:46:36 -0700127 public static final String EXTRA_DISPLAY_NAME = "extraDisplayName";
James Laskeye5a140a2016-10-18 15:43:42 -0700128 public static final String EXTRA_DISPLAY_NAME_ARRAY = "extraDisplayNameArray";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800129
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800130 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
131 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
132 public static final String EXTRA_DATA_ID = "dataId";
133
Gary Mai7efa9942016-05-12 11:26:49 -0700134 public static final String ACTION_SPLIT_CONTACT = "splitContact";
Gary Maib9065dd2016-11-08 10:49:00 -0800135 public static final String EXTRA_HARD_SPLIT = "extraHardSplit";
Gary Mai7efa9942016-05-12 11:26:49 -0700136
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";
Gary Maib9065dd2016-11-08 10:49:00 -0800162 public static final String BROADCAST_LINK_COMPLETE = "linkComplete";
163 public static final String BROADCAST_UNLINK_COMPLETE = "unlinkComplete";
Marcus Hagerott7333c372016-11-07 09:40:20 -0800164
165 public static final String BROADCAST_SERVICE_STATE_CHANGED = "serviceStateChanged";
Marcus Hagerott819214d2016-09-29 14:58:27 -0700166
167 public static final String EXTRA_RESULT_CODE = "resultCode";
168 public static final String EXTRA_RESULT_COUNT = "count";
169 public static final String EXTRA_OPERATION_REQUESTED_AT_TIME = "requestedTime";
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700170
Gary Mai7efa9942016-05-12 11:26:49 -0700171 public static final int CP2_ERROR = 0;
172 public static final int CONTACTS_LINKED = 1;
173 public static final int CONTACTS_SPLIT = 2;
Gary Mai31d572e2016-06-03 14:04:32 -0700174 public static final int BAD_ARGUMENTS = 3;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700175 public static final int RESULT_UNKNOWN = 0;
176 public static final int RESULT_SUCCESS = 1;
177 public static final int RESULT_FAILURE = 2;
Gary Mai7efa9942016-05-12 11:26:49 -0700178
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700179 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
180 Data.MIMETYPE,
181 Data.IS_PRIMARY,
182 Data.DATA1,
183 Data.DATA2,
184 Data.DATA3,
185 Data.DATA4,
186 Data.DATA5,
187 Data.DATA6,
188 Data.DATA7,
189 Data.DATA8,
190 Data.DATA9,
191 Data.DATA10,
192 Data.DATA11,
193 Data.DATA12,
194 Data.DATA13,
195 Data.DATA14,
196 Data.DATA15
197 );
198
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800199 private static final int PERSIST_TRIES = 3;
200
Walter Jang0653de32015-07-24 12:12:40 -0700201 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
202
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800203 public interface Listener {
204 public void onServiceCompleted(Intent callbackIntent);
205 }
206
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100207 private static final CopyOnWriteArrayList<Listener> sListeners =
208 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800209
Marcus Hagerott7333c372016-11-07 09:40:20 -0800210 // Holds the current state of the service
211 private static final State sState = new State();
212
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800213 private Handler mMainHandler;
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700214 private GroupsDao mGroupsDao;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700215 private SimContactDao mSimContactDao;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800216
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700217 public ContactSaveService() {
218 super(TAG);
219 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800220 mMainHandler = new Handler(Looper.getMainLooper());
221 }
222
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700223 @Override
224 public void onCreate() {
225 super.onCreate();
226 mGroupsDao = new GroupsDaoImpl(this);
Marcus Hagerott66e8b222016-10-23 15:41:55 -0700227 mSimContactDao = SimContactDao.create(this);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700228 }
229
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800230 public static void registerListener(Listener listener) {
231 if (!(listener instanceof Activity)) {
232 throw new ClassCastException("Only activities can be registered to"
233 + " receive callback from " + ContactSaveService.class.getName());
234 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100235 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800236 }
237
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700238 public static boolean canUndo(Intent resultIntent) {
239 return resultIntent.hasExtra(EXTRA_UNDO_DATA);
240 }
241
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800242 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100243 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700244 }
245
Marcus Hagerott7333c372016-11-07 09:40:20 -0800246 public static State getState() {
247 return sState;
248 }
249
250 private void notifyStateChanged() {
251 LocalBroadcastManager.getInstance(this)
252 .sendBroadcast(new Intent(BROADCAST_SERVICE_STATE_CHANGED));
253 }
254
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800255 /**
256 * Returns true if the ContactSaveService was started successfully and false if an exception
257 * was thrown and a Toast error message was displayed.
258 */
259 public static boolean startService(Context context, Intent intent, int saveMode) {
260 try {
261 context.startService(intent);
262 } catch (Exception exception) {
263 final int resId;
264 switch (saveMode) {
Gary Mai363af602016-09-28 10:01:23 -0700265 case ContactEditorActivity.ContactEditor.SaveMode.SPLIT:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800266 resId = R.string.contactUnlinkErrorToast;
267 break;
Gary Mai363af602016-09-28 10:01:23 -0700268 case ContactEditorActivity.ContactEditor.SaveMode.RELOAD:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800269 resId = R.string.contactJoinErrorToast;
270 break;
Gary Mai363af602016-09-28 10:01:23 -0700271 case ContactEditorActivity.ContactEditor.SaveMode.CLOSE:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800272 resId = R.string.contactSavedErrorToast;
273 break;
274 default:
275 resId = R.string.contactGenericErrorToast;
276 }
277 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
278 return false;
279 }
280 return true;
281 }
282
283 /**
284 * Utility method that starts service and handles exception.
285 */
286 public static void startService(Context context, Intent intent) {
287 try {
288 context.startService(intent);
289 } catch (Exception exception) {
290 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
291 }
292 }
293
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700294 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800295 public Object getSystemService(String name) {
296 Object service = super.getSystemService(name);
297 if (service != null) {
298 return service;
299 }
300
301 return getApplicationContext().getSystemService(name);
302 }
303
Marcus Hagerott7333c372016-11-07 09:40:20 -0800304 // Parent classes Javadoc says not to override this method but we're doing it just to update
305 // our state which should be OK since we're still doing the work in onHandleIntent
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800306 @Override
Marcus Hagerott7333c372016-11-07 09:40:20 -0800307 public int onStartCommand(Intent intent, int flags, int startId) {
308 sState.onStart(intent);
309 notifyStateChanged();
310 return super.onStartCommand(intent, flags, startId);
311 }
312
313 @Override
314 protected void onHandleIntent(final Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800315 if (intent == null) {
316 Log.d(TAG, "onHandleIntent: could not handle null intent");
317 return;
318 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700319 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
320 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
321 // TODO: add more specific error string such as "Turn on Contacts
322 // permission to update your contacts"
323 showToast(R.string.contactSavedErrorToast);
324 return;
325 }
326
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700327 // Call an appropriate method. If we're sure it affects how incoming phone calls are
328 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700329 String action = intent.getAction();
330 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
331 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800332 } else if (ACTION_SAVE_CONTACT.equals(action)) {
333 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800334 } else if (ACTION_CREATE_GROUP.equals(action)) {
335 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800336 } else if (ACTION_RENAME_GROUP.equals(action)) {
337 renameGroup(intent);
338 } else if (ACTION_DELETE_GROUP.equals(action)) {
339 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700340 } else if (ACTION_UPDATE_GROUP.equals(action)) {
341 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800342 } else if (ACTION_SET_STARRED.equals(action)) {
343 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800344 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
345 setSuperPrimary(intent);
346 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
347 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800348 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
349 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800350 } else if (ACTION_DELETE_CONTACT.equals(action)) {
351 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700352 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
353 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800354 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
355 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800356 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
357 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700358 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
359 setSendToVoicemail(intent);
360 } else if (ACTION_SET_RINGTONE.equals(action)) {
361 setRingtone(intent);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700362 } else if (ACTION_UNDO.equals(action)) {
363 undo(intent);
Marcus Hagerott819214d2016-09-29 14:58:27 -0700364 } else if (ACTION_IMPORT_FROM_SIM.equals(action)) {
365 importFromSim(intent);
Marcus Hagerott7333c372016-11-07 09:40:20 -0800366 } else if (ACTION_SLEEP.equals(action)) {
367 sleepForDebugging(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700368 }
Marcus Hagerott7333c372016-11-07 09:40:20 -0800369
370 sState.onFinish(intent);
371 notifyStateChanged();
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700372 }
373
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800374 /**
375 * Creates an intent that can be sent to this service to create a new raw contact
376 * using data presented as a set of ContentValues.
377 */
378 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700379 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700380 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800381 Intent serviceIntent = new Intent(
382 context, ContactSaveService.class);
383 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
384 if (account != null) {
385 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
386 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700387 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800388 }
389 serviceIntent.putParcelableArrayListExtra(
390 ContactSaveService.EXTRA_CONTENT_VALUES, values);
391
392 // Callback intent will be invoked by the service once the new contact is
393 // created. The service will put the URI of the new contact as "data" on
394 // the callback intent.
395 Intent callbackIntent = new Intent(context, callbackActivity);
396 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800397 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
398 return serviceIntent;
399 }
400
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700401 private void createRawContact(Intent intent) {
402 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
403 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700404 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700405 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
406 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
407
408 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
409 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
410 .withValue(RawContacts.ACCOUNT_NAME, accountName)
411 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700412 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700413 .build());
414
415 int size = valueList.size();
416 for (int i = 0; i < size; i++) {
417 ContentValues values = valueList.get(i);
418 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
419 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
420 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
421 .withValues(values)
422 .build());
423 }
424
425 ContentResolver resolver = getContentResolver();
426 ContentProviderResult[] results;
427 try {
428 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
429 } catch (Exception e) {
430 throw new RuntimeException("Failed to store new contact", e);
431 }
432
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700433 Uri rawContactUri = results[0].uri;
434 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
435
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800436 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700437 }
438
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700439 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800440 * Creates an intent that can be sent to this service to create a new raw contact
441 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800442 * This variant is more convenient to use when there is only one photo that can
443 * possibly be updated, as in the Contact Details screen.
444 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
445 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800446 */
Maurice Chu851222a2012-06-21 11:43:08 -0700447 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700448 String saveModeExtraKey, int saveMode, boolean isProfile,
449 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700450 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800451 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700452 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800453 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700454 callbackActivity, callbackAction, bundle,
455 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800456 }
457
458 /**
459 * Creates an intent that can be sent to this service to create a new raw contact
460 * using data presented as a set of ContentValues.
461 * This variant is used when multiple contacts' photos may be updated, as in the
462 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700463 *
Josh Garguse692e012012-01-18 14:53:11 -0800464 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700465 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
466 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800467 */
Maurice Chu851222a2012-06-21 11:43:08 -0700468 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700469 String saveModeExtraKey, int saveMode, boolean isProfile,
470 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700471 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000472 Intent serviceIntent = new Intent(
473 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800474 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
475 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700476 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800477 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
478
Josh Garguse692e012012-01-18 14:53:11 -0800479 if (updatedPhotos != null) {
480 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
481 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800482
Josh Garguse5d3f892012-04-11 11:56:15 -0700483 if (callbackActivity != null) {
484 // Callback intent will be invoked by the service once the contact is
485 // saved. The service will put the URI of the new contact as "data" on
486 // the callback intent.
487 Intent callbackIntent = new Intent(context, callbackActivity);
488 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700489 if (joinContactIdExtraKey != null && joinContactId != null) {
490 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
491 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700492 callbackIntent.setAction(callbackAction);
493 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
494 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800495 return serviceIntent;
496 }
497
498 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700499 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700500 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800501 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800502
Jay Shrauner08099782015-03-25 14:17:11 -0700503 if (state == null) {
504 Log.e(TAG, "Invalid arguments for saveContact request");
505 return;
506 }
507
benny.lin3a4e7a22014-01-08 10:58:08 +0800508 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800509 // Trim any empty fields, and RawContacts, before persisting
510 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700511 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800512
513 Uri lookupUri = null;
514
515 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700516
Josh Garguse692e012012-01-18 14:53:11 -0800517 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800518
Josh Gargusef15c8e2012-01-30 16:42:02 -0800519 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
520 long insertedRawContactId = -1;
521
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800522 // Attempt to persist changes
523 int tries = 0;
524 while (tries++ < PERSIST_TRIES) {
525 try {
526 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800527 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
528
529 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
530
531 for (CPOWrapper cpoWrapper : diffWrapper) {
532 diff.add(cpoWrapper.getOperation());
533 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700534
Katherine Kuana007e442011-07-07 09:25:34 -0700535 if (DEBUG) {
536 Log.v(TAG, "Content Provider Operations:");
537 for (ContentProviderOperation operation : diff) {
538 Log.v(TAG, operation.toString());
539 }
540 }
541
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700542 int numberProcessed = 0;
543 boolean batchFailed = false;
544 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
545 while (numberProcessed < diff.size()) {
546 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
547 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700548 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700549 batchFailed = true;
550 break;
551 } else {
552 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700553 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800554 }
555
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700556 if (batchFailed) {
557 // Retry save
558 continue;
559 }
560
Wenyi Wang67addcc2015-11-23 10:07:48 -0800561 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800562 if (rawContactId == -1) {
563 throw new IllegalStateException("Could not determine RawContact ID after save");
564 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800565 // We don't have to check to see if the value is still -1. If we reach here,
566 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800567 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700568 if (isProfile) {
569 // Since the profile supports local raw contacts, which may have been completely
570 // removed if all information was removed, we need to do a special query to
571 // get the lookup URI for the profile contact (if it still exists).
572 Cursor c = resolver.query(Profile.CONTENT_URI,
573 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
574 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800575 if (c == null) {
576 continue;
577 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700578 try {
Erik162b7e32011-09-20 15:23:55 -0700579 if (c.moveToFirst()) {
580 final long contactId = c.getLong(0);
581 final String lookupKey = c.getString(1);
582 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
583 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700584 } finally {
585 c.close();
586 }
587 } else {
588 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
589 rawContactId);
590 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
591 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800592 if (lookupUri != null) {
593 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
594 }
Josh Garguse692e012012-01-18 14:53:11 -0800595
596 // We can change this back to false later, if we fail to save the contact photo.
597 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800598 break;
599
600 } catch (RemoteException e) {
601 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700602 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800603 break;
604
Jay Shrauner57fca182014-01-17 14:20:50 -0800605 } catch (IllegalArgumentException e) {
606 // This is thrown by applyBatch on malformed requests
Walter Jang3a0b4832016-10-12 11:02:54 -0700607 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Jay Shrauner57fca182014-01-17 14:20:50 -0800608 showToast(R.string.contactSavedErrorToast);
609 break;
610
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800611 } catch (OperationApplicationException e) {
612 // Version consistency failed, re-parent change and try again
613 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
614 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
615 boolean first = true;
616 final int count = state.size();
617 for (int i = 0; i < count; i++) {
618 Long rawContactId = state.getRawContactId(i);
619 if (rawContactId != null && rawContactId != -1) {
620 if (!first) {
621 sb.append(',');
622 }
623 sb.append(rawContactId);
624 first = false;
625 }
626 }
627 sb.append(")");
628
629 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800630 throw new IllegalStateException(
631 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800632 }
633
Maurice Chu851222a2012-06-21 11:43:08 -0700634 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700635 isProfile
636 ? RawContactsEntity.PROFILE_CONTENT_URI
637 : RawContactsEntity.CONTENT_URI,
638 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700639 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700640
641 // Update the new state to use profile URIs if appropriate.
642 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700643 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700644 delta.setProfileQueryUri();
645 }
646 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800647 }
648 }
649
Josh Garguse692e012012-01-18 14:53:11 -0800650 // Now save any updated photos. We do this at the end to ensure that
651 // the ContactProvider already knows about newly-created contacts.
652 if (updatedPhotos != null) {
653 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700654 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800655 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800656
657 // If the raw-contact ID is negative, we are saving a new raw-contact;
658 // replace the bogus ID with the new one that we actually saved the contact at.
659 if (rawContactId < 0) {
660 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800661 }
662
Jay Shrauner511561d2015-04-02 10:35:33 -0700663 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700664 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700665 succeeded = false;
666 }
Josh Garguse692e012012-01-18 14:53:11 -0800667 }
668 }
669
Josh Garguse5d3f892012-04-11 11:56:15 -0700670 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
671 if (callbackIntent != null) {
672 if (succeeded) {
673 // Mark the intent to indicate that the save was successful (even if the lookup URI
674 // is now null). For local contacts or the local profile, it's possible that the
675 // save triggered removal of the contact, so no lookup URI would exist..
676 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
677 }
678 callbackIntent.setData(lookupUri);
679 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800680 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800681 }
682
Josh Garguse692e012012-01-18 14:53:11 -0800683 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700684 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
685 * subsets, adds the returned array to "results".
686 *
687 * @return the size of the array, if not null; -1 when the array is null.
688 */
689 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
690 ContentProviderResult[] results, ContentResolver resolver)
691 throws RemoteException, OperationApplicationException {
692 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
693 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
694 subset.addAll(diff.subList(offset, offset + subsetCount));
695 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
696 .AUTHORITY, subset);
697 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
698 return -1;
699 }
700 for (ContentProviderResult c : subsetResult) {
701 results[offset++] = c;
702 }
703 return subsetResult.length;
704 }
705
706 /**
Josh Garguse692e012012-01-18 14:53:11 -0800707 * Save updated photo for the specified raw-contact.
708 * @return true for success, false for failure
709 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800710 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800711 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800712 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
713 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
714
benny.lin3a4e7a22014-01-08 10:58:08 +0800715 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800716 }
717
Josh Gargusef15c8e2012-01-30 16:42:02 -0800718 /**
719 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
720 */
Maurice Chu851222a2012-06-21 11:43:08 -0700721 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800722 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800723 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800724 long existingRawContactId = state.findRawContactId();
725 if (existingRawContactId != -1) {
726 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800727 }
728
Wenyi Wang67addcc2015-11-23 10:07:48 -0800729 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800730 }
731
732 /**
733 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
734 */
735 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800736 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800737 if (results == null) {
738 return -1;
739 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800740 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800741 final int numResults = results.length;
742 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800743 final CPOWrapper cpoWrapper = diffWrapper.get(i);
744 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
745 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
746 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800747 return ContentUris.parseId(results[i].uri);
748 }
749 }
750 return -1;
751 }
752
753 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700754 * Creates an intent that can be sent to this service to create a new group as
755 * well as add new members at the same time.
756 *
757 * @param context of the application
758 * @param account in which the group should be created
759 * @param label is the name of the group (cannot be null)
760 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
761 * should be added to the group
762 * @param callbackActivity is the activity to send the callback intent to
763 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700764 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700765 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700766 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700767 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800768 Intent serviceIntent = new Intent(context, ContactSaveService.class);
769 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
770 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
771 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700772 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800773 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700774 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700775
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800776 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700777 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800778 Intent callbackIntent = new Intent(context, callbackActivity);
779 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700780 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800781
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700782 return serviceIntent;
783 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800784
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800785 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700786 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
787 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
788 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
789 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700790 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800791
Katherine Kuan717e3432011-07-13 17:03:24 -0700792 // Create the new group
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700793 final Uri groupUri = mGroupsDao.create(label,
794 new AccountWithDataSet(accountName, accountType, dataSet));
795 final ContentResolver resolver = getContentResolver();
Katherine Kuan717e3432011-07-13 17:03:24 -0700796
797 // If there's no URI, then the insertion failed. Abort early because group members can't be
798 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800799 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700800 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800801 return;
802 }
803
Katherine Kuan717e3432011-07-13 17:03:24 -0700804 // Add new group members
805 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
806
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700807 ContentValues values = new ContentValues();
Katherine Kuan717e3432011-07-13 17:03:24 -0700808 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
Walter Jang8bac28b2016-08-30 10:34:55 -0700809 // with the way other intent extras that are passed to the
Gary Mai363af602016-09-28 10:01:23 -0700810 // {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800811 values.clear();
812 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
813 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
814
815 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700816 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700817 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800818 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800819 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800820 }
821
822 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800823 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800824 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700825 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700826 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800827 Intent serviceIntent = new Intent(context, ContactSaveService.class);
828 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
829 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
830 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700831
832 // Callback intent will be invoked by the service once the group is renamed.
833 Intent callbackIntent = new Intent(context, callbackActivity);
834 callbackIntent.setAction(callbackAction);
835 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
836
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800837 return serviceIntent;
838 }
839
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800840 private void renameGroup(Intent intent) {
841 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
842 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
843
844 if (groupId == -1) {
845 Log.e(TAG, "Invalid arguments for renameGroup request");
846 return;
847 }
848
849 ContentValues values = new ContentValues();
850 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700851 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
852 getContentResolver().update(groupUri, values, null, null);
853
854 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
855 callbackIntent.setData(groupUri);
856 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800857 }
858
859 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800860 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800861 */
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700862 public static Intent createGroupDeletionIntent(Context context, long groupId) {
863 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800864 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800865 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700866
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800867 return serviceIntent;
868 }
869
870 private void deleteGroup(Intent intent) {
871 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
872 if (groupId == -1) {
873 Log.e(TAG, "Invalid arguments for deleteGroup request");
874 return;
875 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700876 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800877
Marcus Hagerott819214d2016-09-29 14:58:27 -0700878 final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700879 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
880 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
881 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
Walter Jang72f99882016-05-26 09:01:31 -0700882
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700883 mGroupsDao.delete(groupUri);
884
885 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
886 }
887
888 public static Intent createUndoIntent(Context context, Intent resultIntent) {
889 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
890 serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
891 serviceIntent.putExtras(resultIntent);
892 return serviceIntent;
893 }
894
895 private void undo(Intent intent) {
896 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
897 if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
898 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
Walter Jang72f99882016-05-26 09:01:31 -0700899 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800900 }
901
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700902
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800903 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700904 * Creates an intent that can be sent to this service to rename a group as
905 * well as add and remove members from the group.
906 *
907 * @param context of the application
908 * @param groupId of the group that should be modified
909 * @param newLabel is the updated name of the group (can be null if the name
910 * should not be updated)
911 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
912 * should be added to the group
913 * @param rawContactsToRemove is an array of raw contact IDs for contacts
914 * that should be removed from the group
915 * @param callbackActivity is the activity to send the callback intent to
916 * @param callbackAction is the intent action for the callback intent
917 */
918 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
919 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700920 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700921 Intent serviceIntent = new Intent(context, ContactSaveService.class);
922 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
923 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
924 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
925 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
926 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
927 rawContactsToRemove);
928
929 // Callback intent will be invoked by the service once the group is updated
930 Intent callbackIntent = new Intent(context, callbackActivity);
931 callbackIntent.setAction(callbackAction);
932 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
933
934 return serviceIntent;
935 }
936
937 private void updateGroup(Intent intent) {
938 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
939 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
940 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
941 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
942
943 if (groupId == -1) {
944 Log.e(TAG, "Invalid arguments for updateGroup request");
945 return;
946 }
947
948 final ContentResolver resolver = getContentResolver();
949 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
950
951 // Update group name if necessary
952 if (label != null) {
953 ContentValues values = new ContentValues();
954 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700955 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700956 }
957
Katherine Kuan717e3432011-07-13 17:03:24 -0700958 // Add and remove members if necessary
959 addMembersToGroup(resolver, rawContactsToAdd, groupId);
960 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
961
962 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
963 callbackIntent.setData(groupUri);
964 deliverCallback(callbackIntent);
965 }
966
Walter Jang3a0b4832016-10-12 11:02:54 -0700967 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700968 long groupId) {
969 if (rawContactsToAdd == null) {
970 return;
971 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700972 for (long rawContactId : rawContactsToAdd) {
973 try {
974 final ArrayList<ContentProviderOperation> rawContactOperations =
975 new ArrayList<ContentProviderOperation>();
976
977 // Build an assert operation to ensure the contact is not already in the group
978 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
979 .newAssertQuery(Data.CONTENT_URI);
980 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
981 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
982 new String[] { String.valueOf(rawContactId),
983 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
984 assertBuilder.withExpectedCount(0);
985 rawContactOperations.add(assertBuilder.build());
986
987 // Build an insert operation to add the contact to the group
988 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
989 .newInsert(Data.CONTENT_URI);
990 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
991 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
992 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
993 rawContactOperations.add(insertBuilder.build());
994
995 if (DEBUG) {
996 for (ContentProviderOperation operation : rawContactOperations) {
997 Log.v(TAG, operation.toString());
998 }
999 }
1000
1001 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001002 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -08001003 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001004 }
1005 } catch (RemoteException e) {
1006 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -07001007 FeedbackHelper.sendFeedback(this, TAG,
1008 "Problem persisting user edits for raw contact ID " +
1009 String.valueOf(rawContactId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001010 } catch (OperationApplicationException e) {
1011 // The assert could have failed because the contact is already in the group,
1012 // just continue to the next contact
Walter Jang3a0b4832016-10-12 11:02:54 -07001013 FeedbackHelper.sendFeedback(this, TAG,
1014 "Assert failed in adding raw contact ID " +
1015 String.valueOf(rawContactId) + ". Already exists in group " +
1016 String.valueOf(groupId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001017 }
1018 }
Katherine Kuan717e3432011-07-13 17:03:24 -07001019 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001020
Daniel Lehmann18958a22012-02-28 17:45:25 -08001021 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -07001022 long groupId) {
1023 if (rawContactsToRemove == null) {
1024 return;
1025 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001026 for (long rawContactId : rawContactsToRemove) {
1027 // Apply the delete operation on the data row for the given raw contact's
1028 // membership in the given group. If no contact matches the provided selection, then
1029 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -08001030 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001031 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1032 new String[] { String.valueOf(rawContactId),
1033 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
1034 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001035 }
1036
1037 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001038 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001039 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001040 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
1041 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1042 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
1043 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1044 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
1045
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001046 return serviceIntent;
1047 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001048
1049 private void setStarred(Intent intent) {
1050 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1051 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
1052 if (contactUri == null) {
1053 Log.e(TAG, "Invalid arguments for setStarred request");
1054 return;
1055 }
1056
1057 final ContentValues values = new ContentValues(1);
1058 values.put(Contacts.STARRED, value);
1059 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -07001060
1061 // Undemote the contact if necessary
1062 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
1063 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -08001064 if (c == null) {
1065 return;
1066 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001067 try {
1068 if (c.moveToFirst()) {
1069 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -07001070
1071 // Don't bother undemoting if this contact is the user's profile.
1072 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -08001073 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -07001074 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001075 }
1076 } finally {
1077 c.close();
1078 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001079 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001080
1081 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -07001082 * Creates an intent that can be sent to this service to set the redirect to voicemail.
1083 */
1084 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
1085 boolean value) {
1086 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1087 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1088 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1089 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1090
1091 return serviceIntent;
1092 }
1093
1094 private void setSendToVoicemail(Intent intent) {
1095 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1096 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1097 if (contactUri == null) {
1098 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1099 return;
1100 }
1101
1102 final ContentValues values = new ContentValues(1);
1103 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1104 getContentResolver().update(contactUri, values, null, null);
1105 }
1106
1107 /**
1108 * Creates an intent that can be sent to this service to save the contact's ringtone.
1109 */
1110 public static Intent createSetRingtone(Context context, Uri contactUri,
1111 String value) {
1112 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1113 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1114 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1115 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1116
1117 return serviceIntent;
1118 }
1119
1120 private void setRingtone(Intent intent) {
1121 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1122 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1123 if (contactUri == null) {
1124 Log.e(TAG, "Invalid arguments for setRingtone");
1125 return;
1126 }
1127 ContentValues values = new ContentValues(1);
1128 values.put(Contacts.CUSTOM_RINGTONE, value);
1129 getContentResolver().update(contactUri, values, null, null);
1130 }
1131
1132 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001133 * Creates an intent that sets the selected data item as super primary (default)
1134 */
1135 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1136 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1137 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1138 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1139 return serviceIntent;
1140 }
1141
1142 private void setSuperPrimary(Intent intent) {
1143 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1144 if (dataId == -1) {
1145 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1146 return;
1147 }
1148
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001149 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001150 }
1151
1152 /**
1153 * Creates an intent that clears the primary flag of all data items that belong to the same
1154 * raw_contact as the given data item. Will only clear, if the data item was primary before
1155 * this call
1156 */
1157 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1158 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1159 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1160 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1161 return serviceIntent;
1162 }
1163
1164 private void clearPrimary(Intent intent) {
1165 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1166 if (dataId == -1) {
1167 Log.e(TAG, "Invalid arguments for clearPrimary request");
1168 return;
1169 }
1170
1171 // Update the primary values in the data record.
1172 ContentValues values = new ContentValues(1);
1173 values.put(Data.IS_SUPER_PRIMARY, 0);
1174 values.put(Data.IS_PRIMARY, 0);
1175
1176 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1177 values, null, null);
1178 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001179
1180 /**
1181 * Creates an intent that can be sent to this service to delete a contact.
1182 */
1183 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1184 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1185 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1186 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1187 return serviceIntent;
1188 }
1189
Brian Attwelld2962a32015-03-02 14:48:50 -08001190 /**
1191 * Creates an intent that can be sent to this service to delete multiple contacts.
1192 */
1193 public static Intent createDeleteMultipleContactsIntent(Context context,
James Laskeye5a140a2016-10-18 15:43:42 -07001194 long[] contactIds, final String[] names) {
Brian Attwelld2962a32015-03-02 14:48:50 -08001195 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1196 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1197 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
James Laskeye5a140a2016-10-18 15:43:42 -07001198 serviceIntent.putExtra(ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY, names);
Brian Attwelld2962a32015-03-02 14:48:50 -08001199 return serviceIntent;
1200 }
1201
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001202 private void deleteContact(Intent intent) {
1203 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1204 if (contactUri == null) {
1205 Log.e(TAG, "Invalid arguments for deleteContact request");
1206 return;
1207 }
1208
1209 getContentResolver().delete(contactUri, null, null);
1210 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001211
Brian Attwelld2962a32015-03-02 14:48:50 -08001212 private void deleteMultipleContacts(Intent intent) {
1213 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1214 if (contactIds == null) {
1215 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1216 return;
1217 }
1218 for (long contactId : contactIds) {
1219 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1220 getContentResolver().delete(contactUri, null, null);
1221 }
James Laskeye5a140a2016-10-18 15:43:42 -07001222 final String[] names = intent.getStringArrayExtra(
1223 ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY);
1224 final String deleteToastMessage;
1225 if (names.length == 0) {
1226 deleteToastMessage = getResources().getQuantityString(
1227 R.plurals.contacts_deleted_toast, contactIds.length);
1228 } else if (names.length == 1) {
1229 deleteToastMessage = getResources().getString(
1230 R.string.contacts_deleted_one_named_toast, names);
1231 } else if (names.length == 2) {
1232 deleteToastMessage = getResources().getString(
1233 R.string.contacts_deleted_two_named_toast, names);
1234 } else {
1235 deleteToastMessage = getResources().getString(
1236 R.string.contacts_deleted_many_named_toast, names);
1237 }
Wenyi Wang687d2182015-10-28 17:03:18 -07001238 mMainHandler.post(new Runnable() {
1239 @Override
1240 public void run() {
1241 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1242 .show();
1243 }
1244 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001245 }
1246
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001247 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001248 * Creates an intent that can be sent to this service to split a contact into it's constituent
Gary Maib9065dd2016-11-08 10:49:00 -08001249 * pieces. This will set the raw contact ids to {@link AggregationExceptions#TYPE_AUTOMATIC} so
Gary Mai53fe0d22016-07-26 17:23:53 -07001250 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001251 */
1252 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1253 ResultReceiver receiver) {
1254 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1255 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1256 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1257 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1258 return serviceIntent;
1259 }
1260
Gary Maib9065dd2016-11-08 10:49:00 -08001261 /**
1262 * Creates an intent that can be sent to this service to split a contact into it's constituent
1263 * pieces. This will explicitly set the raw contact ids to
1264 * {@link AggregationExceptions#TYPE_KEEP_SEPARATE}.
1265 */
1266 public static Intent createHardSplitContactIntent(Context context, long[][] rawContactIds) {
1267 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1268 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1269 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1270 serviceIntent.putExtra(ContactSaveService.EXTRA_HARD_SPLIT, true);
1271 return serviceIntent;
1272 }
1273
Gary Mai7efa9942016-05-12 11:26:49 -07001274 private void splitContact(Intent intent) {
1275 final long rawContactIds[][] = (long[][]) intent
1276 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001277 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Maib9065dd2016-11-08 10:49:00 -08001278 final boolean hardSplit = intent.getBooleanExtra(EXTRA_HARD_SPLIT, false);
Gary Mai7efa9942016-05-12 11:26:49 -07001279 if (rawContactIds == null) {
1280 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001281 if (receiver != null) {
1282 receiver.send(BAD_ARGUMENTS, new Bundle());
1283 }
Gary Mai7efa9942016-05-12 11:26:49 -07001284 return;
1285 }
1286 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1287 final ContentResolver resolver = getContentResolver();
1288 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001289 for (int i = 0; i < rawContactIds.length; i++) {
1290 for (int j = 0; j < rawContactIds.length; j++) {
1291 if (i != j) {
Gary Maib9065dd2016-11-08 10:49:00 -08001292 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j],
1293 hardSplit)) {
Gary Mai7efa9942016-05-12 11:26:49 -07001294 if (receiver != null) {
1295 receiver.send(CP2_ERROR, new Bundle());
1296 return;
1297 }
1298 }
1299 }
1300 }
1301 }
1302 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1303 if (receiver != null) {
1304 receiver.send(CP2_ERROR, new Bundle());
1305 }
1306 return;
1307 }
Gary Maib9065dd2016-11-08 10:49:00 -08001308 LocalBroadcastManager.getInstance(this)
1309 .sendBroadcast(new Intent(BROADCAST_UNLINK_COMPLETE));
Gary Mai7efa9942016-05-12 11:26:49 -07001310 if (receiver != null) {
1311 receiver.send(CONTACTS_SPLIT, new Bundle());
1312 } else {
1313 showToast(R.string.contactUnlinkedToast);
1314 }
1315 }
1316
1317 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001318 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001319 * and {@param rawContactIds2} to {@param operations}.
1320 * @return false if an error occurred, true otherwise.
1321 */
1322 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
Gary Maib9065dd2016-11-08 10:49:00 -08001323 long[] rawContactIds1, long[] rawContactIds2, boolean hardSplit) {
Gary Mai7efa9942016-05-12 11:26:49 -07001324 if (rawContactIds1 == null || rawContactIds2 == null) {
1325 Log.e(TAG, "Invalid arguments for splitContact request");
1326 return false;
1327 }
1328 // For each pair of raw contacts, insert an aggregation exception
1329 final ContentResolver resolver = getContentResolver();
1330 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1331 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1332 for (int i = 0; i < rawContactIds1.length; i++) {
1333 for (int j = 0; j < rawContactIds2.length; j++) {
Gary Maib9065dd2016-11-08 10:49:00 -08001334 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j], hardSplit);
Gary Mai7efa9942016-05-12 11:26:49 -07001335 // Before we get to 500 we need to flush the operations list
1336 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1337 if (!applyOperations(resolver, operations)) {
1338 return false;
1339 }
1340 operations.clear();
1341 }
1342 }
1343 }
1344 return true;
1345 }
1346
1347 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001348 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001349 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001350 */
1351 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001352 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001353 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1354 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1355 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1356 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001357
1358 // Callback intent will be invoked by the service once the contacts are joined.
1359 Intent callbackIntent = new Intent(context, callbackActivity);
1360 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001361 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1362
1363 return serviceIntent;
1364 }
1365
Brian Attwelld3946ca2015-03-03 11:13:49 -08001366 /**
1367 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1368 * No special attention is paid to where the resulting contact's name is taken from.
1369 */
Gary Mai7efa9942016-05-12 11:26:49 -07001370 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1371 ResultReceiver receiver) {
1372 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001373 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1374 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001375 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001376 return serviceIntent;
1377 }
1378
Gary Mai7efa9942016-05-12 11:26:49 -07001379 /**
1380 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1381 * No special attention is paid to where the resulting contact's name is taken from.
1382 */
1383 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1384 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1385 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001386
1387 private interface JoinContactQuery {
1388 String[] PROJECTION = {
1389 RawContacts._ID,
1390 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001391 RawContacts.DISPLAY_NAME_SOURCE,
1392 };
1393
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001394 int _ID = 0;
1395 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001396 int DISPLAY_NAME_SOURCE = 2;
1397 }
1398
1399 private interface ContactEntityQuery {
1400 String[] PROJECTION = {
1401 Contacts.Entity.DATA_ID,
1402 Contacts.Entity.CONTACT_ID,
1403 Contacts.Entity.IS_SUPER_PRIMARY,
1404 };
1405 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1406 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1407 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1408 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1409
1410 int DATA_ID = 0;
1411 int CONTACT_ID = 1;
1412 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001413 }
1414
Brian Attwelld3946ca2015-03-03 11:13:49 -08001415 private void joinSeveralContacts(Intent intent) {
1416 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001417
Gary Mai7efa9942016-05-12 11:26:49 -07001418 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001419
Brian Attwelld3946ca2015-03-03 11:13:49 -08001420 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001421 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1422 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001423 if (rawContactIds == null) {
1424 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001425 if (receiver != null) {
1426 receiver.send(BAD_ARGUMENTS, new Bundle());
1427 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001428 return;
1429 }
1430
Brian Attwelld3946ca2015-03-03 11:13:49 -08001431 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001432 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001433 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1434 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1435 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001436 for (int i = 0; i < rawContactIds.length; i++) {
1437 for (int j = 0; j < rawContactIds.length; j++) {
1438 if (i != j) {
1439 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1440 }
Walter Jang0653de32015-07-24 12:12:40 -07001441 // Before we get to 500 we need to flush the operations list
1442 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001443 if (!applyOperations(resolver, operations)) {
1444 if (receiver != null) {
1445 receiver.send(CP2_ERROR, new Bundle());
1446 }
Walter Jang0653de32015-07-24 12:12:40 -07001447 return;
1448 }
1449 operations.clear();
1450 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001451 }
1452 }
Gary Mai7efa9942016-05-12 11:26:49 -07001453 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1454 if (receiver != null) {
1455 receiver.send(CP2_ERROR, new Bundle());
1456 }
Walter Jang0653de32015-07-24 12:12:40 -07001457 return;
1458 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001459
John Shaoa3c507a2016-09-13 14:26:17 -07001460
1461 final String name = queryNameOfLinkedContacts(contactIds);
1462 if (name != null) {
1463 if (receiver != null) {
1464 final Bundle result = new Bundle();
1465 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1466 result.putString(EXTRA_DISPLAY_NAME, name);
1467 receiver.send(CONTACTS_LINKED, result);
1468 } else {
James Laskeyf62b4882016-10-21 11:36:40 -07001469 if (TextUtils.isEmpty(name)) {
1470 showToast(R.string.contactsJoinedMessage);
1471 } else {
1472 showToast(R.string.contactsJoinedNamedMessage, name);
1473 }
John Shaoa3c507a2016-09-13 14:26:17 -07001474 }
Gary Maib9065dd2016-11-08 10:49:00 -08001475 LocalBroadcastManager.getInstance(this)
1476 .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
Gary Mai7efa9942016-05-12 11:26:49 -07001477 } else {
John Shaoa3c507a2016-09-13 14:26:17 -07001478 if (receiver != null) {
1479 receiver.send(CP2_ERROR, new Bundle());
1480 }
1481 showToast(R.string.contactJoinErrorToast);
Gary Mai7efa9942016-05-12 11:26:49 -07001482 }
Walter Jang0653de32015-07-24 12:12:40 -07001483 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001484
John Shaoa3c507a2016-09-13 14:26:17 -07001485 /** Get the display name of the top-level contact after the contacts have been linked. */
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001486 private String queryNameOfLinkedContacts(long[] contactIds) {
1487 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1488 final String[] whereArgs = new String[contactIds.length];
1489 for (int i = 0; i < contactIds.length; i++) {
1490 whereArgs[i] = String.valueOf(contactIds[i]);
1491 whereBuilder.append("?,");
1492 }
1493 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1494 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
James Laskeyf62b4882016-10-21 11:36:40 -07001495 new String[]{Contacts._ID, Contacts.DISPLAY_NAME,
1496 Contacts.DISPLAY_NAME_ALTERNATIVE},
John Shaoa3c507a2016-09-13 14:26:17 -07001497 whereBuilder.toString(), whereArgs, null);
1498
1499 String name = null;
James Laskeyf62b4882016-10-21 11:36:40 -07001500 String nameAlt = null;
John Shaoa3c507a2016-09-13 14:26:17 -07001501 long contactId = 0;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001502 try {
1503 if (cursor.moveToFirst()) {
John Shaoa3c507a2016-09-13 14:26:17 -07001504 contactId = cursor.getLong(0);
1505 name = cursor.getString(1);
James Laskeyf62b4882016-10-21 11:36:40 -07001506 nameAlt = cursor.getString(2);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001507 }
John Shaoa3c507a2016-09-13 14:26:17 -07001508 while(cursor.moveToNext()) {
1509 if (cursor.getLong(0) != contactId) {
1510 return null;
1511 }
1512 }
James Laskeyf62b4882016-10-21 11:36:40 -07001513
1514 final String formattedName = ContactDisplayUtils.getPreferredDisplayName(name, nameAlt,
1515 new ContactsPreferences(getApplicationContext()));
1516 return formattedName == null ? "" : formattedName;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001517 } finally {
John Shaoa3c507a2016-09-13 14:26:17 -07001518 if (cursor != null) {
1519 cursor.close();
1520 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001521 }
1522 }
1523
Walter Jang0653de32015-07-24 12:12:40 -07001524 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001525 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001526 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001527 try {
John Shaoa3c507a2016-09-13 14:26:17 -07001528 final ContentProviderResult[] result =
1529 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1530 for (int i = 0; i < result.length; ++i) {
1531 // if no rows were modified in the operation then we count it as fail.
1532 if (result[i].count < 0) {
1533 throw new OperationApplicationException();
1534 }
1535 }
Walter Jang0653de32015-07-24 12:12:40 -07001536 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001537 } catch (RemoteException | OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001538 FeedbackHelper.sendFeedback(this, TAG,
1539 "Failed to apply aggregation exception batch", e);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001540 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001541 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001542 }
1543 }
1544
Brian Attwelld3946ca2015-03-03 11:13:49 -08001545 private void joinContacts(Intent intent) {
1546 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1547 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001548
1549 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001550 // in the join UIs.
1551 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1552 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001553 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001554 return;
1555 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001556
Brian Attwell548f5c62015-01-27 17:46:46 -08001557 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001558
1559 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001560 for (int i = 0; i < rawContactIds.length; i++) {
1561 for (int j = 0; j < rawContactIds.length; j++) {
1562 if (i != j) {
1563 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1564 }
1565 }
1566 }
1567
Brian Attwelld3946ca2015-03-03 11:13:49 -08001568 final ContentResolver resolver = getContentResolver();
1569
Brian Attwell548f5c62015-01-27 17:46:46 -08001570 // Use the name for contactId1 as the name for the newly aggregated contact.
1571 final Uri contactId1Uri = ContentUris.withAppendedId(
1572 Contacts.CONTENT_URI, contactId1);
1573 final Uri entityUri = Uri.withAppendedPath(
1574 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1575 Cursor c = resolver.query(entityUri,
1576 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1577 if (c == null) {
1578 Log.e(TAG, "Unable to open Contacts DB cursor");
1579 showToast(R.string.contactSavedErrorToast);
1580 return;
1581 }
1582 long dataIdToAddSuperPrimary = -1;
1583 try {
1584 if (c.moveToFirst()) {
1585 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1586 }
1587 } finally {
1588 c.close();
1589 }
1590
1591 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1592 // display name does not change as a result of the join.
1593 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001594 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001595 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1596 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1597 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001598 operations.add(builder.build());
1599 }
1600
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001601 // Apply all aggregation exceptions as one batch
John Shaoa3c507a2016-09-13 14:26:17 -07001602 final boolean success = applyOperations(resolver, operations);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001603
John Shaoa3c507a2016-09-13 14:26:17 -07001604 final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2});
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001605 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
John Shaoa3c507a2016-09-13 14:26:17 -07001606 if (success && name != null) {
James Laskeyf62b4882016-10-21 11:36:40 -07001607 if (TextUtils.isEmpty(name)) {
1608 showToast(R.string.contactsJoinedMessage);
1609 } else {
1610 showToast(R.string.contactsJoinedNamedMessage, name);
1611 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001612 Uri uri = RawContacts.getContactLookupUri(resolver,
1613 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1614 callbackIntent.setData(uri);
Gary Maib9065dd2016-11-08 10:49:00 -08001615 LocalBroadcastManager.getInstance(this)
1616 .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001617 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001618 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001619 }
1620
Gary Mai7efa9942016-05-12 11:26:49 -07001621 /**
1622 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1623 * array of the return value holds an array of raw contact ids for one contactId.
1624 * @param contactIds
1625 * @return
1626 */
1627 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1628 final long[][] rawContactIds = new long[contactIds.length][];
1629 for (int i = 0; i < contactIds.length; i++) {
1630 rawContactIds[i] = getRawContactIds(contactIds[i]);
1631 }
1632 return rawContactIds;
1633 }
1634
1635 /**
1636 * Gets the raw contact ids associated with {@param contactId}.
1637 * @param contactId
1638 * @return Array of raw contact ids.
1639 */
1640 private long[] getRawContactIds(long contactId) {
1641 final ContentResolver resolver = getContentResolver();
1642 long rawContactIds[];
1643
1644 final StringBuilder queryBuilder = new StringBuilder();
1645 queryBuilder.append(RawContacts.CONTACT_ID)
1646 .append("=")
1647 .append(String.valueOf(contactId));
1648
1649 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1650 JoinContactQuery.PROJECTION,
1651 queryBuilder.toString(),
1652 null, null);
1653 if (c == null) {
1654 Log.e(TAG, "Unable to open Contacts DB cursor");
1655 return null;
1656 }
1657 try {
1658 rawContactIds = new long[c.getCount()];
1659 for (int i = 0; i < rawContactIds.length; i++) {
1660 c.moveToPosition(i);
1661 final long rawContactId = c.getLong(JoinContactQuery._ID);
1662 rawContactIds[i] = rawContactId;
1663 }
1664 } finally {
1665 c.close();
1666 }
1667 return rawContactIds;
1668 }
1669
Brian Attwelld3946ca2015-03-03 11:13:49 -08001670 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1671 if (contactIds == null) {
1672 return null;
1673 }
1674
Brian Attwell548f5c62015-01-27 17:46:46 -08001675 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001676
1677 final StringBuilder queryBuilder = new StringBuilder();
1678 final String stringContactIds[] = new String[contactIds.length];
1679 for (int i = 0; i < contactIds.length; i++) {
1680 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1681 stringContactIds[i] = String.valueOf(contactIds[i]);
1682 if (contactIds[i] == -1) {
1683 return null;
1684 }
1685 if (i == contactIds.length -1) {
1686 break;
1687 }
1688 queryBuilder.append(" OR ");
1689 }
1690
Brian Attwell548f5c62015-01-27 17:46:46 -08001691 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1692 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001693 queryBuilder.toString(),
1694 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001695 if (c == null) {
1696 Log.e(TAG, "Unable to open Contacts DB cursor");
1697 showToast(R.string.contactSavedErrorToast);
1698 return null;
1699 }
Gary Mai7efa9942016-05-12 11:26:49 -07001700 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001701 try {
1702 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001703 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001704 return null;
1705 }
1706 rawContactIds = new long[c.getCount()];
1707 for (int i = 0; i < rawContactIds.length; i++) {
1708 c.moveToPosition(i);
1709 long rawContactId = c.getLong(JoinContactQuery._ID);
1710 rawContactIds[i] = rawContactId;
1711 }
1712 } finally {
1713 c.close();
1714 }
1715 return rawContactIds;
1716 }
1717
Brian Attwelld3946ca2015-03-03 11:13:49 -08001718 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1719 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1720 }
1721
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001722 /**
1723 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1724 */
1725 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1726 long rawContactId1, long rawContactId2) {
1727 Builder builder =
1728 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1729 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1730 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1731 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1732 operations.add(builder.build());
1733 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001734
1735 /**
Gary Maib9065dd2016-11-08 10:49:00 -08001736 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} or a
1737 * {@link AggregationExceptions#TYPE_KEEP_SEPARATE} ContentProviderOperation if a hard split is
1738 * requested.
Gary Mai7efa9942016-05-12 11:26:49 -07001739 */
1740 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
Gary Maib9065dd2016-11-08 10:49:00 -08001741 long rawContactId1, long rawContactId2, boolean hardSplit) {
Gary Mai7efa9942016-05-12 11:26:49 -07001742 final Builder builder =
1743 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Maib9065dd2016-11-08 10:49:00 -08001744 builder.withValue(AggregationExceptions.TYPE,
1745 hardSplit
1746 ? AggregationExceptions.TYPE_KEEP_SEPARATE
1747 : AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001748 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1749 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1750 operations.add(builder.build());
1751 }
1752
Marcus Hagerott7333c372016-11-07 09:40:20 -08001753 /**
1754 * Returns an intent that can be used to import the contacts into targetAccount.
1755 *
1756 * @param context context to use for creating the intent
1757 * @param subscriptionId the subscriptionId of the SIM card that is being imported. See
1758 * {@link SubscriptionInfo#getSubscriptionId()}. Upon completion the
1759 * SIM for that subscription ID will be marked as imported
1760 * @param contacts the contacts to import
1761 * @param targetAccount the account import the contacts into
1762 */
1763 public static Intent createImportFromSimIntent(Context context, int subscriptionId,
1764 ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
Marcus Hagerott819214d2016-09-29 14:58:27 -07001765 return new Intent(context, ContactSaveService.class)
1766 .setAction(ACTION_IMPORT_FROM_SIM)
1767 .putExtra(EXTRA_SIM_CONTACTS, contacts)
Marcus Hagerott7333c372016-11-07 09:40:20 -08001768 .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId)
1769 .putExtra(EXTRA_ACCOUNT, targetAccount);
Marcus Hagerott819214d2016-09-29 14:58:27 -07001770 }
1771
1772 private void importFromSim(Intent intent) {
1773 final Intent result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
1774 .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, System.currentTimeMillis());
Marcus Hagerott7333c372016-11-07 09:40:20 -08001775 final int subscriptionId = intent.getIntExtra(EXTRA_SIM_SUBSCRIPTION_ID,
1776 SimCard.NO_SUBSCRIPTION_ID);
Marcus Hagerott819214d2016-09-29 14:58:27 -07001777 try {
1778 final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
1779 final ArrayList<SimContact> contacts =
1780 intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
1781 mSimContactDao.importContacts(contacts, targetAccount);
Marcus Hagerott7333c372016-11-07 09:40:20 -08001782
1783 // Update the imported state of the SIM card that was imported
1784 final SimCard sim = mSimContactDao.getSimBySubscriptionId(subscriptionId);
1785 if (sim != null) {
1786 mSimContactDao.persistSimState(sim.withImportedState(true));
1787 }
1788
Marcus Hagerott819214d2016-09-29 14:58:27 -07001789 // notify success
1790 LocalBroadcastManager.getInstance(this).sendBroadcast(result
1791 .putExtra(EXTRA_RESULT_COUNT, contacts.size())
Marcus Hagerott66e8b222016-10-23 15:41:55 -07001792 .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS)
Marcus Hagerott7333c372016-11-07 09:40:20 -08001793 .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId));
Marcus Hagerott819214d2016-09-29 14:58:27 -07001794 if (Log.isLoggable(TAG, Log.DEBUG)) {
1795 Log.d(TAG, "importFromSim completed successfully");
1796 }
1797 } catch (RemoteException|OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001798 FeedbackHelper.sendFeedback(this, TAG, "Failed to import contacts from SIM card", e);
Marcus Hagerott819214d2016-09-29 14:58:27 -07001799 LocalBroadcastManager.getInstance(this).sendBroadcast(result
Marcus Hagerott7333c372016-11-07 09:40:20 -08001800 .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE)
1801 .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId));
1802 }
1803 }
1804
1805 /**
1806 * Returns an intent that can start this service and cause it to sleep for the specified time.
1807 *
1808 * This exists purely for debugging and manual testing. Since this service uses a single thread
1809 * it is useful to have a way to test behavior when work is queued up and most of the other
1810 * operations complete too quickly to simulate that under normal conditions.
1811 */
1812 public static Intent createSleepIntent(Context context, long millis) {
1813 return new Intent(context, ContactSaveService.class).setAction(ACTION_SLEEP)
1814 .putExtra(EXTRA_SLEEP_DURATION, millis);
1815 }
1816
1817 private void sleepForDebugging(Intent intent) {
1818 long duration = intent.getLongExtra(EXTRA_SLEEP_DURATION, 1000);
1819 if (Log.isLoggable(TAG, Log.DEBUG)) {
1820 Log.d(TAG, "sleeping for " + duration + "ms");
1821 }
1822 try {
1823 Thread.sleep(duration);
1824 } catch (InterruptedException e) {
1825 e.printStackTrace();
1826 }
1827 if (Log.isLoggable(TAG, Log.DEBUG)) {
1828 Log.d(TAG, "finished sleeping");
Marcus Hagerott819214d2016-09-29 14:58:27 -07001829 }
1830 }
1831
Gary Mai7efa9942016-05-12 11:26:49 -07001832 /**
James Laskeyf62b4882016-10-21 11:36:40 -07001833 * Shows a toast on the UI thread by formatting messageId using args.
1834 * @param messageId id of message string
1835 * @param args args to format string
1836 */
1837 private void showToast(final int messageId, final Object... args) {
1838 final String message = getResources().getString(messageId, args);
1839 mMainHandler.post(new Runnable() {
1840 @Override
1841 public void run() {
1842 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1843 }
1844 });
1845 }
1846
1847
1848 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001849 * Shows a toast on the UI thread.
1850 */
1851 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001852 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001853
1854 @Override
1855 public void run() {
1856 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1857 }
1858 });
1859 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001860
1861 private void deliverCallback(final Intent callbackIntent) {
1862 mMainHandler.post(new Runnable() {
1863
1864 @Override
1865 public void run() {
1866 deliverCallbackOnUiThread(callbackIntent);
1867 }
1868 });
1869 }
1870
1871 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1872 // TODO: this assumes that if there are multiple instances of the same
1873 // activity registered, the last one registered is the one waiting for
1874 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001875 for (Listener listener : sListeners) {
1876 if (callbackIntent.getComponent().equals(
1877 ((Activity) listener).getIntent().getComponent())) {
1878 listener.onServiceCompleted(callbackIntent);
1879 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001880 }
1881 }
1882 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001883
1884 public interface GroupsDao {
1885 Uri create(String title, AccountWithDataSet account);
1886 int delete(Uri groupUri);
1887 Bundle captureDeletionUndoData(Uri groupUri);
1888 Uri undoDeletion(Bundle undoData);
1889 }
1890
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001891 public static class GroupsDaoImpl implements GroupsDao {
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001892 public static final String KEY_GROUP_DATA = "groupData";
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001893 public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
1894
1895 private static final String TAG = "GroupsDao";
1896 private final Context context;
1897 private final ContentResolver contentResolver;
1898
1899 public GroupsDaoImpl(Context context) {
1900 this(context, context.getContentResolver());
1901 }
1902
1903 public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
1904 this.context = context;
1905 this.contentResolver = contentResolver;
1906 }
1907
1908 public Bundle captureDeletionUndoData(Uri groupUri) {
1909 final long groupId = ContentUris.parseId(groupUri);
1910 final Bundle result = new Bundle();
1911
1912 final Cursor cursor = contentResolver.query(groupUri,
1913 new String[]{
1914 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
1915 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
1916 Groups.SHOULD_SYNC
1917 },
1918 Groups.DELETED + "=?", new String[] { "0" }, null);
1919 try {
1920 if (cursor.moveToFirst()) {
1921 final ContentValues groupValues = new ContentValues();
1922 DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
1923 result.putParcelable(KEY_GROUP_DATA, groupValues);
1924 } else {
1925 // Group doesn't exist.
1926 return result;
1927 }
1928 } finally {
1929 cursor.close();
1930 }
1931
1932 final Cursor membersCursor = contentResolver.query(
1933 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
1934 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1935 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
1936 final long[] memberIds = new long[membersCursor.getCount()];
1937 int i = 0;
1938 while (membersCursor.moveToNext()) {
1939 memberIds[i++] = membersCursor.getLong(0);
1940 }
1941 result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
1942 return result;
1943 }
1944
1945 public Uri undoDeletion(Bundle deletedGroupData) {
1946 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
1947 if (groupData == null) {
1948 return null;
1949 }
1950 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
1951 final long groupId = ContentUris.parseId(groupUri);
1952
1953 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
1954 if (memberIds == null) {
1955 return groupUri;
1956 }
1957 final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
1958 for (int i = 0; i < memberIds.length; i++) {
1959 memberInsertions[i] = new ContentValues();
1960 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
1961 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1962 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
1963 }
1964 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
1965 if (inserted != memberIds.length) {
1966 Log.e(TAG, "Could not recover some members for group deletion undo");
1967 }
1968
1969 return groupUri;
1970 }
1971
1972 public Uri create(String title, AccountWithDataSet account) {
1973 final ContentValues values = new ContentValues();
1974 values.put(Groups.TITLE, title);
1975 values.put(Groups.ACCOUNT_NAME, account.name);
1976 values.put(Groups.ACCOUNT_TYPE, account.type);
1977 values.put(Groups.DATA_SET, account.dataSet);
1978 return contentResolver.insert(Groups.CONTENT_URI, values);
1979 }
1980
1981 public int delete(Uri groupUri) {
1982 return contentResolver.delete(groupUri, null, null);
1983 }
1984 }
Marcus Hagerott7333c372016-11-07 09:40:20 -08001985
1986 /**
1987 * Keeps track of which operations have been requested but have not yet finished for this
1988 * service.
1989 */
1990 public static class State {
1991 private final CopyOnWriteArrayList<Intent> mPending;
1992
1993 public State() {
1994 mPending = new CopyOnWriteArrayList<>();
1995 }
1996
1997 public State(Collection<Intent> pendingActions) {
1998 mPending = new CopyOnWriteArrayList<>(pendingActions);
1999 }
2000
2001 public boolean isIdle() {
2002 return mPending.isEmpty();
2003 }
2004
2005 public Intent getCurrentIntent() {
2006 return mPending.isEmpty() ? null : mPending.get(0);
2007 }
2008
2009 /**
2010 * Returns the first intent requested that has the specified action or null if no intent
2011 * with that action has been requested.
2012 */
2013 public Intent getNextIntentWithAction(String action) {
2014 for (Intent intent : mPending) {
2015 if (action.equals(intent.getAction())) {
2016 return intent;
2017 }
2018 }
2019 return null;
2020 }
2021
2022 public boolean isActionPending(String action) {
2023 return getNextIntentWithAction(action) != null;
2024 }
2025
2026 private void onFinish(Intent intent) {
2027 if (mPending.isEmpty()) {
2028 return;
2029 }
2030 final String action = mPending.get(0).getAction();
2031 if (action.equals(intent.getAction())) {
2032 mPending.remove(0);
2033 }
2034 }
2035
2036 private void onStart(Intent intent) {
2037 if (intent.getAction() == null) {
2038 return;
2039 }
2040 mPending.add(intent);
2041 }
2042 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07002043}