blob: 4e2d1789626406a5a2ae3dccd90e817dddad3552 [file] [log] [blame]
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080017package com.android.contacts;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070018
Gary Mai0a49afa2016-12-05 15:53:58 -080019import static android.Manifest.permission.WRITE_CONTACTS;
20
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -080021import android.app.Activity;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070022import android.app.IntentService;
23import android.content.ContentProviderOperation;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080024import android.content.ContentProviderOperation.Builder;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070025import android.content.ContentProviderResult;
26import android.content.ContentResolver;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080027import android.content.ContentUris;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070028import android.content.ContentValues;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080029import android.content.Context;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070030import android.content.Intent;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080031import android.content.OperationApplicationException;
32import android.database.Cursor;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070033import android.database.DatabaseUtils;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070034import android.net.Uri;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080035import android.os.Bundle;
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -080036import android.os.Handler;
37import android.os.Looper;
Dmitri Plotnikova0114142011-02-15 13:53:21 -080038import android.os.Parcelable;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080039import android.os.RemoteException;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070040import android.provider.ContactsContract;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080041import android.provider.ContactsContract.AggregationExceptions;
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -080042import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Brian Attwell548f5c62015-01-27 17:46:46 -080043import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -080044import android.provider.ContactsContract.Contacts;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070045import android.provider.ContactsContract.Data;
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -080046import android.provider.ContactsContract.Groups;
Isaac Katzenelsonead19c52011-07-29 18:24:53 -070047import android.provider.ContactsContract.Profile;
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070048import android.provider.ContactsContract.RawContacts;
Dave Santoroc90f95e2011-09-07 17:47:15 -070049import android.provider.ContactsContract.RawContactsEntity;
Marcus Hagerottbea2b852016-08-11 14:55:52 -070050import android.support.v4.content.LocalBroadcastManager;
Gary Mai7efa9942016-05-12 11:26:49 -070051import android.support.v4.os.ResultReceiver;
James Laskeyf62b4882016-10-21 11:36:40 -070052import android.text.TextUtils;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070053import android.util.Log;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -080054import android.widget.Toast;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070055
Gary Mai363af602016-09-28 10:01:23 -070056import com.android.contacts.activities.ContactEditorActivity;
Gary Mai69c182a2016-12-05 13:07:03 -080057import com.android.contacts.compat.CompatUtils;
Gary Mai0a49afa2016-12-05 15:53:58 -080058import com.android.contacts.compat.PinnedPositionsCompat;
Gary Mai69c182a2016-12-05 13:07:03 -080059import com.android.contacts.database.ContactUpdateUtils;
60import com.android.contacts.database.SimContactDao;
61import com.android.contacts.model.AccountTypeManager;
62import com.android.contacts.model.CPOWrapper;
63import com.android.contacts.model.RawContactDelta;
64import com.android.contacts.model.RawContactDeltaList;
65import com.android.contacts.model.RawContactModifier;
66import com.android.contacts.model.account.AccountWithDataSet;
67import com.android.contacts.preference.ContactsPreferences;
68import com.android.contacts.util.ContactDisplayUtils;
Yorke Lee637a38e2013-09-14 08:36:33 -070069import com.android.contacts.util.ContactPhotoUtils;
Gary Mai0a49afa2016-12-05 15:53:58 -080070import com.android.contacts.util.PermissionsUtil;
Walter Jang3a0b4832016-10-12 11:02:54 -070071import com.android.contactsbind.FeedbackHelper;
Gary Mai0a49afa2016-12-05 15:53:58 -080072
Chiao Chenge0b2f1e2012-06-12 13:07:56 -070073import com.google.common.collect.Lists;
74import com.google.common.collect.Sets;
75
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080076import java.util.ArrayList;
Marcus Hagerott7333c372016-11-07 09:40:20 -080077import java.util.Collection;
Daniel Lehmannc42ea4e2012-02-16 21:22:37 -080078import java.util.HashSet;
79import java.util.List;
80import java.util.concurrent.CopyOnWriteArrayList;
Daniel Lehmann173ffe12010-06-14 18:19:10 -070081
Dmitri Plotnikov18ffaa22010-12-03 14:28:00 -080082/**
83 * A service responsible for saving changes to the content provider.
84 */
Daniel Lehmann173ffe12010-06-14 18:19:10 -070085public class ContactSaveService extends IntentService {
86 private static final String TAG = "ContactSaveService";
87
Katherine Kuana007e442011-07-07 09:25:34 -070088 /** Set to true in order to view logs on content provider operations */
89 private static final boolean DEBUG = false;
90
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070091 public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
92
93 public static final String EXTRA_ACCOUNT_NAME = "accountName";
94 public static final String EXTRA_ACCOUNT_TYPE = "accountType";
Dave Santoro2b3f3c52011-07-26 17:35:42 -070095 public static final String EXTRA_DATA_SET = "dataSet";
Marcus Hagerott819214d2016-09-29 14:58:27 -070096 public static final String EXTRA_ACCOUNT = "account";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -070097 public static final String EXTRA_CONTENT_VALUES = "contentValues";
98 public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
Gary Mai7efa9942016-05-12 11:26:49 -070099 public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
100 public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700101
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800102 public static final String ACTION_SAVE_CONTACT = "saveContact";
103 public static final String EXTRA_CONTACT_STATE = "state";
104 public static final String EXTRA_SAVE_MODE = "saveMode";
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700105 public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
Dave Santoro36d24d72011-09-25 17:08:10 -0700106 public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
Josh Garguse692e012012-01-18 14:53:11 -0800107 public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700108
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800109 public static final String ACTION_CREATE_GROUP = "createGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800110 public static final String ACTION_RENAME_GROUP = "renameGroup";
111 public static final String ACTION_DELETE_GROUP = "deleteGroup";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700112 public static final String ACTION_UPDATE_GROUP = "updateGroup";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800113 public static final String EXTRA_GROUP_ID = "groupId";
114 public static final String EXTRA_GROUP_LABEL = "groupLabel";
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700115 public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
116 public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800117
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800118 public static final String ACTION_SET_STARRED = "setStarred";
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800119 public static final String ACTION_DELETE_CONTACT = "delete";
Brian Attwelld2962a32015-03-02 14:48:50 -0800120 public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800121 public static final String EXTRA_CONTACT_URI = "contactUri";
Brian Attwelld2962a32015-03-02 14:48:50 -0800122 public static final String EXTRA_CONTACT_IDS = "contactIds";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800123 public static final String EXTRA_STARRED_FLAG = "starred";
Marcus Hagerott3bb85142016-07-29 10:46:36 -0700124 public static final String EXTRA_DISPLAY_NAME = "extraDisplayName";
James Laskeye5a140a2016-10-18 15:43:42 -0700125 public static final String EXTRA_DISPLAY_NAME_ARRAY = "extraDisplayNameArray";
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800126
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800127 public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
128 public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
129 public static final String EXTRA_DATA_ID = "dataId";
130
Gary Mai7efa9942016-05-12 11:26:49 -0700131 public static final String ACTION_SPLIT_CONTACT = "splitContact";
Gary Maib9065dd2016-11-08 10:49:00 -0800132 public static final String EXTRA_HARD_SPLIT = "extraHardSplit";
Gary Mai7efa9942016-05-12 11:26:49 -0700133
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800134 public static final String ACTION_JOIN_CONTACTS = "joinContacts";
Brian Attwelld3946ca2015-03-03 11:13:49 -0800135 public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800136 public static final String EXTRA_CONTACT_ID1 = "contactId1";
137 public static final String EXTRA_CONTACT_ID2 = "contactId2";
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800138
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700139 public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
140 public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
141
142 public static final String ACTION_SET_RINGTONE = "setRingtone";
143 public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
144
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700145 public static final String ACTION_UNDO = "undo";
146 public static final String EXTRA_UNDO_ACTION = "undoAction";
147 public static final String EXTRA_UNDO_DATA = "undoData";
148
Marcus Hagerott7333c372016-11-07 09:40:20 -0800149 // For debugging and testing what happens when requests are queued up.
150 public static final String ACTION_SLEEP = "sleep";
151 public static final String EXTRA_SLEEP_DURATION = "sleepDuration";
Marcus Hagerott819214d2016-09-29 14:58:27 -0700152
153 public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
Gary Maib9065dd2016-11-08 10:49:00 -0800154 public static final String BROADCAST_LINK_COMPLETE = "linkComplete";
155 public static final String BROADCAST_UNLINK_COMPLETE = "unlinkComplete";
Marcus Hagerott7333c372016-11-07 09:40:20 -0800156
157 public static final String BROADCAST_SERVICE_STATE_CHANGED = "serviceStateChanged";
Marcus Hagerott819214d2016-09-29 14:58:27 -0700158
159 public static final String EXTRA_RESULT_CODE = "resultCode";
160 public static final String EXTRA_RESULT_COUNT = "count";
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700161
Gary Mai7efa9942016-05-12 11:26:49 -0700162 public static final int CP2_ERROR = 0;
163 public static final int CONTACTS_LINKED = 1;
164 public static final int CONTACTS_SPLIT = 2;
Gary Mai31d572e2016-06-03 14:04:32 -0700165 public static final int BAD_ARGUMENTS = 3;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700166 public static final int RESULT_UNKNOWN = 0;
167 public static final int RESULT_SUCCESS = 1;
168 public static final int RESULT_FAILURE = 2;
Gary Mai7efa9942016-05-12 11:26:49 -0700169
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700170 private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
171 Data.MIMETYPE,
172 Data.IS_PRIMARY,
173 Data.DATA1,
174 Data.DATA2,
175 Data.DATA3,
176 Data.DATA4,
177 Data.DATA5,
178 Data.DATA6,
179 Data.DATA7,
180 Data.DATA8,
181 Data.DATA9,
182 Data.DATA10,
183 Data.DATA11,
184 Data.DATA12,
185 Data.DATA13,
186 Data.DATA14,
187 Data.DATA15
188 );
189
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800190 private static final int PERSIST_TRIES = 3;
191
Walter Jang0653de32015-07-24 12:12:40 -0700192 private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
193
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800194 public interface Listener {
195 public void onServiceCompleted(Intent callbackIntent);
196 }
197
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100198 private static final CopyOnWriteArrayList<Listener> sListeners =
199 new CopyOnWriteArrayList<Listener>();
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800200
Marcus Hagerott7333c372016-11-07 09:40:20 -0800201 // Holds the current state of the service
202 private static final State sState = new State();
203
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800204 private Handler mMainHandler;
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700205 private GroupsDao mGroupsDao;
Marcus Hagerott819214d2016-09-29 14:58:27 -0700206 private SimContactDao mSimContactDao;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800207
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700208 public ContactSaveService() {
209 super(TAG);
210 setIntentRedelivery(true);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800211 mMainHandler = new Handler(Looper.getMainLooper());
212 }
213
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700214 @Override
215 public void onCreate() {
216 super.onCreate();
217 mGroupsDao = new GroupsDaoImpl(this);
Marcus Hagerott66e8b222016-10-23 15:41:55 -0700218 mSimContactDao = SimContactDao.create(this);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700219 }
220
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800221 public static void registerListener(Listener listener) {
222 if (!(listener instanceof Activity)) {
223 throw new ClassCastException("Only activities can be registered to"
224 + " receive callback from " + ContactSaveService.class.getName());
225 }
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100226 sListeners.add(0, listener);
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800227 }
228
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700229 public static boolean canUndo(Intent resultIntent) {
230 return resultIntent.hasExtra(EXTRA_UNDO_DATA);
231 }
232
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800233 public static void unregisterListener(Listener listener) {
Hugo Hudsona831c0b2011-08-13 11:50:15 +0100234 sListeners.remove(listener);
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700235 }
236
Marcus Hagerott7333c372016-11-07 09:40:20 -0800237 public static State getState() {
238 return sState;
239 }
240
241 private void notifyStateChanged() {
242 LocalBroadcastManager.getInstance(this)
243 .sendBroadcast(new Intent(BROADCAST_SERVICE_STATE_CHANGED));
244 }
245
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800246 /**
247 * Returns true if the ContactSaveService was started successfully and false if an exception
248 * was thrown and a Toast error message was displayed.
249 */
250 public static boolean startService(Context context, Intent intent, int saveMode) {
251 try {
252 context.startService(intent);
253 } catch (Exception exception) {
254 final int resId;
255 switch (saveMode) {
Gary Mai363af602016-09-28 10:01:23 -0700256 case ContactEditorActivity.ContactEditor.SaveMode.SPLIT:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800257 resId = R.string.contactUnlinkErrorToast;
258 break;
Gary Mai363af602016-09-28 10:01:23 -0700259 case ContactEditorActivity.ContactEditor.SaveMode.RELOAD:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800260 resId = R.string.contactJoinErrorToast;
261 break;
Gary Mai363af602016-09-28 10:01:23 -0700262 case ContactEditorActivity.ContactEditor.SaveMode.CLOSE:
Wenyi Wangdd7d4562015-12-08 13:33:43 -0800263 resId = R.string.contactSavedErrorToast;
264 break;
265 default:
266 resId = R.string.contactGenericErrorToast;
267 }
268 Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
269 return false;
270 }
271 return true;
272 }
273
274 /**
275 * Utility method that starts service and handles exception.
276 */
277 public static void startService(Context context, Intent intent) {
278 try {
279 context.startService(intent);
280 } catch (Exception exception) {
281 Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
282 }
283 }
284
Daniel Lehmann173ffe12010-06-14 18:19:10 -0700285 @Override
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800286 public Object getSystemService(String name) {
287 Object service = super.getSystemService(name);
288 if (service != null) {
289 return service;
290 }
291
292 return getApplicationContext().getSystemService(name);
293 }
294
Marcus Hagerott7333c372016-11-07 09:40:20 -0800295 // Parent classes Javadoc says not to override this method but we're doing it just to update
296 // our state which should be OK since we're still doing the work in onHandleIntent
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800297 @Override
Marcus Hagerott7333c372016-11-07 09:40:20 -0800298 public int onStartCommand(Intent intent, int flags, int startId) {
299 sState.onStart(intent);
300 notifyStateChanged();
301 return super.onStartCommand(intent, flags, startId);
302 }
303
304 @Override
305 protected void onHandleIntent(final Intent intent) {
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800306 if (intent == null) {
Wenyi Wang57a0e982017-03-24 16:02:44 -0700307 if (Log.isLoggable(TAG, Log.DEBUG)) {
308 Log.d(TAG, "onHandleIntent: could not handle null intent");
309 }
Jay Shrauner3a7cc762014-12-01 17:16:33 -0800310 return;
311 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700312 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
313 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
314 // TODO: add more specific error string such as "Turn on Contacts
315 // permission to update your contacts"
316 showToast(R.string.contactSavedErrorToast);
317 return;
318 }
319
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700320 // Call an appropriate method. If we're sure it affects how incoming phone calls are
321 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700322 String action = intent.getAction();
323 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
324 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800325 } else if (ACTION_SAVE_CONTACT.equals(action)) {
326 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800327 } else if (ACTION_CREATE_GROUP.equals(action)) {
328 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800329 } else if (ACTION_RENAME_GROUP.equals(action)) {
330 renameGroup(intent);
331 } else if (ACTION_DELETE_GROUP.equals(action)) {
332 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700333 } else if (ACTION_UPDATE_GROUP.equals(action)) {
334 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800335 } else if (ACTION_SET_STARRED.equals(action)) {
336 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800337 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
338 setSuperPrimary(intent);
339 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
340 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800341 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
342 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800343 } else if (ACTION_DELETE_CONTACT.equals(action)) {
344 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700345 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
346 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800347 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
348 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800349 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
350 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700351 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
352 setSendToVoicemail(intent);
353 } else if (ACTION_SET_RINGTONE.equals(action)) {
354 setRingtone(intent);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700355 } else if (ACTION_UNDO.equals(action)) {
356 undo(intent);
Marcus Hagerott7333c372016-11-07 09:40:20 -0800357 } else if (ACTION_SLEEP.equals(action)) {
358 sleepForDebugging(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700359 }
Marcus Hagerott7333c372016-11-07 09:40:20 -0800360
361 sState.onFinish(intent);
362 notifyStateChanged();
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700363 }
364
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800365 /**
366 * Creates an intent that can be sent to this service to create a new raw contact
367 * using data presented as a set of ContentValues.
368 */
369 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700370 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700371 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800372 Intent serviceIntent = new Intent(
373 context, ContactSaveService.class);
374 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
375 if (account != null) {
376 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
377 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700378 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800379 }
380 serviceIntent.putParcelableArrayListExtra(
381 ContactSaveService.EXTRA_CONTENT_VALUES, values);
382
383 // Callback intent will be invoked by the service once the new contact is
384 // created. The service will put the URI of the new contact as "data" on
385 // the callback intent.
386 Intent callbackIntent = new Intent(context, callbackActivity);
387 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800388 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
389 return serviceIntent;
390 }
391
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700392 private void createRawContact(Intent intent) {
393 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
394 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700395 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700396 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
397 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
398
399 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
400 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
401 .withValue(RawContacts.ACCOUNT_NAME, accountName)
402 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700403 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700404 .build());
405
406 int size = valueList.size();
407 for (int i = 0; i < size; i++) {
408 ContentValues values = valueList.get(i);
409 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
410 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
411 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
412 .withValues(values)
413 .build());
414 }
415
416 ContentResolver resolver = getContentResolver();
417 ContentProviderResult[] results;
418 try {
419 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
420 } catch (Exception e) {
421 throw new RuntimeException("Failed to store new contact", e);
422 }
423
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700424 Uri rawContactUri = results[0].uri;
425 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
426
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800427 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700428 }
429
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700430 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800431 * Creates an intent that can be sent to this service to create a new raw contact
432 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800433 * This variant is more convenient to use when there is only one photo that can
434 * possibly be updated, as in the Contact Details screen.
435 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
436 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800437 */
Maurice Chu851222a2012-06-21 11:43:08 -0700438 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700439 String saveModeExtraKey, int saveMode, boolean isProfile,
440 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700441 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800442 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700443 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800444 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700445 callbackActivity, callbackAction, bundle,
446 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800447 }
448
449 /**
450 * Creates an intent that can be sent to this service to create a new raw contact
451 * using data presented as a set of ContentValues.
452 * This variant is used when multiple contacts' photos may be updated, as in the
453 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700454 *
Josh Garguse692e012012-01-18 14:53:11 -0800455 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700456 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
457 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800458 */
Maurice Chu851222a2012-06-21 11:43:08 -0700459 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700460 String saveModeExtraKey, int saveMode, boolean isProfile,
461 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700462 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000463 Intent serviceIntent = new Intent(
464 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800465 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
466 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700467 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800468 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
469
Josh Garguse692e012012-01-18 14:53:11 -0800470 if (updatedPhotos != null) {
471 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
472 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800473
Josh Garguse5d3f892012-04-11 11:56:15 -0700474 if (callbackActivity != null) {
475 // Callback intent will be invoked by the service once the contact is
476 // saved. The service will put the URI of the new contact as "data" on
477 // the callback intent.
478 Intent callbackIntent = new Intent(context, callbackActivity);
479 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700480 if (joinContactIdExtraKey != null && joinContactId != null) {
481 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
482 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700483 callbackIntent.setAction(callbackAction);
484 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
485 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800486 return serviceIntent;
487 }
488
489 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700490 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700491 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800492 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800493
Jay Shrauner08099782015-03-25 14:17:11 -0700494 if (state == null) {
495 Log.e(TAG, "Invalid arguments for saveContact request");
496 return;
497 }
498
benny.lin3a4e7a22014-01-08 10:58:08 +0800499 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800500 // Trim any empty fields, and RawContacts, before persisting
501 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700502 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800503
504 Uri lookupUri = null;
505
506 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700507
Josh Garguse692e012012-01-18 14:53:11 -0800508 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800509
Josh Gargusef15c8e2012-01-30 16:42:02 -0800510 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
511 long insertedRawContactId = -1;
512
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800513 // Attempt to persist changes
514 int tries = 0;
515 while (tries++ < PERSIST_TRIES) {
516 try {
517 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800518 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
519
520 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
521
522 for (CPOWrapper cpoWrapper : diffWrapper) {
523 diff.add(cpoWrapper.getOperation());
524 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700525
Katherine Kuana007e442011-07-07 09:25:34 -0700526 if (DEBUG) {
527 Log.v(TAG, "Content Provider Operations:");
528 for (ContentProviderOperation operation : diff) {
529 Log.v(TAG, operation.toString());
530 }
531 }
532
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700533 int numberProcessed = 0;
534 boolean batchFailed = false;
535 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
536 while (numberProcessed < diff.size()) {
537 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
538 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700539 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700540 batchFailed = true;
541 break;
542 } else {
543 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700544 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800545 }
546
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700547 if (batchFailed) {
548 // Retry save
549 continue;
550 }
551
Wenyi Wang67addcc2015-11-23 10:07:48 -0800552 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800553 if (rawContactId == -1) {
554 throw new IllegalStateException("Could not determine RawContact ID after save");
555 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800556 // We don't have to check to see if the value is still -1. If we reach here,
557 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800558 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700559 if (isProfile) {
560 // Since the profile supports local raw contacts, which may have been completely
561 // removed if all information was removed, we need to do a special query to
562 // get the lookup URI for the profile contact (if it still exists).
563 Cursor c = resolver.query(Profile.CONTENT_URI,
564 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
565 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800566 if (c == null) {
567 continue;
568 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700569 try {
Erik162b7e32011-09-20 15:23:55 -0700570 if (c.moveToFirst()) {
571 final long contactId = c.getLong(0);
572 final String lookupKey = c.getString(1);
573 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
574 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700575 } finally {
576 c.close();
577 }
578 } else {
579 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
580 rawContactId);
581 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
582 }
Wenyi Wang57a0e982017-03-24 16:02:44 -0700583 if (lookupUri != null && Log.isLoggable(TAG, Log.VERBOSE)) {
Jay Shraunere320c0b2015-03-05 12:45:18 -0800584 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
585 }
Josh Garguse692e012012-01-18 14:53:11 -0800586
587 // We can change this back to false later, if we fail to save the contact photo.
588 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800589 break;
590
591 } catch (RemoteException e) {
592 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700593 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800594 break;
595
Jay Shrauner57fca182014-01-17 14:20:50 -0800596 } catch (IllegalArgumentException e) {
597 // This is thrown by applyBatch on malformed requests
Walter Jang3a0b4832016-10-12 11:02:54 -0700598 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Jay Shrauner57fca182014-01-17 14:20:50 -0800599 showToast(R.string.contactSavedErrorToast);
600 break;
601
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800602 } catch (OperationApplicationException e) {
603 // Version consistency failed, re-parent change and try again
604 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
605 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
606 boolean first = true;
607 final int count = state.size();
608 for (int i = 0; i < count; i++) {
609 Long rawContactId = state.getRawContactId(i);
610 if (rawContactId != null && rawContactId != -1) {
611 if (!first) {
612 sb.append(',');
613 }
614 sb.append(rawContactId);
615 first = false;
616 }
617 }
618 sb.append(")");
619
620 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800621 throw new IllegalStateException(
622 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800623 }
624
Maurice Chu851222a2012-06-21 11:43:08 -0700625 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700626 isProfile
627 ? RawContactsEntity.PROFILE_CONTENT_URI
628 : RawContactsEntity.CONTENT_URI,
629 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700630 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700631
632 // Update the new state to use profile URIs if appropriate.
633 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700634 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700635 delta.setProfileQueryUri();
636 }
637 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800638 }
639 }
640
Josh Garguse692e012012-01-18 14:53:11 -0800641 // Now save any updated photos. We do this at the end to ensure that
642 // the ContactProvider already knows about newly-created contacts.
643 if (updatedPhotos != null) {
644 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700645 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800646 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800647
648 // If the raw-contact ID is negative, we are saving a new raw-contact;
649 // replace the bogus ID with the new one that we actually saved the contact at.
650 if (rawContactId < 0) {
651 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800652 }
653
Jay Shrauner511561d2015-04-02 10:35:33 -0700654 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700655 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700656 succeeded = false;
657 }
Josh Garguse692e012012-01-18 14:53:11 -0800658 }
659 }
660
Josh Garguse5d3f892012-04-11 11:56:15 -0700661 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
662 if (callbackIntent != null) {
663 if (succeeded) {
664 // Mark the intent to indicate that the save was successful (even if the lookup URI
665 // is now null). For local contacts or the local profile, it's possible that the
666 // save triggered removal of the contact, so no lookup URI would exist..
667 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
668 }
669 callbackIntent.setData(lookupUri);
670 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800671 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800672 }
673
Josh Garguse692e012012-01-18 14:53:11 -0800674 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700675 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
676 * subsets, adds the returned array to "results".
677 *
678 * @return the size of the array, if not null; -1 when the array is null.
679 */
680 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
681 ContentProviderResult[] results, ContentResolver resolver)
682 throws RemoteException, OperationApplicationException {
683 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
684 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
685 subset.addAll(diff.subList(offset, offset + subsetCount));
686 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
687 .AUTHORITY, subset);
688 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
689 return -1;
690 }
691 for (ContentProviderResult c : subsetResult) {
692 results[offset++] = c;
693 }
694 return subsetResult.length;
695 }
696
697 /**
Josh Garguse692e012012-01-18 14:53:11 -0800698 * Save updated photo for the specified raw-contact.
699 * @return true for success, false for failure
700 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800701 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800702 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800703 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
704 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
705
benny.lin3a4e7a22014-01-08 10:58:08 +0800706 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800707 }
708
Josh Gargusef15c8e2012-01-30 16:42:02 -0800709 /**
710 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
711 */
Maurice Chu851222a2012-06-21 11:43:08 -0700712 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800713 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800714 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800715 long existingRawContactId = state.findRawContactId();
716 if (existingRawContactId != -1) {
717 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800718 }
719
Wenyi Wang67addcc2015-11-23 10:07:48 -0800720 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800721 }
722
723 /**
724 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
725 */
726 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800727 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800728 if (results == null) {
729 return -1;
730 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800731 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800732 final int numResults = results.length;
733 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800734 final CPOWrapper cpoWrapper = diffWrapper.get(i);
735 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
736 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
737 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800738 return ContentUris.parseId(results[i].uri);
739 }
740 }
741 return -1;
742 }
743
744 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700745 * Creates an intent that can be sent to this service to create a new group as
746 * well as add new members at the same time.
747 *
748 * @param context of the application
749 * @param account in which the group should be created
750 * @param label is the name of the group (cannot be null)
751 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
752 * should be added to the group
753 * @param callbackActivity is the activity to send the callback intent to
754 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700755 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700756 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700757 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700758 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800759 Intent serviceIntent = new Intent(context, ContactSaveService.class);
760 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
761 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
762 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700763 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800764 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700765 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700766
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800767 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700768 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800769 Intent callbackIntent = new Intent(context, callbackActivity);
770 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700771 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800772
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700773 return serviceIntent;
774 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800775
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800776 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700777 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
778 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
779 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
780 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700781 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800782
Katherine Kuan717e3432011-07-13 17:03:24 -0700783 // Create the new group
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700784 final Uri groupUri = mGroupsDao.create(label,
785 new AccountWithDataSet(accountName, accountType, dataSet));
786 final ContentResolver resolver = getContentResolver();
Katherine Kuan717e3432011-07-13 17:03:24 -0700787
788 // If there's no URI, then the insertion failed. Abort early because group members can't be
789 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800790 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700791 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800792 return;
793 }
794
Katherine Kuan717e3432011-07-13 17:03:24 -0700795 // Add new group members
796 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
797
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700798 ContentValues values = new ContentValues();
Katherine Kuan717e3432011-07-13 17:03:24 -0700799 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
Walter Jang8bac28b2016-08-30 10:34:55 -0700800 // with the way other intent extras that are passed to the
Gary Mai363af602016-09-28 10:01:23 -0700801 // {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800802 values.clear();
803 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
804 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
805
806 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700807 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700808 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800809 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800810 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800811 }
812
813 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800814 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800815 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700816 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700817 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800818 Intent serviceIntent = new Intent(context, ContactSaveService.class);
819 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
820 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
821 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700822
823 // Callback intent will be invoked by the service once the group is renamed.
824 Intent callbackIntent = new Intent(context, callbackActivity);
825 callbackIntent.setAction(callbackAction);
826 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
827
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800828 return serviceIntent;
829 }
830
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800831 private void renameGroup(Intent intent) {
832 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
833 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
834
835 if (groupId == -1) {
836 Log.e(TAG, "Invalid arguments for renameGroup request");
837 return;
838 }
839
840 ContentValues values = new ContentValues();
841 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700842 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
843 getContentResolver().update(groupUri, values, null, null);
844
845 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
846 callbackIntent.setData(groupUri);
847 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800848 }
849
850 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800851 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800852 */
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700853 public static Intent createGroupDeletionIntent(Context context, long groupId) {
854 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800855 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800856 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700857
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800858 return serviceIntent;
859 }
860
861 private void deleteGroup(Intent intent) {
862 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
863 if (groupId == -1) {
864 Log.e(TAG, "Invalid arguments for deleteGroup request");
865 return;
866 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700867 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800868
Marcus Hagerott819214d2016-09-29 14:58:27 -0700869 final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700870 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
871 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
872 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
Walter Jang72f99882016-05-26 09:01:31 -0700873
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700874 mGroupsDao.delete(groupUri);
875
876 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
877 }
878
879 public static Intent createUndoIntent(Context context, Intent resultIntent) {
880 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
881 serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
882 serviceIntent.putExtras(resultIntent);
883 return serviceIntent;
884 }
885
886 private void undo(Intent intent) {
887 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
888 if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
889 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
Walter Jang72f99882016-05-26 09:01:31 -0700890 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800891 }
892
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700893
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800894 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700895 * Creates an intent that can be sent to this service to rename a group as
896 * well as add and remove members from the group.
897 *
898 * @param context of the application
899 * @param groupId of the group that should be modified
900 * @param newLabel is the updated name of the group (can be null if the name
901 * should not be updated)
902 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
903 * should be added to the group
904 * @param rawContactsToRemove is an array of raw contact IDs for contacts
905 * that should be removed from the group
906 * @param callbackActivity is the activity to send the callback intent to
907 * @param callbackAction is the intent action for the callback intent
908 */
909 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
910 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700911 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700912 Intent serviceIntent = new Intent(context, ContactSaveService.class);
913 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
914 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
915 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
916 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
917 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
918 rawContactsToRemove);
919
920 // Callback intent will be invoked by the service once the group is updated
921 Intent callbackIntent = new Intent(context, callbackActivity);
922 callbackIntent.setAction(callbackAction);
923 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
924
925 return serviceIntent;
926 }
927
928 private void updateGroup(Intent intent) {
929 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
930 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
931 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
932 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
933
934 if (groupId == -1) {
935 Log.e(TAG, "Invalid arguments for updateGroup request");
936 return;
937 }
938
939 final ContentResolver resolver = getContentResolver();
940 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
941
942 // Update group name if necessary
943 if (label != null) {
944 ContentValues values = new ContentValues();
945 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700946 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700947 }
948
Katherine Kuan717e3432011-07-13 17:03:24 -0700949 // Add and remove members if necessary
950 addMembersToGroup(resolver, rawContactsToAdd, groupId);
951 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
952
953 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
954 callbackIntent.setData(groupUri);
955 deliverCallback(callbackIntent);
956 }
957
Walter Jang3a0b4832016-10-12 11:02:54 -0700958 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700959 long groupId) {
960 if (rawContactsToAdd == null) {
961 return;
962 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700963 for (long rawContactId : rawContactsToAdd) {
964 try {
965 final ArrayList<ContentProviderOperation> rawContactOperations =
966 new ArrayList<ContentProviderOperation>();
967
968 // Build an assert operation to ensure the contact is not already in the group
969 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
970 .newAssertQuery(Data.CONTENT_URI);
971 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
972 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
973 new String[] { String.valueOf(rawContactId),
974 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
975 assertBuilder.withExpectedCount(0);
976 rawContactOperations.add(assertBuilder.build());
977
978 // Build an insert operation to add the contact to the group
979 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
980 .newInsert(Data.CONTENT_URI);
981 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
982 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
983 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
984 rawContactOperations.add(insertBuilder.build());
985
986 if (DEBUG) {
987 for (ContentProviderOperation operation : rawContactOperations) {
988 Log.v(TAG, operation.toString());
989 }
990 }
991
992 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700993 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800994 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700995 }
996 } catch (RemoteException e) {
997 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700998 FeedbackHelper.sendFeedback(this, TAG,
999 "Problem persisting user edits for raw contact ID " +
1000 String.valueOf(rawContactId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001001 } catch (OperationApplicationException e) {
1002 // The assert could have failed because the contact is already in the group,
1003 // just continue to the next contact
Walter Jang3a0b4832016-10-12 11:02:54 -07001004 FeedbackHelper.sendFeedback(this, TAG,
1005 "Assert failed in adding raw contact ID " +
1006 String.valueOf(rawContactId) + ". Already exists in group " +
1007 String.valueOf(groupId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001008 }
1009 }
Katherine Kuan717e3432011-07-13 17:03:24 -07001010 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001011
Daniel Lehmann18958a22012-02-28 17:45:25 -08001012 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -07001013 long groupId) {
1014 if (rawContactsToRemove == null) {
1015 return;
1016 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001017 for (long rawContactId : rawContactsToRemove) {
1018 // Apply the delete operation on the data row for the given raw contact's
1019 // membership in the given group. If no contact matches the provided selection, then
1020 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -08001021 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001022 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1023 new String[] { String.valueOf(rawContactId),
1024 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
1025 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001026 }
1027
1028 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001029 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001030 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001031 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
1032 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1033 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
1034 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1035 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
1036
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001037 return serviceIntent;
1038 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001039
1040 private void setStarred(Intent intent) {
1041 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1042 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
1043 if (contactUri == null) {
1044 Log.e(TAG, "Invalid arguments for setStarred request");
1045 return;
1046 }
1047
1048 final ContentValues values = new ContentValues(1);
1049 values.put(Contacts.STARRED, value);
1050 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -07001051
1052 // Undemote the contact if necessary
1053 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
1054 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -08001055 if (c == null) {
1056 return;
1057 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001058 try {
1059 if (c.moveToFirst()) {
1060 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -07001061
1062 // Don't bother undemoting if this contact is the user's profile.
1063 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -08001064 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -07001065 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001066 }
1067 } finally {
1068 c.close();
1069 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001070 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001071
1072 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -07001073 * Creates an intent that can be sent to this service to set the redirect to voicemail.
1074 */
1075 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
1076 boolean value) {
1077 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1078 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1079 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1080 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1081
1082 return serviceIntent;
1083 }
1084
1085 private void setSendToVoicemail(Intent intent) {
1086 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1087 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1088 if (contactUri == null) {
1089 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1090 return;
1091 }
1092
1093 final ContentValues values = new ContentValues(1);
1094 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1095 getContentResolver().update(contactUri, values, null, null);
1096 }
1097
1098 /**
1099 * Creates an intent that can be sent to this service to save the contact's ringtone.
1100 */
1101 public static Intent createSetRingtone(Context context, Uri contactUri,
1102 String value) {
1103 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1104 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1105 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1106 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1107
1108 return serviceIntent;
1109 }
1110
1111 private void setRingtone(Intent intent) {
1112 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1113 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1114 if (contactUri == null) {
1115 Log.e(TAG, "Invalid arguments for setRingtone");
1116 return;
1117 }
1118 ContentValues values = new ContentValues(1);
1119 values.put(Contacts.CUSTOM_RINGTONE, value);
1120 getContentResolver().update(contactUri, values, null, null);
1121 }
1122
1123 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001124 * Creates an intent that sets the selected data item as super primary (default)
1125 */
1126 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1127 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1128 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1129 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1130 return serviceIntent;
1131 }
1132
1133 private void setSuperPrimary(Intent intent) {
1134 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1135 if (dataId == -1) {
1136 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1137 return;
1138 }
1139
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001140 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001141 }
1142
1143 /**
1144 * Creates an intent that clears the primary flag of all data items that belong to the same
1145 * raw_contact as the given data item. Will only clear, if the data item was primary before
1146 * this call
1147 */
1148 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1149 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1150 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1151 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1152 return serviceIntent;
1153 }
1154
1155 private void clearPrimary(Intent intent) {
1156 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1157 if (dataId == -1) {
1158 Log.e(TAG, "Invalid arguments for clearPrimary request");
1159 return;
1160 }
1161
1162 // Update the primary values in the data record.
1163 ContentValues values = new ContentValues(1);
1164 values.put(Data.IS_SUPER_PRIMARY, 0);
1165 values.put(Data.IS_PRIMARY, 0);
1166
1167 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1168 values, null, null);
1169 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001170
1171 /**
1172 * Creates an intent that can be sent to this service to delete a contact.
1173 */
1174 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1175 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1176 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1177 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1178 return serviceIntent;
1179 }
1180
Brian Attwelld2962a32015-03-02 14:48:50 -08001181 /**
1182 * Creates an intent that can be sent to this service to delete multiple contacts.
1183 */
1184 public static Intent createDeleteMultipleContactsIntent(Context context,
James Laskeye5a140a2016-10-18 15:43:42 -07001185 long[] contactIds, final String[] names) {
Brian Attwelld2962a32015-03-02 14:48:50 -08001186 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1187 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1188 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
James Laskeye5a140a2016-10-18 15:43:42 -07001189 serviceIntent.putExtra(ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY, names);
Brian Attwelld2962a32015-03-02 14:48:50 -08001190 return serviceIntent;
1191 }
1192
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001193 private void deleteContact(Intent intent) {
1194 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1195 if (contactUri == null) {
1196 Log.e(TAG, "Invalid arguments for deleteContact request");
1197 return;
1198 }
1199
1200 getContentResolver().delete(contactUri, null, null);
1201 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001202
Brian Attwelld2962a32015-03-02 14:48:50 -08001203 private void deleteMultipleContacts(Intent intent) {
1204 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1205 if (contactIds == null) {
1206 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1207 return;
1208 }
1209 for (long contactId : contactIds) {
1210 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1211 getContentResolver().delete(contactUri, null, null);
1212 }
James Laskeye5a140a2016-10-18 15:43:42 -07001213 final String[] names = intent.getStringArrayExtra(
1214 ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY);
1215 final String deleteToastMessage;
James Laskey56019ad2016-11-14 16:38:35 -08001216 if (contactIds.length != names.length || names.length == 0) {
James Laskeye5a140a2016-10-18 15:43:42 -07001217 deleteToastMessage = getResources().getQuantityString(
1218 R.plurals.contacts_deleted_toast, contactIds.length);
1219 } else if (names.length == 1) {
1220 deleteToastMessage = getResources().getString(
1221 R.string.contacts_deleted_one_named_toast, names);
1222 } else if (names.length == 2) {
1223 deleteToastMessage = getResources().getString(
1224 R.string.contacts_deleted_two_named_toast, names);
1225 } else {
1226 deleteToastMessage = getResources().getString(
1227 R.string.contacts_deleted_many_named_toast, names);
1228 }
James Laskey56019ad2016-11-14 16:38:35 -08001229
Wenyi Wang687d2182015-10-28 17:03:18 -07001230 mMainHandler.post(new Runnable() {
1231 @Override
1232 public void run() {
1233 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1234 .show();
1235 }
1236 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001237 }
1238
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001239 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001240 * 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 -08001241 * pieces. This will set the raw contact ids to {@link AggregationExceptions#TYPE_AUTOMATIC} so
Gary Mai53fe0d22016-07-26 17:23:53 -07001242 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001243 */
1244 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1245 ResultReceiver receiver) {
1246 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1247 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1248 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1249 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1250 return serviceIntent;
1251 }
1252
Gary Maib9065dd2016-11-08 10:49:00 -08001253 /**
1254 * Creates an intent that can be sent to this service to split a contact into it's constituent
1255 * pieces. This will explicitly set the raw contact ids to
1256 * {@link AggregationExceptions#TYPE_KEEP_SEPARATE}.
1257 */
1258 public static Intent createHardSplitContactIntent(Context context, long[][] rawContactIds) {
1259 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1260 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1261 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1262 serviceIntent.putExtra(ContactSaveService.EXTRA_HARD_SPLIT, true);
1263 return serviceIntent;
1264 }
1265
Gary Mai7efa9942016-05-12 11:26:49 -07001266 private void splitContact(Intent intent) {
1267 final long rawContactIds[][] = (long[][]) intent
1268 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001269 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Maib9065dd2016-11-08 10:49:00 -08001270 final boolean hardSplit = intent.getBooleanExtra(EXTRA_HARD_SPLIT, false);
Gary Mai7efa9942016-05-12 11:26:49 -07001271 if (rawContactIds == null) {
1272 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001273 if (receiver != null) {
1274 receiver.send(BAD_ARGUMENTS, new Bundle());
1275 }
Gary Mai7efa9942016-05-12 11:26:49 -07001276 return;
1277 }
1278 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1279 final ContentResolver resolver = getContentResolver();
1280 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001281 for (int i = 0; i < rawContactIds.length; i++) {
1282 for (int j = 0; j < rawContactIds.length; j++) {
1283 if (i != j) {
Gary Maib9065dd2016-11-08 10:49:00 -08001284 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j],
1285 hardSplit)) {
Gary Mai7efa9942016-05-12 11:26:49 -07001286 if (receiver != null) {
1287 receiver.send(CP2_ERROR, new Bundle());
1288 return;
1289 }
1290 }
1291 }
1292 }
1293 }
1294 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1295 if (receiver != null) {
1296 receiver.send(CP2_ERROR, new Bundle());
1297 }
1298 return;
1299 }
Gary Maib9065dd2016-11-08 10:49:00 -08001300 LocalBroadcastManager.getInstance(this)
1301 .sendBroadcast(new Intent(BROADCAST_UNLINK_COMPLETE));
Gary Mai7efa9942016-05-12 11:26:49 -07001302 if (receiver != null) {
1303 receiver.send(CONTACTS_SPLIT, new Bundle());
1304 } else {
1305 showToast(R.string.contactUnlinkedToast);
1306 }
1307 }
1308
1309 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001310 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001311 * and {@param rawContactIds2} to {@param operations}.
1312 * @return false if an error occurred, true otherwise.
1313 */
1314 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
Gary Maib9065dd2016-11-08 10:49:00 -08001315 long[] rawContactIds1, long[] rawContactIds2, boolean hardSplit) {
Gary Mai7efa9942016-05-12 11:26:49 -07001316 if (rawContactIds1 == null || rawContactIds2 == null) {
1317 Log.e(TAG, "Invalid arguments for splitContact request");
1318 return false;
1319 }
1320 // For each pair of raw contacts, insert an aggregation exception
1321 final ContentResolver resolver = getContentResolver();
1322 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1323 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1324 for (int i = 0; i < rawContactIds1.length; i++) {
1325 for (int j = 0; j < rawContactIds2.length; j++) {
Gary Maib9065dd2016-11-08 10:49:00 -08001326 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j], hardSplit);
Gary Mai7efa9942016-05-12 11:26:49 -07001327 // Before we get to 500 we need to flush the operations list
1328 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1329 if (!applyOperations(resolver, operations)) {
1330 return false;
1331 }
1332 operations.clear();
1333 }
1334 }
1335 }
1336 return true;
1337 }
1338
1339 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001340 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001341 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001342 */
1343 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001344 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001345 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1346 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1347 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1348 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001349
1350 // Callback intent will be invoked by the service once the contacts are joined.
1351 Intent callbackIntent = new Intent(context, callbackActivity);
1352 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001353 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1354
1355 return serviceIntent;
1356 }
1357
Brian Attwelld3946ca2015-03-03 11:13:49 -08001358 /**
1359 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1360 * No special attention is paid to where the resulting contact's name is taken from.
1361 */
Gary Mai7efa9942016-05-12 11:26:49 -07001362 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1363 ResultReceiver receiver) {
1364 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001365 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1366 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001367 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001368 return serviceIntent;
1369 }
1370
Gary Mai7efa9942016-05-12 11:26:49 -07001371 /**
1372 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1373 * No special attention is paid to where the resulting contact's name is taken from.
1374 */
1375 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1376 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1377 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001378
1379 private interface JoinContactQuery {
1380 String[] PROJECTION = {
1381 RawContacts._ID,
1382 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001383 RawContacts.DISPLAY_NAME_SOURCE,
1384 };
1385
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001386 int _ID = 0;
1387 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001388 int DISPLAY_NAME_SOURCE = 2;
1389 }
1390
1391 private interface ContactEntityQuery {
1392 String[] PROJECTION = {
1393 Contacts.Entity.DATA_ID,
1394 Contacts.Entity.CONTACT_ID,
1395 Contacts.Entity.IS_SUPER_PRIMARY,
1396 };
1397 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1398 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1399 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1400 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1401
1402 int DATA_ID = 0;
1403 int CONTACT_ID = 1;
1404 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001405 }
1406
Brian Attwelld3946ca2015-03-03 11:13:49 -08001407 private void joinSeveralContacts(Intent intent) {
1408 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001409
Gary Mai7efa9942016-05-12 11:26:49 -07001410 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001411
Brian Attwelld3946ca2015-03-03 11:13:49 -08001412 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001413 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1414 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001415 if (rawContactIds == null) {
1416 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001417 if (receiver != null) {
1418 receiver.send(BAD_ARGUMENTS, new Bundle());
1419 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001420 return;
1421 }
1422
Brian Attwelld3946ca2015-03-03 11:13:49 -08001423 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001424 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001425 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1426 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1427 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001428 for (int i = 0; i < rawContactIds.length; i++) {
1429 for (int j = 0; j < rawContactIds.length; j++) {
1430 if (i != j) {
1431 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1432 }
Walter Jang0653de32015-07-24 12:12:40 -07001433 // Before we get to 500 we need to flush the operations list
1434 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001435 if (!applyOperations(resolver, operations)) {
1436 if (receiver != null) {
1437 receiver.send(CP2_ERROR, new Bundle());
1438 }
Walter Jang0653de32015-07-24 12:12:40 -07001439 return;
1440 }
1441 operations.clear();
1442 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001443 }
1444 }
Gary Mai7efa9942016-05-12 11:26:49 -07001445 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1446 if (receiver != null) {
1447 receiver.send(CP2_ERROR, new Bundle());
1448 }
Walter Jang0653de32015-07-24 12:12:40 -07001449 return;
1450 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001451
John Shaoa3c507a2016-09-13 14:26:17 -07001452
1453 final String name = queryNameOfLinkedContacts(contactIds);
1454 if (name != null) {
1455 if (receiver != null) {
1456 final Bundle result = new Bundle();
1457 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1458 result.putString(EXTRA_DISPLAY_NAME, name);
1459 receiver.send(CONTACTS_LINKED, result);
1460 } else {
James Laskeyf62b4882016-10-21 11:36:40 -07001461 if (TextUtils.isEmpty(name)) {
1462 showToast(R.string.contactsJoinedMessage);
1463 } else {
1464 showToast(R.string.contactsJoinedNamedMessage, name);
1465 }
John Shaoa3c507a2016-09-13 14:26:17 -07001466 }
Gary Maib9065dd2016-11-08 10:49:00 -08001467 LocalBroadcastManager.getInstance(this)
1468 .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
Gary Mai7efa9942016-05-12 11:26:49 -07001469 } else {
John Shaoa3c507a2016-09-13 14:26:17 -07001470 if (receiver != null) {
1471 receiver.send(CP2_ERROR, new Bundle());
1472 }
1473 showToast(R.string.contactJoinErrorToast);
Gary Mai7efa9942016-05-12 11:26:49 -07001474 }
Walter Jang0653de32015-07-24 12:12:40 -07001475 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001476
John Shaoa3c507a2016-09-13 14:26:17 -07001477 /** Get the display name of the top-level contact after the contacts have been linked. */
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001478 private String queryNameOfLinkedContacts(long[] contactIds) {
1479 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1480 final String[] whereArgs = new String[contactIds.length];
1481 for (int i = 0; i < contactIds.length; i++) {
1482 whereArgs[i] = String.valueOf(contactIds[i]);
1483 whereBuilder.append("?,");
1484 }
1485 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1486 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
James Laskeyf62b4882016-10-21 11:36:40 -07001487 new String[]{Contacts._ID, Contacts.DISPLAY_NAME,
1488 Contacts.DISPLAY_NAME_ALTERNATIVE},
John Shaoa3c507a2016-09-13 14:26:17 -07001489 whereBuilder.toString(), whereArgs, null);
1490
1491 String name = null;
James Laskeyf62b4882016-10-21 11:36:40 -07001492 String nameAlt = null;
John Shaoa3c507a2016-09-13 14:26:17 -07001493 long contactId = 0;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001494 try {
1495 if (cursor.moveToFirst()) {
John Shaoa3c507a2016-09-13 14:26:17 -07001496 contactId = cursor.getLong(0);
1497 name = cursor.getString(1);
James Laskeyf62b4882016-10-21 11:36:40 -07001498 nameAlt = cursor.getString(2);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001499 }
John Shaoa3c507a2016-09-13 14:26:17 -07001500 while(cursor.moveToNext()) {
1501 if (cursor.getLong(0) != contactId) {
1502 return null;
1503 }
1504 }
James Laskeyf62b4882016-10-21 11:36:40 -07001505
1506 final String formattedName = ContactDisplayUtils.getPreferredDisplayName(name, nameAlt,
1507 new ContactsPreferences(getApplicationContext()));
1508 return formattedName == null ? "" : formattedName;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001509 } finally {
John Shaoa3c507a2016-09-13 14:26:17 -07001510 if (cursor != null) {
1511 cursor.close();
1512 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001513 }
1514 }
1515
Walter Jang0653de32015-07-24 12:12:40 -07001516 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001517 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001518 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001519 try {
John Shaoa3c507a2016-09-13 14:26:17 -07001520 final ContentProviderResult[] result =
1521 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1522 for (int i = 0; i < result.length; ++i) {
1523 // if no rows were modified in the operation then we count it as fail.
1524 if (result[i].count < 0) {
1525 throw new OperationApplicationException();
1526 }
1527 }
Walter Jang0653de32015-07-24 12:12:40 -07001528 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001529 } catch (RemoteException | OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001530 FeedbackHelper.sendFeedback(this, TAG,
1531 "Failed to apply aggregation exception batch", e);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001532 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001533 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001534 }
1535 }
1536
Brian Attwelld3946ca2015-03-03 11:13:49 -08001537 private void joinContacts(Intent intent) {
1538 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1539 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001540
1541 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001542 // in the join UIs.
1543 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1544 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001545 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001546 return;
1547 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001548
Brian Attwell548f5c62015-01-27 17:46:46 -08001549 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001550
1551 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001552 for (int i = 0; i < rawContactIds.length; i++) {
1553 for (int j = 0; j < rawContactIds.length; j++) {
1554 if (i != j) {
1555 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1556 }
1557 }
1558 }
1559
Brian Attwelld3946ca2015-03-03 11:13:49 -08001560 final ContentResolver resolver = getContentResolver();
1561
Brian Attwell548f5c62015-01-27 17:46:46 -08001562 // Use the name for contactId1 as the name for the newly aggregated contact.
1563 final Uri contactId1Uri = ContentUris.withAppendedId(
1564 Contacts.CONTENT_URI, contactId1);
1565 final Uri entityUri = Uri.withAppendedPath(
1566 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1567 Cursor c = resolver.query(entityUri,
1568 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1569 if (c == null) {
1570 Log.e(TAG, "Unable to open Contacts DB cursor");
1571 showToast(R.string.contactSavedErrorToast);
1572 return;
1573 }
1574 long dataIdToAddSuperPrimary = -1;
1575 try {
1576 if (c.moveToFirst()) {
1577 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1578 }
1579 } finally {
1580 c.close();
1581 }
1582
1583 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1584 // display name does not change as a result of the join.
1585 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001586 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001587 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1588 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1589 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001590 operations.add(builder.build());
1591 }
1592
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001593 // Apply all aggregation exceptions as one batch
John Shaoa3c507a2016-09-13 14:26:17 -07001594 final boolean success = applyOperations(resolver, operations);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001595
John Shaoa3c507a2016-09-13 14:26:17 -07001596 final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2});
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001597 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
John Shaoa3c507a2016-09-13 14:26:17 -07001598 if (success && name != null) {
James Laskeyf62b4882016-10-21 11:36:40 -07001599 if (TextUtils.isEmpty(name)) {
1600 showToast(R.string.contactsJoinedMessage);
1601 } else {
1602 showToast(R.string.contactsJoinedNamedMessage, name);
1603 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001604 Uri uri = RawContacts.getContactLookupUri(resolver,
1605 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1606 callbackIntent.setData(uri);
Gary Maib9065dd2016-11-08 10:49:00 -08001607 LocalBroadcastManager.getInstance(this)
1608 .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001609 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001610 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001611 }
1612
Gary Mai7efa9942016-05-12 11:26:49 -07001613 /**
1614 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1615 * array of the return value holds an array of raw contact ids for one contactId.
1616 * @param contactIds
1617 * @return
1618 */
1619 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1620 final long[][] rawContactIds = new long[contactIds.length][];
1621 for (int i = 0; i < contactIds.length; i++) {
1622 rawContactIds[i] = getRawContactIds(contactIds[i]);
1623 }
1624 return rawContactIds;
1625 }
1626
1627 /**
1628 * Gets the raw contact ids associated with {@param contactId}.
1629 * @param contactId
1630 * @return Array of raw contact ids.
1631 */
1632 private long[] getRawContactIds(long contactId) {
1633 final ContentResolver resolver = getContentResolver();
1634 long rawContactIds[];
1635
1636 final StringBuilder queryBuilder = new StringBuilder();
1637 queryBuilder.append(RawContacts.CONTACT_ID)
1638 .append("=")
1639 .append(String.valueOf(contactId));
1640
1641 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1642 JoinContactQuery.PROJECTION,
1643 queryBuilder.toString(),
1644 null, null);
1645 if (c == null) {
1646 Log.e(TAG, "Unable to open Contacts DB cursor");
1647 return null;
1648 }
1649 try {
1650 rawContactIds = new long[c.getCount()];
1651 for (int i = 0; i < rawContactIds.length; i++) {
1652 c.moveToPosition(i);
1653 final long rawContactId = c.getLong(JoinContactQuery._ID);
1654 rawContactIds[i] = rawContactId;
1655 }
1656 } finally {
1657 c.close();
1658 }
1659 return rawContactIds;
1660 }
1661
Brian Attwelld3946ca2015-03-03 11:13:49 -08001662 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1663 if (contactIds == null) {
1664 return null;
1665 }
1666
Brian Attwell548f5c62015-01-27 17:46:46 -08001667 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001668
1669 final StringBuilder queryBuilder = new StringBuilder();
1670 final String stringContactIds[] = new String[contactIds.length];
1671 for (int i = 0; i < contactIds.length; i++) {
1672 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1673 stringContactIds[i] = String.valueOf(contactIds[i]);
1674 if (contactIds[i] == -1) {
1675 return null;
1676 }
1677 if (i == contactIds.length -1) {
1678 break;
1679 }
1680 queryBuilder.append(" OR ");
1681 }
1682
Brian Attwell548f5c62015-01-27 17:46:46 -08001683 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1684 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001685 queryBuilder.toString(),
1686 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001687 if (c == null) {
1688 Log.e(TAG, "Unable to open Contacts DB cursor");
1689 showToast(R.string.contactSavedErrorToast);
1690 return null;
1691 }
Gary Mai7efa9942016-05-12 11:26:49 -07001692 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001693 try {
1694 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001695 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001696 return null;
1697 }
1698 rawContactIds = new long[c.getCount()];
1699 for (int i = 0; i < rawContactIds.length; i++) {
1700 c.moveToPosition(i);
1701 long rawContactId = c.getLong(JoinContactQuery._ID);
1702 rawContactIds[i] = rawContactId;
1703 }
1704 } finally {
1705 c.close();
1706 }
1707 return rawContactIds;
1708 }
1709
Brian Attwelld3946ca2015-03-03 11:13:49 -08001710 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1711 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1712 }
1713
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001714 /**
1715 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1716 */
1717 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1718 long rawContactId1, long rawContactId2) {
1719 Builder builder =
1720 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1721 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1722 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1723 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1724 operations.add(builder.build());
1725 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001726
1727 /**
Gary Maib9065dd2016-11-08 10:49:00 -08001728 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} or a
1729 * {@link AggregationExceptions#TYPE_KEEP_SEPARATE} ContentProviderOperation if a hard split is
1730 * requested.
Gary Mai7efa9942016-05-12 11:26:49 -07001731 */
1732 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
Gary Maib9065dd2016-11-08 10:49:00 -08001733 long rawContactId1, long rawContactId2, boolean hardSplit) {
Gary Mai7efa9942016-05-12 11:26:49 -07001734 final Builder builder =
1735 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Maib9065dd2016-11-08 10:49:00 -08001736 builder.withValue(AggregationExceptions.TYPE,
1737 hardSplit
1738 ? AggregationExceptions.TYPE_KEEP_SEPARATE
1739 : AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001740 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1741 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1742 operations.add(builder.build());
1743 }
1744
Marcus Hagerott7333c372016-11-07 09:40:20 -08001745 /**
Marcus Hagerott7333c372016-11-07 09:40:20 -08001746 * Returns an intent that can start this service and cause it to sleep for the specified time.
1747 *
1748 * This exists purely for debugging and manual testing. Since this service uses a single thread
1749 * it is useful to have a way to test behavior when work is queued up and most of the other
1750 * operations complete too quickly to simulate that under normal conditions.
1751 */
1752 public static Intent createSleepIntent(Context context, long millis) {
1753 return new Intent(context, ContactSaveService.class).setAction(ACTION_SLEEP)
1754 .putExtra(EXTRA_SLEEP_DURATION, millis);
1755 }
1756
1757 private void sleepForDebugging(Intent intent) {
1758 long duration = intent.getLongExtra(EXTRA_SLEEP_DURATION, 1000);
1759 if (Log.isLoggable(TAG, Log.DEBUG)) {
1760 Log.d(TAG, "sleeping for " + duration + "ms");
1761 }
1762 try {
1763 Thread.sleep(duration);
1764 } catch (InterruptedException e) {
1765 e.printStackTrace();
1766 }
1767 if (Log.isLoggable(TAG, Log.DEBUG)) {
1768 Log.d(TAG, "finished sleeping");
Marcus Hagerott819214d2016-09-29 14:58:27 -07001769 }
1770 }
1771
Gary Mai7efa9942016-05-12 11:26:49 -07001772 /**
James Laskeyf62b4882016-10-21 11:36:40 -07001773 * Shows a toast on the UI thread by formatting messageId using args.
1774 * @param messageId id of message string
1775 * @param args args to format string
1776 */
1777 private void showToast(final int messageId, final Object... args) {
1778 final String message = getResources().getString(messageId, args);
1779 mMainHandler.post(new Runnable() {
1780 @Override
1781 public void run() {
1782 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1783 }
1784 });
1785 }
1786
1787
1788 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001789 * Shows a toast on the UI thread.
1790 */
1791 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001792 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001793
1794 @Override
1795 public void run() {
1796 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1797 }
1798 });
1799 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001800
1801 private void deliverCallback(final Intent callbackIntent) {
1802 mMainHandler.post(new Runnable() {
1803
1804 @Override
1805 public void run() {
1806 deliverCallbackOnUiThread(callbackIntent);
1807 }
1808 });
1809 }
1810
1811 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1812 // TODO: this assumes that if there are multiple instances of the same
1813 // activity registered, the last one registered is the one waiting for
1814 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001815 for (Listener listener : sListeners) {
1816 if (callbackIntent.getComponent().equals(
1817 ((Activity) listener).getIntent().getComponent())) {
1818 listener.onServiceCompleted(callbackIntent);
1819 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001820 }
1821 }
1822 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001823
1824 public interface GroupsDao {
1825 Uri create(String title, AccountWithDataSet account);
1826 int delete(Uri groupUri);
1827 Bundle captureDeletionUndoData(Uri groupUri);
1828 Uri undoDeletion(Bundle undoData);
1829 }
1830
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001831 public static class GroupsDaoImpl implements GroupsDao {
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001832 public static final String KEY_GROUP_DATA = "groupData";
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001833 public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
1834
1835 private static final String TAG = "GroupsDao";
1836 private final Context context;
1837 private final ContentResolver contentResolver;
1838
1839 public GroupsDaoImpl(Context context) {
1840 this(context, context.getContentResolver());
1841 }
1842
1843 public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
1844 this.context = context;
1845 this.contentResolver = contentResolver;
1846 }
1847
1848 public Bundle captureDeletionUndoData(Uri groupUri) {
1849 final long groupId = ContentUris.parseId(groupUri);
1850 final Bundle result = new Bundle();
1851
1852 final Cursor cursor = contentResolver.query(groupUri,
1853 new String[]{
1854 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
1855 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
1856 Groups.SHOULD_SYNC
1857 },
1858 Groups.DELETED + "=?", new String[] { "0" }, null);
1859 try {
1860 if (cursor.moveToFirst()) {
1861 final ContentValues groupValues = new ContentValues();
1862 DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
1863 result.putParcelable(KEY_GROUP_DATA, groupValues);
1864 } else {
1865 // Group doesn't exist.
1866 return result;
1867 }
1868 } finally {
1869 cursor.close();
1870 }
1871
1872 final Cursor membersCursor = contentResolver.query(
1873 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
1874 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1875 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
1876 final long[] memberIds = new long[membersCursor.getCount()];
1877 int i = 0;
1878 while (membersCursor.moveToNext()) {
1879 memberIds[i++] = membersCursor.getLong(0);
1880 }
1881 result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
1882 return result;
1883 }
1884
1885 public Uri undoDeletion(Bundle deletedGroupData) {
1886 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
1887 if (groupData == null) {
1888 return null;
1889 }
1890 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
1891 final long groupId = ContentUris.parseId(groupUri);
1892
1893 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
1894 if (memberIds == null) {
1895 return groupUri;
1896 }
1897 final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
1898 for (int i = 0; i < memberIds.length; i++) {
1899 memberInsertions[i] = new ContentValues();
1900 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
1901 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1902 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
1903 }
1904 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
1905 if (inserted != memberIds.length) {
1906 Log.e(TAG, "Could not recover some members for group deletion undo");
1907 }
1908
1909 return groupUri;
1910 }
1911
1912 public Uri create(String title, AccountWithDataSet account) {
1913 final ContentValues values = new ContentValues();
1914 values.put(Groups.TITLE, title);
1915 values.put(Groups.ACCOUNT_NAME, account.name);
1916 values.put(Groups.ACCOUNT_TYPE, account.type);
1917 values.put(Groups.DATA_SET, account.dataSet);
1918 return contentResolver.insert(Groups.CONTENT_URI, values);
1919 }
1920
1921 public int delete(Uri groupUri) {
1922 return contentResolver.delete(groupUri, null, null);
1923 }
1924 }
Marcus Hagerott7333c372016-11-07 09:40:20 -08001925
1926 /**
1927 * Keeps track of which operations have been requested but have not yet finished for this
1928 * service.
1929 */
1930 public static class State {
1931 private final CopyOnWriteArrayList<Intent> mPending;
1932
1933 public State() {
1934 mPending = new CopyOnWriteArrayList<>();
1935 }
1936
1937 public State(Collection<Intent> pendingActions) {
1938 mPending = new CopyOnWriteArrayList<>(pendingActions);
1939 }
1940
1941 public boolean isIdle() {
1942 return mPending.isEmpty();
1943 }
1944
1945 public Intent getCurrentIntent() {
1946 return mPending.isEmpty() ? null : mPending.get(0);
1947 }
1948
1949 /**
1950 * Returns the first intent requested that has the specified action or null if no intent
1951 * with that action has been requested.
1952 */
1953 public Intent getNextIntentWithAction(String action) {
1954 for (Intent intent : mPending) {
1955 if (action.equals(intent.getAction())) {
1956 return intent;
1957 }
1958 }
1959 return null;
1960 }
1961
1962 public boolean isActionPending(String action) {
1963 return getNextIntentWithAction(action) != null;
1964 }
1965
1966 private void onFinish(Intent intent) {
1967 if (mPending.isEmpty()) {
1968 return;
1969 }
1970 final String action = mPending.get(0).getAction();
1971 if (action.equals(intent.getAction())) {
1972 mPending.remove(0);
1973 }
1974 }
1975
1976 private void onStart(Intent intent) {
1977 if (intent.getAction() == null) {
1978 return;
1979 }
1980 mPending.add(intent);
1981 }
1982 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001983}