blob: 2aba4c009b86a0f81fb160202571c6639d9fa7b5 [file] [log] [blame]
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080017package com.android.contacts;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070018
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080019import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070020import android.app.IntentService;
21import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080022import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070023import android.content.ContentProviderResult;
24import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080025import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070026import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080027import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070028import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080029import android.content.OperationApplicationException;
30import android.database.Cursor;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070031import android.database.DatabaseUtils;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070032import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080033import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080034import android.os.Handler;
35import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080036import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080037import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070038import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080039import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080040import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080041import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080042import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070043import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080044import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070045import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070046import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070047import android.provider.ContactsContract.RawContactsEntity;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070048import android.support.v4.content.LocalBroadcastManager;
Gary Mai7efa9942016-05-12 11:26:49 -070049import android.support.v4.os.ResultReceiver;
James Laskeyf62b4882016-10-21 11:36:40 -070050import android.text.TextUtils;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070051import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080052import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070053
Gary Mai363af602016-09-28 10:01:23 -070054import com.android.contacts.activities.ContactEditorActivity;
Gary Mai69c182a2016-12-05 13:07:03 -080055import com.android.contacts.compat.CompatUtils;
56import com.android.contacts.database.ContactUpdateUtils;
57import com.android.contacts.database.SimContactDao;
58import com.android.contacts.model.AccountTypeManager;
59import com.android.contacts.model.CPOWrapper;
60import com.android.contacts.model.RawContactDelta;
61import com.android.contacts.model.RawContactDeltaList;
62import com.android.contacts.model.RawContactModifier;
63import com.android.contacts.model.account.AccountWithDataSet;
64import com.android.contacts.preference.ContactsPreferences;
65import com.android.contacts.util.ContactDisplayUtils;
66import com.android.contacts.util.PermissionsUtil;
Wenyi Wangaac0e662015-12-18 17:17:33 -080067import com.android.contacts.compat.PinnedPositionsCompat;
Yorke Lee637a38e2013-09-14 08:36:33 -070068import com.android.contacts.util.ContactPhotoUtils;
Walter Jang3a0b4832016-10-12 11:02:54 -070069import com.android.contactsbind.FeedbackHelper;
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070070import com.google.common.collect.Lists;
71import com.google.common.collect.Sets;
72
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080073import java.util.ArrayList;
Marcus Hagerott7333c372016-11-07 09:40:20 -080074import java.util.Collection;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080075import java.util.HashSet;
76import java.util.List;
77import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070078
Marcus Hagerott95246bb2016-11-11 10:56:09 -080079import static android.Manifest.permission.WRITE_CONTACTS;
80
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080081/**
82 * A service responsible for saving changes to the content provider.
83 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070084public class ContactSaveService extends IntentService {
85 private static final String TAG = "ContactSaveService";
86
Katherine Kuana007e442011-07-07 09:25:34 -070087 /** Set to true in order to view logs on content provider operations */
88 private static final boolean DEBUG = false;
89
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070090 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
91
92 public static final String EXTRA_ACCOUNT_NAME = "accountName";
93 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070094 public static final String EXTRA_DATA_SET = "dataSet";
Marcus Hagerott819214d2016-09-29 14:58:27 -070095 public static final String EXTRA_ACCOUNT = "account";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070096 public static final String EXTRA_CONTENT_VALUES = "contentValues";
97 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
Gary Mai7efa9942016-05-12 11:26:49 -070098 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
99 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700100
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800101 public static final String ACTION_SAVE_CONTACT = "saveContact";
102 public static final String EXTRA_CONTACT_STATE = "state";
103 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700104 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -0700105 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -0800106 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700107
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800108 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800109 public static final String ACTION_RENAME_GROUP = "renameGroup";
110 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700111 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800112 public static final String EXTRA_GROUP_ID = "groupId";
113 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700114 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
115 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800116
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800117 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800118 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800119 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800120 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800121 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800122 public static final String EXTRA_STARRED_FLAG = "starred";
Marcus Hagerott3bb85142016-07-29 10:46:36 -0700123 public static final String EXTRA_DISPLAY_NAME = "extraDisplayName";
James Laskeye5a140a2016-10-18 15:43:42 -0700124 public static final String EXTRA_DISPLAY_NAME_ARRAY = "extraDisplayNameArray";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800125
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800126 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
127 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
128 public static final String EXTRA_DATA_ID = "dataId";
129
Gary Mai7efa9942016-05-12 11:26:49 -0700130 public static final String ACTION_SPLIT_CONTACT = "splitContact";
Gary Maib9065dd2016-11-08 10:49:00 -0800131 public static final String EXTRA_HARD_SPLIT = "extraHardSplit";
Gary Mai7efa9942016-05-12 11:26:49 -0700132
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800133 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800134 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800135 public static final String EXTRA_CONTACT_ID1 = "contactId1";
136 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800137
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700138 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
139 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
140
141 public static final String ACTION_SET_RINGTONE = "setRingtone";
142 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
143
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700144 public static final String ACTION_UNDO = "undo";
145 public static final String EXTRA_UNDO_ACTION = "undoAction";
146 public static final String EXTRA_UNDO_DATA = "undoData";
147
Marcus Hagerott7333c372016-11-07 09:40:20 -0800148 // For debugging and testing what happens when requests are queued up.
149 public static final String ACTION_SLEEP = "sleep";
150 public static final String EXTRA_SLEEP_DURATION = "sleepDuration";
Marcus Hagerott819214d2016-09-29 14:58:27 -0700151
152 public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
Gary Maib9065dd2016-11-08 10:49:00 -0800153 public static final String BROADCAST_LINK_COMPLETE = "linkComplete";
154 public static final String BROADCAST_UNLINK_COMPLETE = "unlinkComplete";
Marcus Hagerott7333c372016-11-07 09:40:20 -0800155
156 public static final String BROADCAST_SERVICE_STATE_CHANGED = "serviceStateChanged";
Marcus Hagerott819214d2016-09-29 14:58:27 -0700157
158 public static final String EXTRA_RESULT_CODE = "resultCode";
159 public static final String EXTRA_RESULT_COUNT = "count";
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700160
Gary Mai7efa9942016-05-12 11:26:49 -0700161 public static final int CP2_ERROR = 0;
162 public static final int CONTACTS_LINKED = 1;
163 public static final int CONTACTS_SPLIT = 2;
Gary Mai31d572e2016-06-03 14:04:32 -0700164 public static final int BAD_ARGUMENTS = 3;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700165 public static final int RESULT_UNKNOWN = 0;
166 public static final int RESULT_SUCCESS = 1;
167 public static final int RESULT_FAILURE = 2;
Gary Mai7efa9942016-05-12 11:26:49 -0700168
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700169 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
170 Data.MIMETYPE,
171 Data.IS_PRIMARY,
172 Data.DATA1,
173 Data.DATA2,
174 Data.DATA3,
175 Data.DATA4,
176 Data.DATA5,
177 Data.DATA6,
178 Data.DATA7,
179 Data.DATA8,
180 Data.DATA9,
181 Data.DATA10,
182 Data.DATA11,
183 Data.DATA12,
184 Data.DATA13,
185 Data.DATA14,
186 Data.DATA15
187 );
188
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800189 private static final int PERSIST_TRIES = 3;
190
Walter Jang0653de32015-07-24 12:12:40 -0700191 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
192
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800193 public interface Listener {
194 public void onServiceCompleted(Intent callbackIntent);
195 }
196
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100197 private static final CopyOnWriteArrayList<Listener> sListeners =
198 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800199
Marcus Hagerott7333c372016-11-07 09:40:20 -0800200 // Holds the current state of the service
201 private static final State sState = new State();
202
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800203 private Handler mMainHandler;
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700204 private GroupsDao mGroupsDao;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700205 private SimContactDao mSimContactDao;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800206
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700207 public ContactSaveService() {
208 super(TAG);
209 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800210 mMainHandler = new Handler(Looper.getMainLooper());
211 }
212
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700213 @Override
214 public void onCreate() {
215 super.onCreate();
216 mGroupsDao = new GroupsDaoImpl(this);
Marcus Hagerott66e8b222016-10-23 15:41:55 -0700217 mSimContactDao = SimContactDao.create(this);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700218 }
219
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800220 public static void registerListener(Listener listener) {
221 if (!(listener instanceof Activity)) {
222 throw new ClassCastException("Only activities can be registered to"
223 + " receive callback from " + ContactSaveService.class.getName());
224 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100225 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800226 }
227
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700228 public static boolean canUndo(Intent resultIntent) {
229 return resultIntent.hasExtra(EXTRA_UNDO_DATA);
230 }
231
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800232 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100233 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700234 }
235
Marcus Hagerott7333c372016-11-07 09:40:20 -0800236 public static State getState() {
237 return sState;
238 }
239
240 private void notifyStateChanged() {
241 LocalBroadcastManager.getInstance(this)
242 .sendBroadcast(new Intent(BROADCAST_SERVICE_STATE_CHANGED));
243 }
244
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800245 /**
246 * Returns true if the ContactSaveService was started successfully and false if an exception
247 * was thrown and a Toast error message was displayed.
248 */
249 public static boolean startService(Context context, Intent intent, int saveMode) {
250 try {
251 context.startService(intent);
252 } catch (Exception exception) {
253 final int resId;
254 switch (saveMode) {
Gary Mai363af602016-09-28 10:01:23 -0700255 case ContactEditorActivity.ContactEditor.SaveMode.SPLIT:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800256 resId = R.string.contactUnlinkErrorToast;
257 break;
Gary Mai363af602016-09-28 10:01:23 -0700258 case ContactEditorActivity.ContactEditor.SaveMode.RELOAD:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800259 resId = R.string.contactJoinErrorToast;
260 break;
Gary Mai363af602016-09-28 10:01:23 -0700261 case ContactEditorActivity.ContactEditor.SaveMode.CLOSE:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800262 resId = R.string.contactSavedErrorToast;
263 break;
264 default:
265 resId = R.string.contactGenericErrorToast;
266 }
267 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
268 return false;
269 }
270 return true;
271 }
272
273 /**
274 * Utility method that starts service and handles exception.
275 */
276 public static void startService(Context context, Intent intent) {
277 try {
278 context.startService(intent);
279 } catch (Exception exception) {
280 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
281 }
282 }
283
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700284 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800285 public Object getSystemService(String name) {
286 Object service = super.getSystemService(name);
287 if (service != null) {
288 return service;
289 }
290
291 return getApplicationContext().getSystemService(name);
292 }
293
Marcus Hagerott7333c372016-11-07 09:40:20 -0800294 // Parent classes Javadoc says not to override this method but we're doing it just to update
295 // our state which should be OK since we're still doing the work in onHandleIntent
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800296 @Override
Marcus Hagerott7333c372016-11-07 09:40:20 -0800297 public int onStartCommand(Intent intent, int flags, int startId) {
298 sState.onStart(intent);
299 notifyStateChanged();
300 return super.onStartCommand(intent, flags, startId);
301 }
302
303 @Override
304 protected void onHandleIntent(final Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800305 if (intent == null) {
306 Log.d(TAG, "onHandleIntent: could not handle null intent");
307 return;
308 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700309 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
310 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
311 // TODO: add more specific error string such as "Turn on Contacts
312 // permission to update your contacts"
313 showToast(R.string.contactSavedErrorToast);
314 return;
315 }
316
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700317 // Call an appropriate method. If we're sure it affects how incoming phone calls are
318 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700319 String action = intent.getAction();
320 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
321 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800322 } else if (ACTION_SAVE_CONTACT.equals(action)) {
323 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800324 } else if (ACTION_CREATE_GROUP.equals(action)) {
325 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800326 } else if (ACTION_RENAME_GROUP.equals(action)) {
327 renameGroup(intent);
328 } else if (ACTION_DELETE_GROUP.equals(action)) {
329 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700330 } else if (ACTION_UPDATE_GROUP.equals(action)) {
331 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800332 } else if (ACTION_SET_STARRED.equals(action)) {
333 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800334 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
335 setSuperPrimary(intent);
336 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
337 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800338 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
339 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800340 } else if (ACTION_DELETE_CONTACT.equals(action)) {
341 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700342 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
343 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800344 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
345 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800346 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
347 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700348 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
349 setSendToVoicemail(intent);
350 } else if (ACTION_SET_RINGTONE.equals(action)) {
351 setRingtone(intent);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700352 } else if (ACTION_UNDO.equals(action)) {
353 undo(intent);
Marcus Hagerott7333c372016-11-07 09:40:20 -0800354 } else if (ACTION_SLEEP.equals(action)) {
355 sleepForDebugging(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700356 }
Marcus Hagerott7333c372016-11-07 09:40:20 -0800357
358 sState.onFinish(intent);
359 notifyStateChanged();
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700360 }
361
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800362 /**
363 * Creates an intent that can be sent to this service to create a new raw contact
364 * using data presented as a set of ContentValues.
365 */
366 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700367 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700368 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800369 Intent serviceIntent = new Intent(
370 context, ContactSaveService.class);
371 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
372 if (account != null) {
373 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
374 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700375 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800376 }
377 serviceIntent.putParcelableArrayListExtra(
378 ContactSaveService.EXTRA_CONTENT_VALUES, values);
379
380 // Callback intent will be invoked by the service once the new contact is
381 // created. The service will put the URI of the new contact as "data" on
382 // the callback intent.
383 Intent callbackIntent = new Intent(context, callbackActivity);
384 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800385 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
386 return serviceIntent;
387 }
388
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700389 private void createRawContact(Intent intent) {
390 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
391 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700392 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700393 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
394 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
395
396 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
397 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
398 .withValue(RawContacts.ACCOUNT_NAME, accountName)
399 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700400 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700401 .build());
402
403 int size = valueList.size();
404 for (int i = 0; i < size; i++) {
405 ContentValues values = valueList.get(i);
406 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
407 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
408 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
409 .withValues(values)
410 .build());
411 }
412
413 ContentResolver resolver = getContentResolver();
414 ContentProviderResult[] results;
415 try {
416 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
417 } catch (Exception e) {
418 throw new RuntimeException("Failed to store new contact", e);
419 }
420
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700421 Uri rawContactUri = results[0].uri;
422 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
423
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800424 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700425 }
426
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700427 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800428 * Creates an intent that can be sent to this service to create a new raw contact
429 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800430 * This variant is more convenient to use when there is only one photo that can
431 * possibly be updated, as in the Contact Details screen.
432 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
433 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800434 */
Maurice Chu851222a2012-06-21 11:43:08 -0700435 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700436 String saveModeExtraKey, int saveMode, boolean isProfile,
437 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700438 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800439 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700440 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800441 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700442 callbackActivity, callbackAction, bundle,
443 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800444 }
445
446 /**
447 * Creates an intent that can be sent to this service to create a new raw contact
448 * using data presented as a set of ContentValues.
449 * This variant is used when multiple contacts' photos may be updated, as in the
450 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700451 *
Josh Garguse692e012012-01-18 14:53:11 -0800452 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700453 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
454 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800455 */
Maurice Chu851222a2012-06-21 11:43:08 -0700456 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700457 String saveModeExtraKey, int saveMode, boolean isProfile,
458 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700459 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000460 Intent serviceIntent = new Intent(
461 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800462 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
463 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700464 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800465 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
466
Josh Garguse692e012012-01-18 14:53:11 -0800467 if (updatedPhotos != null) {
468 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
469 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800470
Josh Garguse5d3f892012-04-11 11:56:15 -0700471 if (callbackActivity != null) {
472 // Callback intent will be invoked by the service once the contact is
473 // saved. The service will put the URI of the new contact as "data" on
474 // the callback intent.
475 Intent callbackIntent = new Intent(context, callbackActivity);
476 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700477 if (joinContactIdExtraKey != null && joinContactId != null) {
478 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
479 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700480 callbackIntent.setAction(callbackAction);
481 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
482 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800483 return serviceIntent;
484 }
485
486 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700487 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700488 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800489 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800490
Jay Shrauner08099782015-03-25 14:17:11 -0700491 if (state == null) {
492 Log.e(TAG, "Invalid arguments for saveContact request");
493 return;
494 }
495
benny.lin3a4e7a22014-01-08 10:58:08 +0800496 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800497 // Trim any empty fields, and RawContacts, before persisting
498 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700499 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800500
501 Uri lookupUri = null;
502
503 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700504
Josh Garguse692e012012-01-18 14:53:11 -0800505 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800506
Josh Gargusef15c8e2012-01-30 16:42:02 -0800507 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
508 long insertedRawContactId = -1;
509
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800510 // Attempt to persist changes
511 int tries = 0;
512 while (tries++ < PERSIST_TRIES) {
513 try {
514 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800515 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
516
517 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
518
519 for (CPOWrapper cpoWrapper : diffWrapper) {
520 diff.add(cpoWrapper.getOperation());
521 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700522
Katherine Kuana007e442011-07-07 09:25:34 -0700523 if (DEBUG) {
524 Log.v(TAG, "Content Provider Operations:");
525 for (ContentProviderOperation operation : diff) {
526 Log.v(TAG, operation.toString());
527 }
528 }
529
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700530 int numberProcessed = 0;
531 boolean batchFailed = false;
532 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
533 while (numberProcessed < diff.size()) {
534 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
535 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700536 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700537 batchFailed = true;
538 break;
539 } else {
540 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700541 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800542 }
543
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700544 if (batchFailed) {
545 // Retry save
546 continue;
547 }
548
Wenyi Wang67addcc2015-11-23 10:07:48 -0800549 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800550 if (rawContactId == -1) {
551 throw new IllegalStateException("Could not determine RawContact ID after save");
552 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800553 // We don't have to check to see if the value is still -1. If we reach here,
554 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800555 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700556 if (isProfile) {
557 // Since the profile supports local raw contacts, which may have been completely
558 // removed if all information was removed, we need to do a special query to
559 // get the lookup URI for the profile contact (if it still exists).
560 Cursor c = resolver.query(Profile.CONTENT_URI,
561 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
562 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800563 if (c == null) {
564 continue;
565 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700566 try {
Erik162b7e32011-09-20 15:23:55 -0700567 if (c.moveToFirst()) {
568 final long contactId = c.getLong(0);
569 final String lookupKey = c.getString(1);
570 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
571 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700572 } finally {
573 c.close();
574 }
575 } else {
576 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
577 rawContactId);
578 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
579 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800580 if (lookupUri != null) {
581 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
582 }
Josh Garguse692e012012-01-18 14:53:11 -0800583
584 // We can change this back to false later, if we fail to save the contact photo.
585 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800586 break;
587
588 } catch (RemoteException e) {
589 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700590 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800591 break;
592
Jay Shrauner57fca182014-01-17 14:20:50 -0800593 } catch (IllegalArgumentException e) {
594 // This is thrown by applyBatch on malformed requests
Walter Jang3a0b4832016-10-12 11:02:54 -0700595 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Jay Shrauner57fca182014-01-17 14:20:50 -0800596 showToast(R.string.contactSavedErrorToast);
597 break;
598
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800599 } catch (OperationApplicationException e) {
600 // Version consistency failed, re-parent change and try again
601 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
602 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
603 boolean first = true;
604 final int count = state.size();
605 for (int i = 0; i < count; i++) {
606 Long rawContactId = state.getRawContactId(i);
607 if (rawContactId != null && rawContactId != -1) {
608 if (!first) {
609 sb.append(',');
610 }
611 sb.append(rawContactId);
612 first = false;
613 }
614 }
615 sb.append(")");
616
617 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800618 throw new IllegalStateException(
619 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800620 }
621
Maurice Chu851222a2012-06-21 11:43:08 -0700622 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700623 isProfile
624 ? RawContactsEntity.PROFILE_CONTENT_URI
625 : RawContactsEntity.CONTENT_URI,
626 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700627 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700628
629 // Update the new state to use profile URIs if appropriate.
630 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700631 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700632 delta.setProfileQueryUri();
633 }
634 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800635 }
636 }
637
Josh Garguse692e012012-01-18 14:53:11 -0800638 // Now save any updated photos. We do this at the end to ensure that
639 // the ContactProvider already knows about newly-created contacts.
640 if (updatedPhotos != null) {
641 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700642 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800643 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800644
645 // If the raw-contact ID is negative, we are saving a new raw-contact;
646 // replace the bogus ID with the new one that we actually saved the contact at.
647 if (rawContactId < 0) {
648 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800649 }
650
Jay Shrauner511561d2015-04-02 10:35:33 -0700651 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700652 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700653 succeeded = false;
654 }
Josh Garguse692e012012-01-18 14:53:11 -0800655 }
656 }
657
Josh Garguse5d3f892012-04-11 11:56:15 -0700658 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
659 if (callbackIntent != null) {
660 if (succeeded) {
661 // Mark the intent to indicate that the save was successful (even if the lookup URI
662 // is now null). For local contacts or the local profile, it's possible that the
663 // save triggered removal of the contact, so no lookup URI would exist..
664 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
665 }
666 callbackIntent.setData(lookupUri);
667 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800668 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800669 }
670
Josh Garguse692e012012-01-18 14:53:11 -0800671 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700672 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
673 * subsets, adds the returned array to "results".
674 *
675 * @return the size of the array, if not null; -1 when the array is null.
676 */
677 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
678 ContentProviderResult[] results, ContentResolver resolver)
679 throws RemoteException, OperationApplicationException {
680 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
681 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
682 subset.addAll(diff.subList(offset, offset + subsetCount));
683 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
684 .AUTHORITY, subset);
685 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
686 return -1;
687 }
688 for (ContentProviderResult c : subsetResult) {
689 results[offset++] = c;
690 }
691 return subsetResult.length;
692 }
693
694 /**
Josh Garguse692e012012-01-18 14:53:11 -0800695 * Save updated photo for the specified raw-contact.
696 * @return true for success, false for failure
697 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800698 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800699 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800700 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
701 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
702
benny.lin3a4e7a22014-01-08 10:58:08 +0800703 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800704 }
705
Josh Gargusef15c8e2012-01-30 16:42:02 -0800706 /**
707 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
708 */
Maurice Chu851222a2012-06-21 11:43:08 -0700709 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800710 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800711 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800712 long existingRawContactId = state.findRawContactId();
713 if (existingRawContactId != -1) {
714 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800715 }
716
Wenyi Wang67addcc2015-11-23 10:07:48 -0800717 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800718 }
719
720 /**
721 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
722 */
723 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800724 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800725 if (results == null) {
726 return -1;
727 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800728 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800729 final int numResults = results.length;
730 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800731 final CPOWrapper cpoWrapper = diffWrapper.get(i);
732 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
733 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
734 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800735 return ContentUris.parseId(results[i].uri);
736 }
737 }
738 return -1;
739 }
740
741 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700742 * Creates an intent that can be sent to this service to create a new group as
743 * well as add new members at the same time.
744 *
745 * @param context of the application
746 * @param account in which the group should be created
747 * @param label is the name of the group (cannot be null)
748 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
749 * should be added to the group
750 * @param callbackActivity is the activity to send the callback intent to
751 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700752 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700753 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700754 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700755 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800756 Intent serviceIntent = new Intent(context, ContactSaveService.class);
757 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
758 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
759 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700760 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800761 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700762 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700763
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800764 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700765 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800766 Intent callbackIntent = new Intent(context, callbackActivity);
767 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700768 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800769
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700770 return serviceIntent;
771 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800772
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800773 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700774 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
775 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
776 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
777 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700778 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800779
Katherine Kuan717e3432011-07-13 17:03:24 -0700780 // Create the new group
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700781 final Uri groupUri = mGroupsDao.create(label,
782 new AccountWithDataSet(accountName, accountType, dataSet));
783 final ContentResolver resolver = getContentResolver();
Katherine Kuan717e3432011-07-13 17:03:24 -0700784
785 // If there's no URI, then the insertion failed. Abort early because group members can't be
786 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800787 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700788 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800789 return;
790 }
791
Katherine Kuan717e3432011-07-13 17:03:24 -0700792 // Add new group members
793 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
794
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700795 ContentValues values = new ContentValues();
Katherine Kuan717e3432011-07-13 17:03:24 -0700796 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
Walter Jang8bac28b2016-08-30 10:34:55 -0700797 // with the way other intent extras that are passed to the
Gary Mai363af602016-09-28 10:01:23 -0700798 // {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800799 values.clear();
800 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
801 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
802
803 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700804 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700805 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800806 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800807 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800808 }
809
810 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800811 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800812 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700813 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700814 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800815 Intent serviceIntent = new Intent(context, ContactSaveService.class);
816 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
817 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
818 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700819
820 // Callback intent will be invoked by the service once the group is renamed.
821 Intent callbackIntent = new Intent(context, callbackActivity);
822 callbackIntent.setAction(callbackAction);
823 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
824
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800825 return serviceIntent;
826 }
827
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800828 private void renameGroup(Intent intent) {
829 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
830 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
831
832 if (groupId == -1) {
833 Log.e(TAG, "Invalid arguments for renameGroup request");
834 return;
835 }
836
837 ContentValues values = new ContentValues();
838 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700839 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
840 getContentResolver().update(groupUri, values, null, null);
841
842 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
843 callbackIntent.setData(groupUri);
844 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800845 }
846
847 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800848 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800849 */
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700850 public static Intent createGroupDeletionIntent(Context context, long groupId) {
851 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800852 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800853 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700854
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800855 return serviceIntent;
856 }
857
858 private void deleteGroup(Intent intent) {
859 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
860 if (groupId == -1) {
861 Log.e(TAG, "Invalid arguments for deleteGroup request");
862 return;
863 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700864 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800865
Marcus Hagerott819214d2016-09-29 14:58:27 -0700866 final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700867 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
868 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
869 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
Walter Jang72f99882016-05-26 09:01:31 -0700870
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700871 mGroupsDao.delete(groupUri);
872
873 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
874 }
875
876 public static Intent createUndoIntent(Context context, Intent resultIntent) {
877 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
878 serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
879 serviceIntent.putExtras(resultIntent);
880 return serviceIntent;
881 }
882
883 private void undo(Intent intent) {
884 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
885 if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
886 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
Walter Jang72f99882016-05-26 09:01:31 -0700887 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800888 }
889
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700890
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800891 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700892 * Creates an intent that can be sent to this service to rename a group as
893 * well as add and remove members from the group.
894 *
895 * @param context of the application
896 * @param groupId of the group that should be modified
897 * @param newLabel is the updated name of the group (can be null if the name
898 * should not be updated)
899 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
900 * should be added to the group
901 * @param rawContactsToRemove is an array of raw contact IDs for contacts
902 * that should be removed from the group
903 * @param callbackActivity is the activity to send the callback intent to
904 * @param callbackAction is the intent action for the callback intent
905 */
906 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
907 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700908 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700909 Intent serviceIntent = new Intent(context, ContactSaveService.class);
910 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
911 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
912 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
913 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
914 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
915 rawContactsToRemove);
916
917 // Callback intent will be invoked by the service once the group is updated
918 Intent callbackIntent = new Intent(context, callbackActivity);
919 callbackIntent.setAction(callbackAction);
920 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
921
922 return serviceIntent;
923 }
924
925 private void updateGroup(Intent intent) {
926 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
927 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
928 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
929 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
930
931 if (groupId == -1) {
932 Log.e(TAG, "Invalid arguments for updateGroup request");
933 return;
934 }
935
936 final ContentResolver resolver = getContentResolver();
937 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
938
939 // Update group name if necessary
940 if (label != null) {
941 ContentValues values = new ContentValues();
942 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700943 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700944 }
945
Katherine Kuan717e3432011-07-13 17:03:24 -0700946 // Add and remove members if necessary
947 addMembersToGroup(resolver, rawContactsToAdd, groupId);
948 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
949
950 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
951 callbackIntent.setData(groupUri);
952 deliverCallback(callbackIntent);
953 }
954
Walter Jang3a0b4832016-10-12 11:02:54 -0700955 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700956 long groupId) {
957 if (rawContactsToAdd == null) {
958 return;
959 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700960 for (long rawContactId : rawContactsToAdd) {
961 try {
962 final ArrayList<ContentProviderOperation> rawContactOperations =
963 new ArrayList<ContentProviderOperation>();
964
965 // Build an assert operation to ensure the contact is not already in the group
966 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
967 .newAssertQuery(Data.CONTENT_URI);
968 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
969 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
970 new String[] { String.valueOf(rawContactId),
971 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
972 assertBuilder.withExpectedCount(0);
973 rawContactOperations.add(assertBuilder.build());
974
975 // Build an insert operation to add the contact to the group
976 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
977 .newInsert(Data.CONTENT_URI);
978 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
979 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
980 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
981 rawContactOperations.add(insertBuilder.build());
982
983 if (DEBUG) {
984 for (ContentProviderOperation operation : rawContactOperations) {
985 Log.v(TAG, operation.toString());
986 }
987 }
988
989 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700990 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800991 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700992 }
993 } catch (RemoteException e) {
994 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700995 FeedbackHelper.sendFeedback(this, TAG,
996 "Problem persisting user edits for raw contact ID " +
997 String.valueOf(rawContactId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700998 } catch (OperationApplicationException e) {
999 // The assert could have failed because the contact is already in the group,
1000 // just continue to the next contact
Walter Jang3a0b4832016-10-12 11:02:54 -07001001 FeedbackHelper.sendFeedback(this, TAG,
1002 "Assert failed in adding raw contact ID " +
1003 String.valueOf(rawContactId) + ". Already exists in group " +
1004 String.valueOf(groupId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001005 }
1006 }
Katherine Kuan717e3432011-07-13 17:03:24 -07001007 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001008
Daniel Lehmann18958a22012-02-28 17:45:25 -08001009 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -07001010 long groupId) {
1011 if (rawContactsToRemove == null) {
1012 return;
1013 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001014 for (long rawContactId : rawContactsToRemove) {
1015 // Apply the delete operation on the data row for the given raw contact's
1016 // membership in the given group. If no contact matches the provided selection, then
1017 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -08001018 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001019 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1020 new String[] { String.valueOf(rawContactId),
1021 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
1022 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001023 }
1024
1025 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001026 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001027 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001028 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
1029 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1030 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
1031 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1032 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
1033
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001034 return serviceIntent;
1035 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001036
1037 private void setStarred(Intent intent) {
1038 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1039 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
1040 if (contactUri == null) {
1041 Log.e(TAG, "Invalid arguments for setStarred request");
1042 return;
1043 }
1044
1045 final ContentValues values = new ContentValues(1);
1046 values.put(Contacts.STARRED, value);
1047 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -07001048
1049 // Undemote the contact if necessary
1050 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
1051 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -08001052 if (c == null) {
1053 return;
1054 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001055 try {
1056 if (c.moveToFirst()) {
1057 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -07001058
1059 // Don't bother undemoting if this contact is the user's profile.
1060 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -08001061 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -07001062 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001063 }
1064 } finally {
1065 c.close();
1066 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001067 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001068
1069 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -07001070 * Creates an intent that can be sent to this service to set the redirect to voicemail.
1071 */
1072 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
1073 boolean value) {
1074 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1075 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1076 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1077 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1078
1079 return serviceIntent;
1080 }
1081
1082 private void setSendToVoicemail(Intent intent) {
1083 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1084 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1085 if (contactUri == null) {
1086 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1087 return;
1088 }
1089
1090 final ContentValues values = new ContentValues(1);
1091 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1092 getContentResolver().update(contactUri, values, null, null);
1093 }
1094
1095 /**
1096 * Creates an intent that can be sent to this service to save the contact's ringtone.
1097 */
1098 public static Intent createSetRingtone(Context context, Uri contactUri,
1099 String value) {
1100 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1101 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1102 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1103 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1104
1105 return serviceIntent;
1106 }
1107
1108 private void setRingtone(Intent intent) {
1109 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1110 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1111 if (contactUri == null) {
1112 Log.e(TAG, "Invalid arguments for setRingtone");
1113 return;
1114 }
1115 ContentValues values = new ContentValues(1);
1116 values.put(Contacts.CUSTOM_RINGTONE, value);
1117 getContentResolver().update(contactUri, values, null, null);
1118 }
1119
1120 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001121 * Creates an intent that sets the selected data item as super primary (default)
1122 */
1123 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1124 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1125 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1126 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1127 return serviceIntent;
1128 }
1129
1130 private void setSuperPrimary(Intent intent) {
1131 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1132 if (dataId == -1) {
1133 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1134 return;
1135 }
1136
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001137 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001138 }
1139
1140 /**
1141 * Creates an intent that clears the primary flag of all data items that belong to the same
1142 * raw_contact as the given data item. Will only clear, if the data item was primary before
1143 * this call
1144 */
1145 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1146 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1147 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1148 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1149 return serviceIntent;
1150 }
1151
1152 private void clearPrimary(Intent intent) {
1153 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1154 if (dataId == -1) {
1155 Log.e(TAG, "Invalid arguments for clearPrimary request");
1156 return;
1157 }
1158
1159 // Update the primary values in the data record.
1160 ContentValues values = new ContentValues(1);
1161 values.put(Data.IS_SUPER_PRIMARY, 0);
1162 values.put(Data.IS_PRIMARY, 0);
1163
1164 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1165 values, null, null);
1166 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001167
1168 /**
1169 * Creates an intent that can be sent to this service to delete a contact.
1170 */
1171 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1172 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1173 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1174 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1175 return serviceIntent;
1176 }
1177
Brian Attwelld2962a32015-03-02 14:48:50 -08001178 /**
1179 * Creates an intent that can be sent to this service to delete multiple contacts.
1180 */
1181 public static Intent createDeleteMultipleContactsIntent(Context context,
James Laskeye5a140a2016-10-18 15:43:42 -07001182 long[] contactIds, final String[] names) {
Brian Attwelld2962a32015-03-02 14:48:50 -08001183 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1184 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1185 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
James Laskeye5a140a2016-10-18 15:43:42 -07001186 serviceIntent.putExtra(ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY, names);
Brian Attwelld2962a32015-03-02 14:48:50 -08001187 return serviceIntent;
1188 }
1189
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001190 private void deleteContact(Intent intent) {
1191 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1192 if (contactUri == null) {
1193 Log.e(TAG, "Invalid arguments for deleteContact request");
1194 return;
1195 }
1196
1197 getContentResolver().delete(contactUri, null, null);
1198 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001199
Brian Attwelld2962a32015-03-02 14:48:50 -08001200 private void deleteMultipleContacts(Intent intent) {
1201 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1202 if (contactIds == null) {
1203 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1204 return;
1205 }
1206 for (long contactId : contactIds) {
1207 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1208 getContentResolver().delete(contactUri, null, null);
1209 }
James Laskeye5a140a2016-10-18 15:43:42 -07001210 final String[] names = intent.getStringArrayExtra(
1211 ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY);
1212 final String deleteToastMessage;
James Laskey56019ad2016-11-14 16:38:35 -08001213 if (contactIds.length != names.length || names.length == 0) {
James Laskeye5a140a2016-10-18 15:43:42 -07001214 deleteToastMessage = getResources().getQuantityString(
1215 R.plurals.contacts_deleted_toast, contactIds.length);
1216 } else if (names.length == 1) {
1217 deleteToastMessage = getResources().getString(
1218 R.string.contacts_deleted_one_named_toast, names);
1219 } else if (names.length == 2) {
1220 deleteToastMessage = getResources().getString(
1221 R.string.contacts_deleted_two_named_toast, names);
1222 } else {
1223 deleteToastMessage = getResources().getString(
1224 R.string.contacts_deleted_many_named_toast, names);
1225 }
James Laskey56019ad2016-11-14 16:38:35 -08001226
Wenyi Wang687d2182015-10-28 17:03:18 -07001227 mMainHandler.post(new Runnable() {
1228 @Override
1229 public void run() {
1230 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1231 .show();
1232 }
1233 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001234 }
1235
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001236 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001237 * 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 -08001238 * pieces. This will set the raw contact ids to {@link AggregationExceptions#TYPE_AUTOMATIC} so
Gary Mai53fe0d22016-07-26 17:23:53 -07001239 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001240 */
1241 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1242 ResultReceiver receiver) {
1243 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1244 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1245 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1246 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1247 return serviceIntent;
1248 }
1249
Gary Maib9065dd2016-11-08 10:49:00 -08001250 /**
1251 * Creates an intent that can be sent to this service to split a contact into it's constituent
1252 * pieces. This will explicitly set the raw contact ids to
1253 * {@link AggregationExceptions#TYPE_KEEP_SEPARATE}.
1254 */
1255 public static Intent createHardSplitContactIntent(Context context, long[][] rawContactIds) {
1256 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1257 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1258 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1259 serviceIntent.putExtra(ContactSaveService.EXTRA_HARD_SPLIT, true);
1260 return serviceIntent;
1261 }
1262
Gary Mai7efa9942016-05-12 11:26:49 -07001263 private void splitContact(Intent intent) {
1264 final long rawContactIds[][] = (long[][]) intent
1265 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001266 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Maib9065dd2016-11-08 10:49:00 -08001267 final boolean hardSplit = intent.getBooleanExtra(EXTRA_HARD_SPLIT, false);
Gary Mai7efa9942016-05-12 11:26:49 -07001268 if (rawContactIds == null) {
1269 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001270 if (receiver != null) {
1271 receiver.send(BAD_ARGUMENTS, new Bundle());
1272 }
Gary Mai7efa9942016-05-12 11:26:49 -07001273 return;
1274 }
1275 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1276 final ContentResolver resolver = getContentResolver();
1277 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001278 for (int i = 0; i < rawContactIds.length; i++) {
1279 for (int j = 0; j < rawContactIds.length; j++) {
1280 if (i != j) {
Gary Maib9065dd2016-11-08 10:49:00 -08001281 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j],
1282 hardSplit)) {
Gary Mai7efa9942016-05-12 11:26:49 -07001283 if (receiver != null) {
1284 receiver.send(CP2_ERROR, new Bundle());
1285 return;
1286 }
1287 }
1288 }
1289 }
1290 }
1291 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1292 if (receiver != null) {
1293 receiver.send(CP2_ERROR, new Bundle());
1294 }
1295 return;
1296 }
Gary Maib9065dd2016-11-08 10:49:00 -08001297 LocalBroadcastManager.getInstance(this)
1298 .sendBroadcast(new Intent(BROADCAST_UNLINK_COMPLETE));
Gary Mai7efa9942016-05-12 11:26:49 -07001299 if (receiver != null) {
1300 receiver.send(CONTACTS_SPLIT, new Bundle());
1301 } else {
1302 showToast(R.string.contactUnlinkedToast);
1303 }
1304 }
1305
1306 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001307 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001308 * and {@param rawContactIds2} to {@param operations}.
1309 * @return false if an error occurred, true otherwise.
1310 */
1311 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
Gary Maib9065dd2016-11-08 10:49:00 -08001312 long[] rawContactIds1, long[] rawContactIds2, boolean hardSplit) {
Gary Mai7efa9942016-05-12 11:26:49 -07001313 if (rawContactIds1 == null || rawContactIds2 == null) {
1314 Log.e(TAG, "Invalid arguments for splitContact request");
1315 return false;
1316 }
1317 // For each pair of raw contacts, insert an aggregation exception
1318 final ContentResolver resolver = getContentResolver();
1319 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1320 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1321 for (int i = 0; i < rawContactIds1.length; i++) {
1322 for (int j = 0; j < rawContactIds2.length; j++) {
Gary Maib9065dd2016-11-08 10:49:00 -08001323 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j], hardSplit);
Gary Mai7efa9942016-05-12 11:26:49 -07001324 // Before we get to 500 we need to flush the operations list
1325 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1326 if (!applyOperations(resolver, operations)) {
1327 return false;
1328 }
1329 operations.clear();
1330 }
1331 }
1332 }
1333 return true;
1334 }
1335
1336 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001337 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001338 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001339 */
1340 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001341 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001342 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1343 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1344 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1345 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001346
1347 // Callback intent will be invoked by the service once the contacts are joined.
1348 Intent callbackIntent = new Intent(context, callbackActivity);
1349 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001350 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1351
1352 return serviceIntent;
1353 }
1354
Brian Attwelld3946ca2015-03-03 11:13:49 -08001355 /**
1356 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1357 * No special attention is paid to where the resulting contact's name is taken from.
1358 */
Gary Mai7efa9942016-05-12 11:26:49 -07001359 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1360 ResultReceiver receiver) {
1361 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001362 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1363 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001364 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001365 return serviceIntent;
1366 }
1367
Gary Mai7efa9942016-05-12 11:26:49 -07001368 /**
1369 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1370 * No special attention is paid to where the resulting contact's name is taken from.
1371 */
1372 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1373 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1374 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001375
1376 private interface JoinContactQuery {
1377 String[] PROJECTION = {
1378 RawContacts._ID,
1379 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001380 RawContacts.DISPLAY_NAME_SOURCE,
1381 };
1382
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001383 int _ID = 0;
1384 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001385 int DISPLAY_NAME_SOURCE = 2;
1386 }
1387
1388 private interface ContactEntityQuery {
1389 String[] PROJECTION = {
1390 Contacts.Entity.DATA_ID,
1391 Contacts.Entity.CONTACT_ID,
1392 Contacts.Entity.IS_SUPER_PRIMARY,
1393 };
1394 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1395 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1396 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1397 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1398
1399 int DATA_ID = 0;
1400 int CONTACT_ID = 1;
1401 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001402 }
1403
Brian Attwelld3946ca2015-03-03 11:13:49 -08001404 private void joinSeveralContacts(Intent intent) {
1405 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001406
Gary Mai7efa9942016-05-12 11:26:49 -07001407 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001408
Brian Attwelld3946ca2015-03-03 11:13:49 -08001409 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001410 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1411 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001412 if (rawContactIds == null) {
1413 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001414 if (receiver != null) {
1415 receiver.send(BAD_ARGUMENTS, new Bundle());
1416 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001417 return;
1418 }
1419
Brian Attwelld3946ca2015-03-03 11:13:49 -08001420 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001421 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001422 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1423 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1424 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001425 for (int i = 0; i < rawContactIds.length; i++) {
1426 for (int j = 0; j < rawContactIds.length; j++) {
1427 if (i != j) {
1428 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1429 }
Walter Jang0653de32015-07-24 12:12:40 -07001430 // Before we get to 500 we need to flush the operations list
1431 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001432 if (!applyOperations(resolver, operations)) {
1433 if (receiver != null) {
1434 receiver.send(CP2_ERROR, new Bundle());
1435 }
Walter Jang0653de32015-07-24 12:12:40 -07001436 return;
1437 }
1438 operations.clear();
1439 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001440 }
1441 }
Gary Mai7efa9942016-05-12 11:26:49 -07001442 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1443 if (receiver != null) {
1444 receiver.send(CP2_ERROR, new Bundle());
1445 }
Walter Jang0653de32015-07-24 12:12:40 -07001446 return;
1447 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001448
John Shaoa3c507a2016-09-13 14:26:17 -07001449
1450 final String name = queryNameOfLinkedContacts(contactIds);
1451 if (name != null) {
1452 if (receiver != null) {
1453 final Bundle result = new Bundle();
1454 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1455 result.putString(EXTRA_DISPLAY_NAME, name);
1456 receiver.send(CONTACTS_LINKED, result);
1457 } else {
James Laskeyf62b4882016-10-21 11:36:40 -07001458 if (TextUtils.isEmpty(name)) {
1459 showToast(R.string.contactsJoinedMessage);
1460 } else {
1461 showToast(R.string.contactsJoinedNamedMessage, name);
1462 }
John Shaoa3c507a2016-09-13 14:26:17 -07001463 }
Gary Maib9065dd2016-11-08 10:49:00 -08001464 LocalBroadcastManager.getInstance(this)
1465 .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
Gary Mai7efa9942016-05-12 11:26:49 -07001466 } else {
John Shaoa3c507a2016-09-13 14:26:17 -07001467 if (receiver != null) {
1468 receiver.send(CP2_ERROR, new Bundle());
1469 }
1470 showToast(R.string.contactJoinErrorToast);
Gary Mai7efa9942016-05-12 11:26:49 -07001471 }
Walter Jang0653de32015-07-24 12:12:40 -07001472 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001473
John Shaoa3c507a2016-09-13 14:26:17 -07001474 /** Get the display name of the top-level contact after the contacts have been linked. */
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001475 private String queryNameOfLinkedContacts(long[] contactIds) {
1476 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1477 final String[] whereArgs = new String[contactIds.length];
1478 for (int i = 0; i < contactIds.length; i++) {
1479 whereArgs[i] = String.valueOf(contactIds[i]);
1480 whereBuilder.append("?,");
1481 }
1482 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1483 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
James Laskeyf62b4882016-10-21 11:36:40 -07001484 new String[]{Contacts._ID, Contacts.DISPLAY_NAME,
1485 Contacts.DISPLAY_NAME_ALTERNATIVE},
John Shaoa3c507a2016-09-13 14:26:17 -07001486 whereBuilder.toString(), whereArgs, null);
1487
1488 String name = null;
James Laskeyf62b4882016-10-21 11:36:40 -07001489 String nameAlt = null;
John Shaoa3c507a2016-09-13 14:26:17 -07001490 long contactId = 0;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001491 try {
1492 if (cursor.moveToFirst()) {
John Shaoa3c507a2016-09-13 14:26:17 -07001493 contactId = cursor.getLong(0);
1494 name = cursor.getString(1);
James Laskeyf62b4882016-10-21 11:36:40 -07001495 nameAlt = cursor.getString(2);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001496 }
John Shaoa3c507a2016-09-13 14:26:17 -07001497 while(cursor.moveToNext()) {
1498 if (cursor.getLong(0) != contactId) {
1499 return null;
1500 }
1501 }
James Laskeyf62b4882016-10-21 11:36:40 -07001502
1503 final String formattedName = ContactDisplayUtils.getPreferredDisplayName(name, nameAlt,
1504 new ContactsPreferences(getApplicationContext()));
1505 return formattedName == null ? "" : formattedName;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001506 } finally {
John Shaoa3c507a2016-09-13 14:26:17 -07001507 if (cursor != null) {
1508 cursor.close();
1509 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001510 }
1511 }
1512
Walter Jang0653de32015-07-24 12:12:40 -07001513 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001514 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001515 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001516 try {
John Shaoa3c507a2016-09-13 14:26:17 -07001517 final ContentProviderResult[] result =
1518 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1519 for (int i = 0; i < result.length; ++i) {
1520 // if no rows were modified in the operation then we count it as fail.
1521 if (result[i].count < 0) {
1522 throw new OperationApplicationException();
1523 }
1524 }
Walter Jang0653de32015-07-24 12:12:40 -07001525 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001526 } catch (RemoteException | OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001527 FeedbackHelper.sendFeedback(this, TAG,
1528 "Failed to apply aggregation exception batch", e);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001529 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001530 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001531 }
1532 }
1533
Brian Attwelld3946ca2015-03-03 11:13:49 -08001534 private void joinContacts(Intent intent) {
1535 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1536 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001537
1538 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001539 // in the join UIs.
1540 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1541 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001542 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001543 return;
1544 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001545
Brian Attwell548f5c62015-01-27 17:46:46 -08001546 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001547
1548 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001549 for (int i = 0; i < rawContactIds.length; i++) {
1550 for (int j = 0; j < rawContactIds.length; j++) {
1551 if (i != j) {
1552 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1553 }
1554 }
1555 }
1556
Brian Attwelld3946ca2015-03-03 11:13:49 -08001557 final ContentResolver resolver = getContentResolver();
1558
Brian Attwell548f5c62015-01-27 17:46:46 -08001559 // Use the name for contactId1 as the name for the newly aggregated contact.
1560 final Uri contactId1Uri = ContentUris.withAppendedId(
1561 Contacts.CONTENT_URI, contactId1);
1562 final Uri entityUri = Uri.withAppendedPath(
1563 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1564 Cursor c = resolver.query(entityUri,
1565 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1566 if (c == null) {
1567 Log.e(TAG, "Unable to open Contacts DB cursor");
1568 showToast(R.string.contactSavedErrorToast);
1569 return;
1570 }
1571 long dataIdToAddSuperPrimary = -1;
1572 try {
1573 if (c.moveToFirst()) {
1574 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1575 }
1576 } finally {
1577 c.close();
1578 }
1579
1580 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1581 // display name does not change as a result of the join.
1582 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001583 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001584 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1585 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1586 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001587 operations.add(builder.build());
1588 }
1589
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001590 // Apply all aggregation exceptions as one batch
John Shaoa3c507a2016-09-13 14:26:17 -07001591 final boolean success = applyOperations(resolver, operations);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001592
John Shaoa3c507a2016-09-13 14:26:17 -07001593 final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2});
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001594 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
John Shaoa3c507a2016-09-13 14:26:17 -07001595 if (success && name != null) {
James Laskeyf62b4882016-10-21 11:36:40 -07001596 if (TextUtils.isEmpty(name)) {
1597 showToast(R.string.contactsJoinedMessage);
1598 } else {
1599 showToast(R.string.contactsJoinedNamedMessage, name);
1600 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001601 Uri uri = RawContacts.getContactLookupUri(resolver,
1602 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1603 callbackIntent.setData(uri);
Gary Maib9065dd2016-11-08 10:49:00 -08001604 LocalBroadcastManager.getInstance(this)
1605 .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001606 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001607 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001608 }
1609
Gary Mai7efa9942016-05-12 11:26:49 -07001610 /**
1611 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1612 * array of the return value holds an array of raw contact ids for one contactId.
1613 * @param contactIds
1614 * @return
1615 */
1616 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1617 final long[][] rawContactIds = new long[contactIds.length][];
1618 for (int i = 0; i < contactIds.length; i++) {
1619 rawContactIds[i] = getRawContactIds(contactIds[i]);
1620 }
1621 return rawContactIds;
1622 }
1623
1624 /**
1625 * Gets the raw contact ids associated with {@param contactId}.
1626 * @param contactId
1627 * @return Array of raw contact ids.
1628 */
1629 private long[] getRawContactIds(long contactId) {
1630 final ContentResolver resolver = getContentResolver();
1631 long rawContactIds[];
1632
1633 final StringBuilder queryBuilder = new StringBuilder();
1634 queryBuilder.append(RawContacts.CONTACT_ID)
1635 .append("=")
1636 .append(String.valueOf(contactId));
1637
1638 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1639 JoinContactQuery.PROJECTION,
1640 queryBuilder.toString(),
1641 null, null);
1642 if (c == null) {
1643 Log.e(TAG, "Unable to open Contacts DB cursor");
1644 return null;
1645 }
1646 try {
1647 rawContactIds = new long[c.getCount()];
1648 for (int i = 0; i < rawContactIds.length; i++) {
1649 c.moveToPosition(i);
1650 final long rawContactId = c.getLong(JoinContactQuery._ID);
1651 rawContactIds[i] = rawContactId;
1652 }
1653 } finally {
1654 c.close();
1655 }
1656 return rawContactIds;
1657 }
1658
Brian Attwelld3946ca2015-03-03 11:13:49 -08001659 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1660 if (contactIds == null) {
1661 return null;
1662 }
1663
Brian Attwell548f5c62015-01-27 17:46:46 -08001664 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001665
1666 final StringBuilder queryBuilder = new StringBuilder();
1667 final String stringContactIds[] = new String[contactIds.length];
1668 for (int i = 0; i < contactIds.length; i++) {
1669 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1670 stringContactIds[i] = String.valueOf(contactIds[i]);
1671 if (contactIds[i] == -1) {
1672 return null;
1673 }
1674 if (i == contactIds.length -1) {
1675 break;
1676 }
1677 queryBuilder.append(" OR ");
1678 }
1679
Brian Attwell548f5c62015-01-27 17:46:46 -08001680 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1681 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001682 queryBuilder.toString(),
1683 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001684 if (c == null) {
1685 Log.e(TAG, "Unable to open Contacts DB cursor");
1686 showToast(R.string.contactSavedErrorToast);
1687 return null;
1688 }
Gary Mai7efa9942016-05-12 11:26:49 -07001689 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001690 try {
1691 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001692 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001693 return null;
1694 }
1695 rawContactIds = new long[c.getCount()];
1696 for (int i = 0; i < rawContactIds.length; i++) {
1697 c.moveToPosition(i);
1698 long rawContactId = c.getLong(JoinContactQuery._ID);
1699 rawContactIds[i] = rawContactId;
1700 }
1701 } finally {
1702 c.close();
1703 }
1704 return rawContactIds;
1705 }
1706
Brian Attwelld3946ca2015-03-03 11:13:49 -08001707 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1708 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1709 }
1710
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001711 /**
1712 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1713 */
1714 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1715 long rawContactId1, long rawContactId2) {
1716 Builder builder =
1717 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1718 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1719 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1720 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1721 operations.add(builder.build());
1722 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001723
1724 /**
Gary Maib9065dd2016-11-08 10:49:00 -08001725 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} or a
1726 * {@link AggregationExceptions#TYPE_KEEP_SEPARATE} ContentProviderOperation if a hard split is
1727 * requested.
Gary Mai7efa9942016-05-12 11:26:49 -07001728 */
1729 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
Gary Maib9065dd2016-11-08 10:49:00 -08001730 long rawContactId1, long rawContactId2, boolean hardSplit) {
Gary Mai7efa9942016-05-12 11:26:49 -07001731 final Builder builder =
1732 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Maib9065dd2016-11-08 10:49:00 -08001733 builder.withValue(AggregationExceptions.TYPE,
1734 hardSplit
1735 ? AggregationExceptions.TYPE_KEEP_SEPARATE
1736 : AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001737 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1738 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1739 operations.add(builder.build());
1740 }
1741
Marcus Hagerott7333c372016-11-07 09:40:20 -08001742 /**
Marcus Hagerott7333c372016-11-07 09:40:20 -08001743 * Returns an intent that can start this service and cause it to sleep for the specified time.
1744 *
1745 * This exists purely for debugging and manual testing. Since this service uses a single thread
1746 * it is useful to have a way to test behavior when work is queued up and most of the other
1747 * operations complete too quickly to simulate that under normal conditions.
1748 */
1749 public static Intent createSleepIntent(Context context, long millis) {
1750 return new Intent(context, ContactSaveService.class).setAction(ACTION_SLEEP)
1751 .putExtra(EXTRA_SLEEP_DURATION, millis);
1752 }
1753
1754 private void sleepForDebugging(Intent intent) {
1755 long duration = intent.getLongExtra(EXTRA_SLEEP_DURATION, 1000);
1756 if (Log.isLoggable(TAG, Log.DEBUG)) {
1757 Log.d(TAG, "sleeping for " + duration + "ms");
1758 }
1759 try {
1760 Thread.sleep(duration);
1761 } catch (InterruptedException e) {
1762 e.printStackTrace();
1763 }
1764 if (Log.isLoggable(TAG, Log.DEBUG)) {
1765 Log.d(TAG, "finished sleeping");
Marcus Hagerott819214d2016-09-29 14:58:27 -07001766 }
1767 }
1768
Gary Mai7efa9942016-05-12 11:26:49 -07001769 /**
James Laskeyf62b4882016-10-21 11:36:40 -07001770 * Shows a toast on the UI thread by formatting messageId using args.
1771 * @param messageId id of message string
1772 * @param args args to format string
1773 */
1774 private void showToast(final int messageId, final Object... args) {
1775 final String message = getResources().getString(messageId, args);
1776 mMainHandler.post(new Runnable() {
1777 @Override
1778 public void run() {
1779 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1780 }
1781 });
1782 }
1783
1784
1785 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001786 * Shows a toast on the UI thread.
1787 */
1788 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001789 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001790
1791 @Override
1792 public void run() {
1793 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1794 }
1795 });
1796 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001797
1798 private void deliverCallback(final Intent callbackIntent) {
1799 mMainHandler.post(new Runnable() {
1800
1801 @Override
1802 public void run() {
1803 deliverCallbackOnUiThread(callbackIntent);
1804 }
1805 });
1806 }
1807
1808 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1809 // TODO: this assumes that if there are multiple instances of the same
1810 // activity registered, the last one registered is the one waiting for
1811 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001812 for (Listener listener : sListeners) {
1813 if (callbackIntent.getComponent().equals(
1814 ((Activity) listener).getIntent().getComponent())) {
1815 listener.onServiceCompleted(callbackIntent);
1816 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001817 }
1818 }
1819 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001820
1821 public interface GroupsDao {
1822 Uri create(String title, AccountWithDataSet account);
1823 int delete(Uri groupUri);
1824 Bundle captureDeletionUndoData(Uri groupUri);
1825 Uri undoDeletion(Bundle undoData);
1826 }
1827
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001828 public static class GroupsDaoImpl implements GroupsDao {
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001829 public static final String KEY_GROUP_DATA = "groupData";
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001830 public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
1831
1832 private static final String TAG = "GroupsDao";
1833 private final Context context;
1834 private final ContentResolver contentResolver;
1835
1836 public GroupsDaoImpl(Context context) {
1837 this(context, context.getContentResolver());
1838 }
1839
1840 public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
1841 this.context = context;
1842 this.contentResolver = contentResolver;
1843 }
1844
1845 public Bundle captureDeletionUndoData(Uri groupUri) {
1846 final long groupId = ContentUris.parseId(groupUri);
1847 final Bundle result = new Bundle();
1848
1849 final Cursor cursor = contentResolver.query(groupUri,
1850 new String[]{
1851 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
1852 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
1853 Groups.SHOULD_SYNC
1854 },
1855 Groups.DELETED + "=?", new String[] { "0" }, null);
1856 try {
1857 if (cursor.moveToFirst()) {
1858 final ContentValues groupValues = new ContentValues();
1859 DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
1860 result.putParcelable(KEY_GROUP_DATA, groupValues);
1861 } else {
1862 // Group doesn't exist.
1863 return result;
1864 }
1865 } finally {
1866 cursor.close();
1867 }
1868
1869 final Cursor membersCursor = contentResolver.query(
1870 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
1871 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1872 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
1873 final long[] memberIds = new long[membersCursor.getCount()];
1874 int i = 0;
1875 while (membersCursor.moveToNext()) {
1876 memberIds[i++] = membersCursor.getLong(0);
1877 }
1878 result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
1879 return result;
1880 }
1881
1882 public Uri undoDeletion(Bundle deletedGroupData) {
1883 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
1884 if (groupData == null) {
1885 return null;
1886 }
1887 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
1888 final long groupId = ContentUris.parseId(groupUri);
1889
1890 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
1891 if (memberIds == null) {
1892 return groupUri;
1893 }
1894 final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
1895 for (int i = 0; i < memberIds.length; i++) {
1896 memberInsertions[i] = new ContentValues();
1897 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
1898 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1899 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
1900 }
1901 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
1902 if (inserted != memberIds.length) {
1903 Log.e(TAG, "Could not recover some members for group deletion undo");
1904 }
1905
1906 return groupUri;
1907 }
1908
1909 public Uri create(String title, AccountWithDataSet account) {
1910 final ContentValues values = new ContentValues();
1911 values.put(Groups.TITLE, title);
1912 values.put(Groups.ACCOUNT_NAME, account.name);
1913 values.put(Groups.ACCOUNT_TYPE, account.type);
1914 values.put(Groups.DATA_SET, account.dataSet);
1915 return contentResolver.insert(Groups.CONTENT_URI, values);
1916 }
1917
1918 public int delete(Uri groupUri) {
1919 return contentResolver.delete(groupUri, null, null);
1920 }
1921 }
Marcus Hagerott7333c372016-11-07 09:40:20 -08001922
1923 /**
1924 * Keeps track of which operations have been requested but have not yet finished for this
1925 * service.
1926 */
1927 public static class State {
1928 private final CopyOnWriteArrayList<Intent> mPending;
1929
1930 public State() {
1931 mPending = new CopyOnWriteArrayList<>();
1932 }
1933
1934 public State(Collection<Intent> pendingActions) {
1935 mPending = new CopyOnWriteArrayList<>(pendingActions);
1936 }
1937
1938 public boolean isIdle() {
1939 return mPending.isEmpty();
1940 }
1941
1942 public Intent getCurrentIntent() {
1943 return mPending.isEmpty() ? null : mPending.get(0);
1944 }
1945
1946 /**
1947 * Returns the first intent requested that has the specified action or null if no intent
1948 * with that action has been requested.
1949 */
1950 public Intent getNextIntentWithAction(String action) {
1951 for (Intent intent : mPending) {
1952 if (action.equals(intent.getAction())) {
1953 return intent;
1954 }
1955 }
1956 return null;
1957 }
1958
1959 public boolean isActionPending(String action) {
1960 return getNextIntentWithAction(action) != null;
1961 }
1962
1963 private void onFinish(Intent intent) {
1964 if (mPending.isEmpty()) {
1965 return;
1966 }
1967 final String action = mPending.get(0).getAction();
1968 if (action.equals(intent.getAction())) {
1969 mPending.remove(0);
1970 }
1971 }
1972
1973 private void onStart(Intent intent) {
1974 if (intent.getAction() == null) {
1975 return;
1976 }
1977 mPending.add(intent);
1978 }
1979 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001980}