blob: e05f7c612e74f9b0f0b0376dfb7e63464fc2ad6c [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) {
307 Log.d(TAG, "onHandleIntent: could not handle null intent");
308 return;
309 }
Jay Shrauner615ed9c2015-07-29 11:27:56 -0700310 if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
311 Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
312 // TODO: add more specific error string such as "Turn on Contacts
313 // permission to update your contacts"
314 showToast(R.string.contactSavedErrorToast);
315 return;
316 }
317
Daisuke Miyakawa2f21c442012-03-22 19:12:31 -0700318 // Call an appropriate method. If we're sure it affects how incoming phone calls are
319 // handled, then notify the fact to in-call screen.
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700320 String action = intent.getAction();
321 if (ACTION_NEW_RAW_CONTACT.equals(action)) {
322 createRawContact(intent);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800323 } else if (ACTION_SAVE_CONTACT.equals(action)) {
324 saveContact(intent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800325 } else if (ACTION_CREATE_GROUP.equals(action)) {
326 createGroup(intent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800327 } else if (ACTION_RENAME_GROUP.equals(action)) {
328 renameGroup(intent);
329 } else if (ACTION_DELETE_GROUP.equals(action)) {
330 deleteGroup(intent);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700331 } else if (ACTION_UPDATE_GROUP.equals(action)) {
332 updateGroup(intent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800333 } else if (ACTION_SET_STARRED.equals(action)) {
334 setStarred(intent);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -0800335 } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
336 setSuperPrimary(intent);
337 } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
338 clearPrimary(intent);
Brian Attwelld2962a32015-03-02 14:48:50 -0800339 } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
340 deleteMultipleContacts(intent);
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -0800341 } else if (ACTION_DELETE_CONTACT.equals(action)) {
342 deleteContact(intent);
Gary Mai7efa9942016-05-12 11:26:49 -0700343 } else if (ACTION_SPLIT_CONTACT.equals(action)) {
344 splitContact(intent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -0800345 } else if (ACTION_JOIN_CONTACTS.equals(action)) {
346 joinContacts(intent);
Brian Attwelld3946ca2015-03-03 11:13:49 -0800347 } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
348 joinSeveralContacts(intent);
Isaac Katzenelson683b57e2011-07-20 17:06:11 -0700349 } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
350 setSendToVoicemail(intent);
351 } else if (ACTION_SET_RINGTONE.equals(action)) {
352 setRingtone(intent);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700353 } else if (ACTION_UNDO.equals(action)) {
354 undo(intent);
Marcus Hagerott7333c372016-11-07 09:40:20 -0800355 } else if (ACTION_SLEEP.equals(action)) {
356 sleepForDebugging(intent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700357 }
Marcus Hagerott7333c372016-11-07 09:40:20 -0800358
359 sState.onFinish(intent);
360 notifyStateChanged();
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700361 }
362
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800363 /**
364 * Creates an intent that can be sent to this service to create a new raw contact
365 * using data presented as a set of ContentValues.
366 */
367 public static Intent createNewRawContactIntent(Context context,
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700368 ArrayList<ContentValues> values, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700369 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800370 Intent serviceIntent = new Intent(
371 context, ContactSaveService.class);
372 serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
373 if (account != null) {
374 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
375 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700376 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800377 }
378 serviceIntent.putParcelableArrayListExtra(
379 ContactSaveService.EXTRA_CONTENT_VALUES, values);
380
381 // Callback intent will be invoked by the service once the new contact is
382 // created. The service will put the URI of the new contact as "data" on
383 // the callback intent.
384 Intent callbackIntent = new Intent(context, callbackActivity);
385 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800386 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
387 return serviceIntent;
388 }
389
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700390 private void createRawContact(Intent intent) {
391 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
392 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700393 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700394 List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
395 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
396
397 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
398 operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
399 .withValue(RawContacts.ACCOUNT_NAME, accountName)
400 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700401 .withValue(RawContacts.DATA_SET, dataSet)
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700402 .build());
403
404 int size = valueList.size();
405 for (int i = 0; i < size; i++) {
406 ContentValues values = valueList.get(i);
407 values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
408 operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
409 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
410 .withValues(values)
411 .build());
412 }
413
414 ContentResolver resolver = getContentResolver();
415 ContentProviderResult[] results;
416 try {
417 results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
418 } catch (Exception e) {
419 throw new RuntimeException("Failed to store new contact", e);
420 }
421
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700422 Uri rawContactUri = results[0].uri;
423 callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
424
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800425 deliverCallback(callbackIntent);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700426 }
427
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700428 /**
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800429 * Creates an intent that can be sent to this service to create a new raw contact
430 * using data presented as a set of ContentValues.
Josh Garguse692e012012-01-18 14:53:11 -0800431 * This variant is more convenient to use when there is only one photo that can
432 * possibly be updated, as in the Contact Details screen.
433 * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
434 * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800435 */
Maurice Chu851222a2012-06-21 11:43:08 -0700436 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700437 String saveModeExtraKey, int saveMode, boolean isProfile,
438 Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
Yorke Lee637a38e2013-09-14 08:36:33 -0700439 Uri updatedPhotoPath) {
Josh Garguse692e012012-01-18 14:53:11 -0800440 Bundle bundle = new Bundle();
Yorke Lee637a38e2013-09-14 08:36:33 -0700441 bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
Josh Garguse692e012012-01-18 14:53:11 -0800442 return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
Walter Jange3373dc2015-10-27 15:35:12 -0700443 callbackActivity, callbackAction, bundle,
444 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
Josh Garguse692e012012-01-18 14:53:11 -0800445 }
446
447 /**
448 * Creates an intent that can be sent to this service to create a new raw contact
449 * using data presented as a set of ContentValues.
450 * This variant is used when multiple contacts' photos may be updated, as in the
451 * Contact Editor.
Walter Jange3373dc2015-10-27 15:35:12 -0700452 *
Josh Garguse692e012012-01-18 14:53:11 -0800453 * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
Walter Jange3373dc2015-10-27 15:35:12 -0700454 * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
455 * @param joinContactId the raw contact ID to join to the contact after doing the save.
Josh Garguse692e012012-01-18 14:53:11 -0800456 */
Maurice Chu851222a2012-06-21 11:43:08 -0700457 public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
Josh Garguse5d3f892012-04-11 11:56:15 -0700458 String saveModeExtraKey, int saveMode, boolean isProfile,
459 Class<? extends Activity> callbackActivity, String callbackAction,
Walter Jange3373dc2015-10-27 15:35:12 -0700460 Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
Walter Jangf8c8ac32016-02-20 02:07:15 +0000461 Intent serviceIntent = new Intent(
462 context, ContactSaveService.class);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800463 serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
464 serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700465 serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
benny.lin3a4e7a22014-01-08 10:58:08 +0800466 serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
467
Josh Garguse692e012012-01-18 14:53:11 -0800468 if (updatedPhotos != null) {
469 serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
470 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800471
Josh Garguse5d3f892012-04-11 11:56:15 -0700472 if (callbackActivity != null) {
473 // Callback intent will be invoked by the service once the contact is
474 // saved. The service will put the URI of the new contact as "data" on
475 // the callback intent.
476 Intent callbackIntent = new Intent(context, callbackActivity);
477 callbackIntent.putExtra(saveModeExtraKey, saveMode);
Walter Jange3373dc2015-10-27 15:35:12 -0700478 if (joinContactIdExtraKey != null && joinContactId != null) {
479 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
480 }
Josh Garguse5d3f892012-04-11 11:56:15 -0700481 callbackIntent.setAction(callbackAction);
482 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
483 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800484 return serviceIntent;
485 }
486
487 private void saveContact(Intent intent) {
Maurice Chu851222a2012-06-21 11:43:08 -0700488 RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Isaac Katzenelsonead19c52011-07-29 18:24:53 -0700489 boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
Josh Garguse692e012012-01-18 14:53:11 -0800490 Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800491
Jay Shrauner08099782015-03-25 14:17:11 -0700492 if (state == null) {
493 Log.e(TAG, "Invalid arguments for saveContact request");
494 return;
495 }
496
benny.lin3a4e7a22014-01-08 10:58:08 +0800497 int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800498 // Trim any empty fields, and RawContacts, before persisting
499 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
Maurice Chu851222a2012-06-21 11:43:08 -0700500 RawContactModifier.trimEmpty(state, accountTypes);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800501
502 Uri lookupUri = null;
503
504 final ContentResolver resolver = getContentResolver();
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700505
Josh Garguse692e012012-01-18 14:53:11 -0800506 boolean succeeded = false;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800507
Josh Gargusef15c8e2012-01-30 16:42:02 -0800508 // Keep track of the id of a newly raw-contact (if any... there can be at most one).
509 long insertedRawContactId = -1;
510
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800511 // Attempt to persist changes
512 int tries = 0;
513 while (tries++ < PERSIST_TRIES) {
514 try {
515 // Build operations and try applying
Wenyi Wang67addcc2015-11-23 10:07:48 -0800516 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
517
518 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
519
520 for (CPOWrapper cpoWrapper : diffWrapper) {
521 diff.add(cpoWrapper.getOperation());
522 }
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700523
Katherine Kuana007e442011-07-07 09:25:34 -0700524 if (DEBUG) {
525 Log.v(TAG, "Content Provider Operations:");
526 for (ContentProviderOperation operation : diff) {
527 Log.v(TAG, operation.toString());
528 }
529 }
530
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700531 int numberProcessed = 0;
532 boolean batchFailed = false;
533 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
534 while (numberProcessed < diff.size()) {
535 final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
536 if (subsetCount == -1) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700537 Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700538 batchFailed = true;
539 break;
540 } else {
541 numberProcessed += subsetCount;
Jay Shrauner511561d2015-04-02 10:35:33 -0700542 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800543 }
544
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700545 if (batchFailed) {
546 // Retry save
547 continue;
548 }
549
Wenyi Wang67addcc2015-11-23 10:07:48 -0800550 final long rawContactId = getRawContactId(state, diffWrapper, results);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800551 if (rawContactId == -1) {
552 throw new IllegalStateException("Could not determine RawContact ID after save");
553 }
Josh Gargusef15c8e2012-01-30 16:42:02 -0800554 // We don't have to check to see if the value is still -1. If we reach here,
555 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
Wenyi Wang67addcc2015-11-23 10:07:48 -0800556 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700557 if (isProfile) {
558 // Since the profile supports local raw contacts, which may have been completely
559 // removed if all information was removed, we need to do a special query to
560 // get the lookup URI for the profile contact (if it still exists).
561 Cursor c = resolver.query(Profile.CONTENT_URI,
562 new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
563 null, null, null);
Jay Shraunere320c0b2015-03-05 12:45:18 -0800564 if (c == null) {
565 continue;
566 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700567 try {
Erik162b7e32011-09-20 15:23:55 -0700568 if (c.moveToFirst()) {
569 final long contactId = c.getLong(0);
570 final String lookupKey = c.getString(1);
571 lookupUri = Contacts.getLookupUri(contactId, lookupKey);
572 }
Dave Santoro7c34c0a2011-09-12 14:58:20 -0700573 } finally {
574 c.close();
575 }
576 } else {
577 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
578 rawContactId);
579 lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
580 }
Jay Shraunere320c0b2015-03-05 12:45:18 -0800581 if (lookupUri != null) {
582 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
583 }
Josh Garguse692e012012-01-18 14:53:11 -0800584
585 // We can change this back to false later, if we fail to save the contact photo.
586 succeeded = true;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800587 break;
588
589 } catch (RemoteException e) {
590 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700591 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800592 break;
593
Jay Shrauner57fca182014-01-17 14:20:50 -0800594 } catch (IllegalArgumentException e) {
595 // This is thrown by applyBatch on malformed requests
Walter Jang3a0b4832016-10-12 11:02:54 -0700596 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
Jay Shrauner57fca182014-01-17 14:20:50 -0800597 showToast(R.string.contactSavedErrorToast);
598 break;
599
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800600 } catch (OperationApplicationException e) {
601 // Version consistency failed, re-parent change and try again
602 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
603 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
604 boolean first = true;
605 final int count = state.size();
606 for (int i = 0; i < count; i++) {
607 Long rawContactId = state.getRawContactId(i);
608 if (rawContactId != null && rawContactId != -1) {
609 if (!first) {
610 sb.append(',');
611 }
612 sb.append(rawContactId);
613 first = false;
614 }
615 }
616 sb.append(")");
617
618 if (first) {
Brian Attwell3b6c6282014-02-12 17:53:31 -0800619 throw new IllegalStateException(
620 "Version consistency failed for a new contact", e);
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800621 }
622
Maurice Chu851222a2012-06-21 11:43:08 -0700623 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
Dave Santoroc90f95e2011-09-07 17:47:15 -0700624 isProfile
625 ? RawContactsEntity.PROFILE_CONTENT_URI
626 : RawContactsEntity.CONTENT_URI,
627 resolver, sb.toString(), null, null);
Maurice Chu851222a2012-06-21 11:43:08 -0700628 state = RawContactDeltaList.mergeAfter(newState, state);
Dave Santoroc90f95e2011-09-07 17:47:15 -0700629
630 // Update the new state to use profile URIs if appropriate.
631 if (isProfile) {
Maurice Chu851222a2012-06-21 11:43:08 -0700632 for (RawContactDelta delta : state) {
Dave Santoroc90f95e2011-09-07 17:47:15 -0700633 delta.setProfileQueryUri();
634 }
635 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800636 }
637 }
638
Josh Garguse692e012012-01-18 14:53:11 -0800639 // Now save any updated photos. We do this at the end to ensure that
640 // the ContactProvider already knows about newly-created contacts.
641 if (updatedPhotos != null) {
642 for (String key : updatedPhotos.keySet()) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700643 Uri photoUri = updatedPhotos.getParcelable(key);
Josh Garguse692e012012-01-18 14:53:11 -0800644 long rawContactId = Long.parseLong(key);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800645
646 // If the raw-contact ID is negative, we are saving a new raw-contact;
647 // replace the bogus ID with the new one that we actually saved the contact at.
648 if (rawContactId < 0) {
649 rawContactId = insertedRawContactId;
Josh Gargusef15c8e2012-01-30 16:42:02 -0800650 }
651
Jay Shrauner511561d2015-04-02 10:35:33 -0700652 // If the save failed, insertedRawContactId will be -1
Jay Shraunerc4698fb2015-04-30 12:08:52 -0700653 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
Jay Shrauner511561d2015-04-02 10:35:33 -0700654 succeeded = false;
655 }
Josh Garguse692e012012-01-18 14:53:11 -0800656 }
657 }
658
Josh Garguse5d3f892012-04-11 11:56:15 -0700659 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
660 if (callbackIntent != null) {
661 if (succeeded) {
662 // Mark the intent to indicate that the save was successful (even if the lookup URI
663 // is now null). For local contacts or the local profile, it's possible that the
664 // save triggered removal of the contact, so no lookup URI would exist..
665 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
666 }
667 callbackIntent.setData(lookupUri);
668 deliverCallback(callbackIntent);
Josh Garguse692e012012-01-18 14:53:11 -0800669 }
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800670 }
671
Josh Garguse692e012012-01-18 14:53:11 -0800672 /**
Wenyi Wangccdf69c2015-10-30 11:59:58 -0700673 * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
674 * subsets, adds the returned array to "results".
675 *
676 * @return the size of the array, if not null; -1 when the array is null.
677 */
678 private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
679 ContentProviderResult[] results, ContentResolver resolver)
680 throws RemoteException, OperationApplicationException {
681 final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
682 final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
683 subset.addAll(diff.subList(offset, offset + subsetCount));
684 final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
685 .AUTHORITY, subset);
686 if (subsetResult == null || (offset + subsetResult.length) > results.length) {
687 return -1;
688 }
689 for (ContentProviderResult c : subsetResult) {
690 results[offset++] = c;
691 }
692 return subsetResult.length;
693 }
694
695 /**
Josh Garguse692e012012-01-18 14:53:11 -0800696 * Save updated photo for the specified raw-contact.
697 * @return true for success, false for failure
698 */
benny.lin3a4e7a22014-01-08 10:58:08 +0800699 private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800700 final Uri outputUri = Uri.withAppendedPath(
Josh Garguse692e012012-01-18 14:53:11 -0800701 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
702 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
703
benny.lin3a4e7a22014-01-08 10:58:08 +0800704 return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
Josh Garguse692e012012-01-18 14:53:11 -0800705 }
706
Josh Gargusef15c8e2012-01-30 16:42:02 -0800707 /**
708 * Find the ID of an existing or newly-inserted raw-contact. If none exists, return -1.
709 */
Maurice Chu851222a2012-06-21 11:43:08 -0700710 private long getRawContactId(RawContactDeltaList state,
Wenyi Wang67addcc2015-11-23 10:07:48 -0800711 final ArrayList<CPOWrapper> diffWrapper,
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800712 final ContentProviderResult[] results) {
Josh Gargusef15c8e2012-01-30 16:42:02 -0800713 long existingRawContactId = state.findRawContactId();
714 if (existingRawContactId != -1) {
715 return existingRawContactId;
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800716 }
717
Wenyi Wang67addcc2015-11-23 10:07:48 -0800718 return getInsertedRawContactId(diffWrapper, results);
Josh Gargusef15c8e2012-01-30 16:42:02 -0800719 }
720
721 /**
722 * Find the ID of a newly-inserted raw-contact. If none exists, return -1.
723 */
724 private long getInsertedRawContactId(
Wenyi Wang67addcc2015-11-23 10:07:48 -0800725 final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
Jay Shrauner568f4e72014-11-26 08:16:25 -0800726 if (results == null) {
727 return -1;
728 }
Wenyi Wang67addcc2015-11-23 10:07:48 -0800729 final int diffSize = diffWrapper.size();
Jay Shrauner3d7edc32014-11-10 09:58:23 -0800730 final int numResults = results.length;
731 for (int i = 0; i < diffSize && i < numResults; i++) {
Wenyi Wang67addcc2015-11-23 10:07:48 -0800732 final CPOWrapper cpoWrapper = diffWrapper.get(i);
733 final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
734 if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
735 RawContacts.CONTENT_URI.getEncodedPath())) {
Dmitri Plotnikova0114142011-02-15 13:53:21 -0800736 return ContentUris.parseId(results[i].uri);
737 }
738 }
739 return -1;
740 }
741
742 /**
Katherine Kuan717e3432011-07-13 17:03:24 -0700743 * Creates an intent that can be sent to this service to create a new group as
744 * well as add new members at the same time.
745 *
746 * @param context of the application
747 * @param account in which the group should be created
748 * @param label is the name of the group (cannot be null)
749 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
750 * should be added to the group
751 * @param callbackActivity is the activity to send the callback intent to
752 * @param callbackAction is the intent action for the callback intent
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700753 */
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700754 public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
Josh Garguse5d3f892012-04-11 11:56:15 -0700755 String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
Katherine Kuan717e3432011-07-13 17:03:24 -0700756 String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800757 Intent serviceIntent = new Intent(context, ContactSaveService.class);
758 serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
759 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
760 serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700761 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800762 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700763 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700764
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800765 // Callback intent will be invoked by the service once the new group is
Katherine Kuan717e3432011-07-13 17:03:24 -0700766 // created.
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800767 Intent callbackIntent = new Intent(context, callbackActivity);
768 callbackIntent.setAction(callbackAction);
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700769 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800770
Dmitri Plotnikovcaf0bc72010-09-03 15:16:21 -0700771 return serviceIntent;
772 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800773
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800774 private void createGroup(Intent intent) {
Dave Santoro2b3f3c52011-07-26 17:35:42 -0700775 String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
776 String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
777 String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
778 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
Katherine Kuan717e3432011-07-13 17:03:24 -0700779 final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800780
Katherine Kuan717e3432011-07-13 17:03:24 -0700781 // Create the new group
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700782 final Uri groupUri = mGroupsDao.create(label,
783 new AccountWithDataSet(accountName, accountType, dataSet));
784 final ContentResolver resolver = getContentResolver();
Katherine Kuan717e3432011-07-13 17:03:24 -0700785
786 // If there's no URI, then the insertion failed. Abort early because group members can't be
787 // added if the group doesn't exist
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800788 if (groupUri == null) {
Katherine Kuan717e3432011-07-13 17:03:24 -0700789 Log.e(TAG, "Couldn't create group with label " + label);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800790 return;
791 }
792
Katherine Kuan717e3432011-07-13 17:03:24 -0700793 // Add new group members
794 addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
795
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700796 ContentValues values = new ContentValues();
Katherine Kuan717e3432011-07-13 17:03:24 -0700797 // TODO: Move this into the contact editor where it belongs. This needs to be integrated
Walter Jang8bac28b2016-08-30 10:34:55 -0700798 // with the way other intent extras that are passed to the
Gary Mai363af602016-09-28 10:01:23 -0700799 // {@link ContactEditorActivity}.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800800 values.clear();
801 values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
802 values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
803
804 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700805 callbackIntent.setData(groupUri);
Katherine Kuan717e3432011-07-13 17:03:24 -0700806 // TODO: This can be taken out when the above TODO is addressed
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800807 callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -0800808 deliverCallback(callbackIntent);
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800809 }
810
811 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800812 * Creates an intent that can be sent to this service to rename a group.
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800813 */
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700814 public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
Josh Garguse5d3f892012-04-11 11:56:15 -0700815 Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800816 Intent serviceIntent = new Intent(context, ContactSaveService.class);
817 serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
818 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
819 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700820
821 // Callback intent will be invoked by the service once the group is renamed.
822 Intent callbackIntent = new Intent(context, callbackActivity);
823 callbackIntent.setAction(callbackAction);
824 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
825
Dmitri Plotnikov1ac58b62010-11-19 16:12:09 -0800826 return serviceIntent;
827 }
828
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800829 private void renameGroup(Intent intent) {
830 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
831 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
832
833 if (groupId == -1) {
834 Log.e(TAG, "Invalid arguments for renameGroup request");
835 return;
836 }
837
838 ContentValues values = new ContentValues();
839 values.put(Groups.TITLE, label);
Katherine Kuanc6b8afe2011-06-22 19:03:50 -0700840 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
841 getContentResolver().update(groupUri, values, null, null);
842
843 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
844 callbackIntent.setData(groupUri);
845 deliverCallback(callbackIntent);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800846 }
847
848 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800849 * Creates an intent that can be sent to this service to delete a group.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800850 */
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700851 public static Intent createGroupDeletionIntent(Context context, long groupId) {
852 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -0800853 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800854 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
Walter Jang72f99882016-05-26 09:01:31 -0700855
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800856 return serviceIntent;
857 }
858
859 private void deleteGroup(Intent intent) {
860 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
861 if (groupId == -1) {
862 Log.e(TAG, "Invalid arguments for deleteGroup request");
863 return;
864 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700865 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800866
Marcus Hagerott819214d2016-09-29 14:58:27 -0700867 final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED);
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700868 final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
869 callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
870 callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
Walter Jang72f99882016-05-26 09:01:31 -0700871
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700872 mGroupsDao.delete(groupUri);
873
874 LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
875 }
876
877 public static Intent createUndoIntent(Context context, Intent resultIntent) {
878 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
879 serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
880 serviceIntent.putExtras(resultIntent);
881 return serviceIntent;
882 }
883
884 private void undo(Intent intent) {
885 final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
886 if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
887 mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
Walter Jang72f99882016-05-26 09:01:31 -0700888 }
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800889 }
890
Marcus Hagerottbea2b852016-08-11 14:55:52 -0700891
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -0800892 /**
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700893 * Creates an intent that can be sent to this service to rename a group as
894 * well as add and remove members from the group.
895 *
896 * @param context of the application
897 * @param groupId of the group that should be modified
898 * @param newLabel is the updated name of the group (can be null if the name
899 * should not be updated)
900 * @param rawContactsToAdd is an array of raw contact IDs for contacts that
901 * should be added to the group
902 * @param rawContactsToRemove is an array of raw contact IDs for contacts
903 * that should be removed from the group
904 * @param callbackActivity is the activity to send the callback intent to
905 * @param callbackAction is the intent action for the callback intent
906 */
907 public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
908 long[] rawContactsToAdd, long[] rawContactsToRemove,
Josh Garguse5d3f892012-04-11 11:56:15 -0700909 Class<? extends Activity> callbackActivity, String callbackAction) {
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700910 Intent serviceIntent = new Intent(context, ContactSaveService.class);
911 serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
912 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
913 serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
914 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
915 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
916 rawContactsToRemove);
917
918 // Callback intent will be invoked by the service once the group is updated
919 Intent callbackIntent = new Intent(context, callbackActivity);
920 callbackIntent.setAction(callbackAction);
921 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
922
923 return serviceIntent;
924 }
925
926 private void updateGroup(Intent intent) {
927 long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
928 String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
929 long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
930 long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
931
932 if (groupId == -1) {
933 Log.e(TAG, "Invalid arguments for updateGroup request");
934 return;
935 }
936
937 final ContentResolver resolver = getContentResolver();
938 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
939
940 // Update group name if necessary
941 if (label != null) {
942 ContentValues values = new ContentValues();
943 values.put(Groups.TITLE, label);
Katherine Kuan717e3432011-07-13 17:03:24 -0700944 resolver.update(groupUri, values, null, null);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700945 }
946
Katherine Kuan717e3432011-07-13 17:03:24 -0700947 // Add and remove members if necessary
948 addMembersToGroup(resolver, rawContactsToAdd, groupId);
949 removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
950
951 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
952 callbackIntent.setData(groupUri);
953 deliverCallback(callbackIntent);
954 }
955
Walter Jang3a0b4832016-10-12 11:02:54 -0700956 private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
Katherine Kuan717e3432011-07-13 17:03:24 -0700957 long groupId) {
958 if (rawContactsToAdd == null) {
959 return;
960 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700961 for (long rawContactId : rawContactsToAdd) {
962 try {
963 final ArrayList<ContentProviderOperation> rawContactOperations =
964 new ArrayList<ContentProviderOperation>();
965
966 // Build an assert operation to ensure the contact is not already in the group
967 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
968 .newAssertQuery(Data.CONTENT_URI);
969 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
970 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
971 new String[] { String.valueOf(rawContactId),
972 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
973 assertBuilder.withExpectedCount(0);
974 rawContactOperations.add(assertBuilder.build());
975
976 // Build an insert operation to add the contact to the group
977 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
978 .newInsert(Data.CONTENT_URI);
979 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
980 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
981 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
982 rawContactOperations.add(insertBuilder.build());
983
984 if (DEBUG) {
985 for (ContentProviderOperation operation : rawContactOperations) {
986 Log.v(TAG, operation.toString());
987 }
988 }
989
990 // Apply batch
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700991 if (!rawContactOperations.isEmpty()) {
Daniel Lehmann18958a22012-02-28 17:45:25 -0800992 resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700993 }
994 } catch (RemoteException e) {
995 // Something went wrong, bail without success
Walter Jang3a0b4832016-10-12 11:02:54 -0700996 FeedbackHelper.sendFeedback(this, TAG,
997 "Problem persisting user edits for raw contact ID " +
998 String.valueOf(rawContactId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -0700999 } catch (OperationApplicationException e) {
1000 // The assert could have failed because the contact is already in the group,
1001 // just continue to the next contact
Walter Jang3a0b4832016-10-12 11:02:54 -07001002 FeedbackHelper.sendFeedback(this, TAG,
1003 "Assert failed in adding raw contact ID " +
1004 String.valueOf(rawContactId) + ". Already exists in group " +
1005 String.valueOf(groupId), e);
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001006 }
1007 }
Katherine Kuan717e3432011-07-13 17:03:24 -07001008 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001009
Daniel Lehmann18958a22012-02-28 17:45:25 -08001010 private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
Katherine Kuan717e3432011-07-13 17:03:24 -07001011 long groupId) {
1012 if (rawContactsToRemove == null) {
1013 return;
1014 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001015 for (long rawContactId : rawContactsToRemove) {
1016 // Apply the delete operation on the data row for the given raw contact's
1017 // membership in the given group. If no contact matches the provided selection, then
1018 // nothing will be done. Just continue to the next contact.
Daniel Lehmann18958a22012-02-28 17:45:25 -08001019 resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001020 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1021 new String[] { String.valueOf(rawContactId),
1022 GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
1023 }
Katherine Kuan2d851cc2011-07-05 16:23:27 -07001024 }
1025
1026 /**
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001027 * Creates an intent that can be sent to this service to star or un-star a contact.
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001028 */
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001029 public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
1030 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1031 serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
1032 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1033 serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
1034
Dmitri Plotnikove898a9f2010-11-18 16:58:25 -08001035 return serviceIntent;
1036 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001037
1038 private void setStarred(Intent intent) {
1039 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1040 boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
1041 if (contactUri == null) {
1042 Log.e(TAG, "Invalid arguments for setStarred request");
1043 return;
1044 }
1045
1046 final ContentValues values = new ContentValues(1);
1047 values.put(Contacts.STARRED, value);
1048 getContentResolver().update(contactUri, values, null, null);
Yorke Leee8e3fb82013-09-12 17:53:31 -07001049
1050 // Undemote the contact if necessary
1051 final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
1052 null, null, null);
Jay Shraunerc12a2802014-11-24 10:07:31 -08001053 if (c == null) {
1054 return;
1055 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001056 try {
1057 if (c.moveToFirst()) {
1058 final long id = c.getLong(0);
Yorke Leebbb8c992013-09-23 16:20:53 -07001059
1060 // Don't bother undemoting if this contact is the user's profile.
1061 if (id < Profile.MIN_ID) {
Wenyi Wangaac0e662015-12-18 17:17:33 -08001062 PinnedPositionsCompat.undemote(getContentResolver(), id);
Yorke Leebbb8c992013-09-23 16:20:53 -07001063 }
Yorke Leee8e3fb82013-09-12 17:53:31 -07001064 }
1065 } finally {
1066 c.close();
1067 }
Dmitri Plotnikov9d730dd2010-11-24 13:22:23 -08001068 }
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001069
1070 /**
Isaac Katzenelson683b57e2011-07-20 17:06:11 -07001071 * Creates an intent that can be sent to this service to set the redirect to voicemail.
1072 */
1073 public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
1074 boolean value) {
1075 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1076 serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
1077 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1078 serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
1079
1080 return serviceIntent;
1081 }
1082
1083 private void setSendToVoicemail(Intent intent) {
1084 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1085 boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
1086 if (contactUri == null) {
1087 Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
1088 return;
1089 }
1090
1091 final ContentValues values = new ContentValues(1);
1092 values.put(Contacts.SEND_TO_VOICEMAIL, value);
1093 getContentResolver().update(contactUri, values, null, null);
1094 }
1095
1096 /**
1097 * Creates an intent that can be sent to this service to save the contact's ringtone.
1098 */
1099 public static Intent createSetRingtone(Context context, Uri contactUri,
1100 String value) {
1101 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1102 serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
1103 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1104 serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
1105
1106 return serviceIntent;
1107 }
1108
1109 private void setRingtone(Intent intent) {
1110 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1111 String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
1112 if (contactUri == null) {
1113 Log.e(TAG, "Invalid arguments for setRingtone");
1114 return;
1115 }
1116 ContentValues values = new ContentValues(1);
1117 values.put(Contacts.CUSTOM_RINGTONE, value);
1118 getContentResolver().update(contactUri, values, null, null);
1119 }
1120
1121 /**
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001122 * Creates an intent that sets the selected data item as super primary (default)
1123 */
1124 public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
1125 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1126 serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
1127 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1128 return serviceIntent;
1129 }
1130
1131 private void setSuperPrimary(Intent intent) {
1132 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1133 if (dataId == -1) {
1134 Log.e(TAG, "Invalid arguments for setSuperPrimary request");
1135 return;
1136 }
1137
Chiao Chengd7ca03e2012-10-24 15:14:08 -07001138 ContactUpdateUtils.setSuperPrimary(this, dataId);
Daniel Lehmann0f78e8b2010-11-24 17:32:03 -08001139 }
1140
1141 /**
1142 * Creates an intent that clears the primary flag of all data items that belong to the same
1143 * raw_contact as the given data item. Will only clear, if the data item was primary before
1144 * this call
1145 */
1146 public static Intent createClearPrimaryIntent(Context context, long dataId) {
1147 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1148 serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
1149 serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
1150 return serviceIntent;
1151 }
1152
1153 private void clearPrimary(Intent intent) {
1154 long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
1155 if (dataId == -1) {
1156 Log.e(TAG, "Invalid arguments for clearPrimary request");
1157 return;
1158 }
1159
1160 // Update the primary values in the data record.
1161 ContentValues values = new ContentValues(1);
1162 values.put(Data.IS_SUPER_PRIMARY, 0);
1163 values.put(Data.IS_PRIMARY, 0);
1164
1165 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
1166 values, null, null);
1167 }
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001168
1169 /**
1170 * Creates an intent that can be sent to this service to delete a contact.
1171 */
1172 public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
1173 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1174 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
1175 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
1176 return serviceIntent;
1177 }
1178
Brian Attwelld2962a32015-03-02 14:48:50 -08001179 /**
1180 * Creates an intent that can be sent to this service to delete multiple contacts.
1181 */
1182 public static Intent createDeleteMultipleContactsIntent(Context context,
James Laskeye5a140a2016-10-18 15:43:42 -07001183 long[] contactIds, final String[] names) {
Brian Attwelld2962a32015-03-02 14:48:50 -08001184 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1185 serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
1186 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
James Laskeye5a140a2016-10-18 15:43:42 -07001187 serviceIntent.putExtra(ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY, names);
Brian Attwelld2962a32015-03-02 14:48:50 -08001188 return serviceIntent;
1189 }
1190
Dmitri Plotnikov7d8cabb2010-11-24 17:40:01 -08001191 private void deleteContact(Intent intent) {
1192 Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
1193 if (contactUri == null) {
1194 Log.e(TAG, "Invalid arguments for deleteContact request");
1195 return;
1196 }
1197
1198 getContentResolver().delete(contactUri, null, null);
1199 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001200
Brian Attwelld2962a32015-03-02 14:48:50 -08001201 private void deleteMultipleContacts(Intent intent) {
1202 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
1203 if (contactIds == null) {
1204 Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
1205 return;
1206 }
1207 for (long contactId : contactIds) {
1208 final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
1209 getContentResolver().delete(contactUri, null, null);
1210 }
James Laskeye5a140a2016-10-18 15:43:42 -07001211 final String[] names = intent.getStringArrayExtra(
1212 ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY);
1213 final String deleteToastMessage;
James Laskey56019ad2016-11-14 16:38:35 -08001214 if (contactIds.length != names.length || names.length == 0) {
James Laskeye5a140a2016-10-18 15:43:42 -07001215 deleteToastMessage = getResources().getQuantityString(
1216 R.plurals.contacts_deleted_toast, contactIds.length);
1217 } else if (names.length == 1) {
1218 deleteToastMessage = getResources().getString(
1219 R.string.contacts_deleted_one_named_toast, names);
1220 } else if (names.length == 2) {
1221 deleteToastMessage = getResources().getString(
1222 R.string.contacts_deleted_two_named_toast, names);
1223 } else {
1224 deleteToastMessage = getResources().getString(
1225 R.string.contacts_deleted_many_named_toast, names);
1226 }
James Laskey56019ad2016-11-14 16:38:35 -08001227
Wenyi Wang687d2182015-10-28 17:03:18 -07001228 mMainHandler.post(new Runnable() {
1229 @Override
1230 public void run() {
1231 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
1232 .show();
1233 }
1234 });
Brian Attwelld2962a32015-03-02 14:48:50 -08001235 }
1236
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001237 /**
Gary Mai7efa9942016-05-12 11:26:49 -07001238 * 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 -08001239 * pieces. This will set the raw contact ids to {@link AggregationExceptions#TYPE_AUTOMATIC} so
Gary Mai53fe0d22016-07-26 17:23:53 -07001240 * they may be re-merged by the auto-aggregator.
Gary Mai7efa9942016-05-12 11:26:49 -07001241 */
1242 public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
1243 ResultReceiver receiver) {
1244 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1245 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1246 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1247 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
1248 return serviceIntent;
1249 }
1250
Gary Maib9065dd2016-11-08 10:49:00 -08001251 /**
1252 * Creates an intent that can be sent to this service to split a contact into it's constituent
1253 * pieces. This will explicitly set the raw contact ids to
1254 * {@link AggregationExceptions#TYPE_KEEP_SEPARATE}.
1255 */
1256 public static Intent createHardSplitContactIntent(Context context, long[][] rawContactIds) {
1257 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
1258 serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
1259 serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
1260 serviceIntent.putExtra(ContactSaveService.EXTRA_HARD_SPLIT, true);
1261 return serviceIntent;
1262 }
1263
Gary Mai7efa9942016-05-12 11:26:49 -07001264 private void splitContact(Intent intent) {
1265 final long rawContactIds[][] = (long[][]) intent
1266 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
Gary Mai31d572e2016-06-03 14:04:32 -07001267 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Gary Maib9065dd2016-11-08 10:49:00 -08001268 final boolean hardSplit = intent.getBooleanExtra(EXTRA_HARD_SPLIT, false);
Gary Mai7efa9942016-05-12 11:26:49 -07001269 if (rawContactIds == null) {
1270 Log.e(TAG, "Invalid argument for splitContact request");
Gary Mai31d572e2016-06-03 14:04:32 -07001271 if (receiver != null) {
1272 receiver.send(BAD_ARGUMENTS, new Bundle());
1273 }
Gary Mai7efa9942016-05-12 11:26:49 -07001274 return;
1275 }
1276 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1277 final ContentResolver resolver = getContentResolver();
1278 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Gary Mai7efa9942016-05-12 11:26:49 -07001279 for (int i = 0; i < rawContactIds.length; i++) {
1280 for (int j = 0; j < rawContactIds.length; j++) {
1281 if (i != j) {
Gary Maib9065dd2016-11-08 10:49:00 -08001282 if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j],
1283 hardSplit)) {
Gary Mai7efa9942016-05-12 11:26:49 -07001284 if (receiver != null) {
1285 receiver.send(CP2_ERROR, new Bundle());
1286 return;
1287 }
1288 }
1289 }
1290 }
1291 }
1292 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1293 if (receiver != null) {
1294 receiver.send(CP2_ERROR, new Bundle());
1295 }
1296 return;
1297 }
Gary Maib9065dd2016-11-08 10:49:00 -08001298 LocalBroadcastManager.getInstance(this)
1299 .sendBroadcast(new Intent(BROADCAST_UNLINK_COMPLETE));
Gary Mai7efa9942016-05-12 11:26:49 -07001300 if (receiver != null) {
1301 receiver.send(CONTACTS_SPLIT, new Bundle());
1302 } else {
1303 showToast(R.string.contactUnlinkedToast);
1304 }
1305 }
1306
1307 /**
Gary Mai53fe0d22016-07-26 17:23:53 -07001308 * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
Gary Mai7efa9942016-05-12 11:26:49 -07001309 * and {@param rawContactIds2} to {@param operations}.
1310 * @return false if an error occurred, true otherwise.
1311 */
1312 private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
Gary Maib9065dd2016-11-08 10:49:00 -08001313 long[] rawContactIds1, long[] rawContactIds2, boolean hardSplit) {
Gary Mai7efa9942016-05-12 11:26:49 -07001314 if (rawContactIds1 == null || rawContactIds2 == null) {
1315 Log.e(TAG, "Invalid arguments for splitContact request");
1316 return false;
1317 }
1318 // For each pair of raw contacts, insert an aggregation exception
1319 final ContentResolver resolver = getContentResolver();
1320 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1321 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1322 for (int i = 0; i < rawContactIds1.length; i++) {
1323 for (int j = 0; j < rawContactIds2.length; j++) {
Gary Maib9065dd2016-11-08 10:49:00 -08001324 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j], hardSplit);
Gary Mai7efa9942016-05-12 11:26:49 -07001325 // Before we get to 500 we need to flush the operations list
1326 if (operations.size() > 0 && operations.size() % batchSize == 0) {
1327 if (!applyOperations(resolver, operations)) {
1328 return false;
1329 }
1330 operations.clear();
1331 }
1332 }
1333 }
1334 return true;
1335 }
1336
1337 /**
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001338 * Creates an intent that can be sent to this service to join two contacts.
Brian Attwelld3946ca2015-03-03 11:13:49 -08001339 * The resulting contact uses the name from {@param contactId1} if possible.
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001340 */
1341 public static Intent createJoinContactsIntent(Context context, long contactId1,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001342 long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001343 Intent serviceIntent = new Intent(context, ContactSaveService.class);
1344 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
1345 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
1346 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001347
1348 // Callback intent will be invoked by the service once the contacts are joined.
1349 Intent callbackIntent = new Intent(context, callbackActivity);
1350 callbackIntent.setAction(callbackAction);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001351 serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
1352
1353 return serviceIntent;
1354 }
1355
Brian Attwelld3946ca2015-03-03 11:13:49 -08001356 /**
1357 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1358 * No special attention is paid to where the resulting contact's name is taken from.
1359 */
Gary Mai7efa9942016-05-12 11:26:49 -07001360 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
1361 ResultReceiver receiver) {
1362 final Intent serviceIntent = new Intent(context, ContactSaveService.class);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001363 serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
1364 serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
Gary Mai7efa9942016-05-12 11:26:49 -07001365 serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001366 return serviceIntent;
1367 }
1368
Gary Mai7efa9942016-05-12 11:26:49 -07001369 /**
1370 * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
1371 * No special attention is paid to where the resulting contact's name is taken from.
1372 */
1373 public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
1374 return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
1375 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001376
1377 private interface JoinContactQuery {
1378 String[] PROJECTION = {
1379 RawContacts._ID,
1380 RawContacts.CONTACT_ID,
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001381 RawContacts.DISPLAY_NAME_SOURCE,
1382 };
1383
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001384 int _ID = 0;
1385 int CONTACT_ID = 1;
Brian Attwell548f5c62015-01-27 17:46:46 -08001386 int DISPLAY_NAME_SOURCE = 2;
1387 }
1388
1389 private interface ContactEntityQuery {
1390 String[] PROJECTION = {
1391 Contacts.Entity.DATA_ID,
1392 Contacts.Entity.CONTACT_ID,
1393 Contacts.Entity.IS_SUPER_PRIMARY,
1394 };
1395 String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
1396 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
1397 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
1398 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
1399
1400 int DATA_ID = 0;
1401 int CONTACT_ID = 1;
1402 int IS_SUPER_PRIMARY = 2;
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001403 }
1404
Brian Attwelld3946ca2015-03-03 11:13:49 -08001405 private void joinSeveralContacts(Intent intent) {
1406 final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001407
Gary Mai7efa9942016-05-12 11:26:49 -07001408 final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
Brian Attwell548f5c62015-01-27 17:46:46 -08001409
Brian Attwelld3946ca2015-03-03 11:13:49 -08001410 // Load raw contact IDs for all contacts involved.
Gary Mai7efa9942016-05-12 11:26:49 -07001411 final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
1412 final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001413 if (rawContactIds == null) {
1414 Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
Gary Mai31d572e2016-06-03 14:04:32 -07001415 if (receiver != null) {
1416 receiver.send(BAD_ARGUMENTS, new Bundle());
1417 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001418 return;
1419 }
1420
Brian Attwelld3946ca2015-03-03 11:13:49 -08001421 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001422 final ContentResolver resolver = getContentResolver();
Walter Jang0653de32015-07-24 12:12:40 -07001423 // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
1424 final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
1425 final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001426 for (int i = 0; i < rawContactIds.length; i++) {
1427 for (int j = 0; j < rawContactIds.length; j++) {
1428 if (i != j) {
1429 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1430 }
Walter Jang0653de32015-07-24 12:12:40 -07001431 // Before we get to 500 we need to flush the operations list
1432 if (operations.size() > 0 && operations.size() % batchSize == 0) {
Gary Mai7efa9942016-05-12 11:26:49 -07001433 if (!applyOperations(resolver, operations)) {
1434 if (receiver != null) {
1435 receiver.send(CP2_ERROR, new Bundle());
1436 }
Walter Jang0653de32015-07-24 12:12:40 -07001437 return;
1438 }
1439 operations.clear();
1440 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001441 }
1442 }
Gary Mai7efa9942016-05-12 11:26:49 -07001443 if (operations.size() > 0 && !applyOperations(resolver, operations)) {
1444 if (receiver != null) {
1445 receiver.send(CP2_ERROR, new Bundle());
1446 }
Walter Jang0653de32015-07-24 12:12:40 -07001447 return;
1448 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001449
John Shaoa3c507a2016-09-13 14:26:17 -07001450
1451 final String name = queryNameOfLinkedContacts(contactIds);
1452 if (name != null) {
1453 if (receiver != null) {
1454 final Bundle result = new Bundle();
1455 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
1456 result.putString(EXTRA_DISPLAY_NAME, name);
1457 receiver.send(CONTACTS_LINKED, result);
1458 } else {
James Laskeyf62b4882016-10-21 11:36:40 -07001459 if (TextUtils.isEmpty(name)) {
1460 showToast(R.string.contactsJoinedMessage);
1461 } else {
1462 showToast(R.string.contactsJoinedNamedMessage, name);
1463 }
John Shaoa3c507a2016-09-13 14:26:17 -07001464 }
Gary Maib9065dd2016-11-08 10:49:00 -08001465 LocalBroadcastManager.getInstance(this)
1466 .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
Gary Mai7efa9942016-05-12 11:26:49 -07001467 } else {
John Shaoa3c507a2016-09-13 14:26:17 -07001468 if (receiver != null) {
1469 receiver.send(CP2_ERROR, new Bundle());
1470 }
1471 showToast(R.string.contactJoinErrorToast);
Gary Mai7efa9942016-05-12 11:26:49 -07001472 }
Walter Jang0653de32015-07-24 12:12:40 -07001473 }
Brian Attwelld3946ca2015-03-03 11:13:49 -08001474
John Shaoa3c507a2016-09-13 14:26:17 -07001475 /** Get the display name of the top-level contact after the contacts have been linked. */
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001476 private String queryNameOfLinkedContacts(long[] contactIds) {
1477 final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
1478 final String[] whereArgs = new String[contactIds.length];
1479 for (int i = 0; i < contactIds.length; i++) {
1480 whereArgs[i] = String.valueOf(contactIds[i]);
1481 whereBuilder.append("?,");
1482 }
1483 whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
1484 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
James Laskeyf62b4882016-10-21 11:36:40 -07001485 new String[]{Contacts._ID, Contacts.DISPLAY_NAME,
1486 Contacts.DISPLAY_NAME_ALTERNATIVE},
John Shaoa3c507a2016-09-13 14:26:17 -07001487 whereBuilder.toString(), whereArgs, null);
1488
1489 String name = null;
James Laskeyf62b4882016-10-21 11:36:40 -07001490 String nameAlt = null;
John Shaoa3c507a2016-09-13 14:26:17 -07001491 long contactId = 0;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001492 try {
1493 if (cursor.moveToFirst()) {
John Shaoa3c507a2016-09-13 14:26:17 -07001494 contactId = cursor.getLong(0);
1495 name = cursor.getString(1);
James Laskeyf62b4882016-10-21 11:36:40 -07001496 nameAlt = cursor.getString(2);
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001497 }
John Shaoa3c507a2016-09-13 14:26:17 -07001498 while(cursor.moveToNext()) {
1499 if (cursor.getLong(0) != contactId) {
1500 return null;
1501 }
1502 }
James Laskeyf62b4882016-10-21 11:36:40 -07001503
1504 final String formattedName = ContactDisplayUtils.getPreferredDisplayName(name, nameAlt,
1505 new ContactsPreferences(getApplicationContext()));
1506 return formattedName == null ? "" : formattedName;
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001507 } finally {
John Shaoa3c507a2016-09-13 14:26:17 -07001508 if (cursor != null) {
1509 cursor.close();
1510 }
Marcus Hagerott3bb85142016-07-29 10:46:36 -07001511 }
1512 }
1513
Walter Jang0653de32015-07-24 12:12:40 -07001514 /** Returns true if the batch was successfully applied and false otherwise. */
Gary Mai7efa9942016-05-12 11:26:49 -07001515 private boolean applyOperations(ContentResolver resolver,
Walter Jang0653de32015-07-24 12:12:40 -07001516 ArrayList<ContentProviderOperation> operations) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001517 try {
John Shaoa3c507a2016-09-13 14:26:17 -07001518 final ContentProviderResult[] result =
1519 resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1520 for (int i = 0; i < result.length; ++i) {
1521 // if no rows were modified in the operation then we count it as fail.
1522 if (result[i].count < 0) {
1523 throw new OperationApplicationException();
1524 }
1525 }
Walter Jang0653de32015-07-24 12:12:40 -07001526 return true;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001527 } catch (RemoteException | OperationApplicationException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -07001528 FeedbackHelper.sendFeedback(this, TAG,
1529 "Failed to apply aggregation exception batch", e);
Brian Attwelld3946ca2015-03-03 11:13:49 -08001530 showToast(R.string.contactSavedErrorToast);
Walter Jang0653de32015-07-24 12:12:40 -07001531 return false;
Brian Attwelld3946ca2015-03-03 11:13:49 -08001532 }
1533 }
1534
Brian Attwelld3946ca2015-03-03 11:13:49 -08001535 private void joinContacts(Intent intent) {
1536 long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
1537 long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001538
1539 // Load raw contact IDs for all raw contacts involved - currently edited and selected
Brian Attwell548f5c62015-01-27 17:46:46 -08001540 // in the join UIs.
1541 long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
1542 if (rawContactIds == null) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001543 Log.e(TAG, "Invalid arguments for joinContacts request");
Jay Shraunerc12a2802014-11-24 10:07:31 -08001544 return;
1545 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001546
Brian Attwell548f5c62015-01-27 17:46:46 -08001547 ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001548
1549 // For each pair of raw contacts, insert an aggregation exception
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001550 for (int i = 0; i < rawContactIds.length; i++) {
1551 for (int j = 0; j < rawContactIds.length; j++) {
1552 if (i != j) {
1553 buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1554 }
1555 }
1556 }
1557
Brian Attwelld3946ca2015-03-03 11:13:49 -08001558 final ContentResolver resolver = getContentResolver();
1559
Brian Attwell548f5c62015-01-27 17:46:46 -08001560 // Use the name for contactId1 as the name for the newly aggregated contact.
1561 final Uri contactId1Uri = ContentUris.withAppendedId(
1562 Contacts.CONTENT_URI, contactId1);
1563 final Uri entityUri = Uri.withAppendedPath(
1564 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
1565 Cursor c = resolver.query(entityUri,
1566 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
1567 if (c == null) {
1568 Log.e(TAG, "Unable to open Contacts DB cursor");
1569 showToast(R.string.contactSavedErrorToast);
1570 return;
1571 }
1572 long dataIdToAddSuperPrimary = -1;
1573 try {
1574 if (c.moveToFirst()) {
1575 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
1576 }
1577 } finally {
1578 c.close();
1579 }
1580
1581 // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
1582 // display name does not change as a result of the join.
1583 if (dataIdToAddSuperPrimary != -1) {
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001584 Builder builder = ContentProviderOperation.newUpdate(
Brian Attwell548f5c62015-01-27 17:46:46 -08001585 ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
1586 builder.withValue(Data.IS_SUPER_PRIMARY, 1);
1587 builder.withValue(Data.IS_PRIMARY, 1);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001588 operations.add(builder.build());
1589 }
1590
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001591 // Apply all aggregation exceptions as one batch
John Shaoa3c507a2016-09-13 14:26:17 -07001592 final boolean success = applyOperations(resolver, operations);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001593
John Shaoa3c507a2016-09-13 14:26:17 -07001594 final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2});
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001595 Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
John Shaoa3c507a2016-09-13 14:26:17 -07001596 if (success && name != null) {
James Laskeyf62b4882016-10-21 11:36:40 -07001597 if (TextUtils.isEmpty(name)) {
1598 showToast(R.string.contactsJoinedMessage);
1599 } else {
1600 showToast(R.string.contactsJoinedNamedMessage, name);
1601 }
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001602 Uri uri = RawContacts.getContactLookupUri(resolver,
1603 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1604 callbackIntent.setData(uri);
Gary Maib9065dd2016-11-08 10:49:00 -08001605 LocalBroadcastManager.getInstance(this)
1606 .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001607 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001608 deliverCallback(callbackIntent);
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001609 }
1610
Gary Mai7efa9942016-05-12 11:26:49 -07001611 /**
1612 * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
1613 * array of the return value holds an array of raw contact ids for one contactId.
1614 * @param contactIds
1615 * @return
1616 */
1617 private long[][] getSeparatedRawContactIds(long[] contactIds) {
1618 final long[][] rawContactIds = new long[contactIds.length][];
1619 for (int i = 0; i < contactIds.length; i++) {
1620 rawContactIds[i] = getRawContactIds(contactIds[i]);
1621 }
1622 return rawContactIds;
1623 }
1624
1625 /**
1626 * Gets the raw contact ids associated with {@param contactId}.
1627 * @param contactId
1628 * @return Array of raw contact ids.
1629 */
1630 private long[] getRawContactIds(long contactId) {
1631 final ContentResolver resolver = getContentResolver();
1632 long rawContactIds[];
1633
1634 final StringBuilder queryBuilder = new StringBuilder();
1635 queryBuilder.append(RawContacts.CONTACT_ID)
1636 .append("=")
1637 .append(String.valueOf(contactId));
1638
1639 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1640 JoinContactQuery.PROJECTION,
1641 queryBuilder.toString(),
1642 null, null);
1643 if (c == null) {
1644 Log.e(TAG, "Unable to open Contacts DB cursor");
1645 return null;
1646 }
1647 try {
1648 rawContactIds = new long[c.getCount()];
1649 for (int i = 0; i < rawContactIds.length; i++) {
1650 c.moveToPosition(i);
1651 final long rawContactId = c.getLong(JoinContactQuery._ID);
1652 rawContactIds[i] = rawContactId;
1653 }
1654 } finally {
1655 c.close();
1656 }
1657 return rawContactIds;
1658 }
1659
Brian Attwelld3946ca2015-03-03 11:13:49 -08001660 private long[] getRawContactIdsForAggregation(long[] contactIds) {
1661 if (contactIds == null) {
1662 return null;
1663 }
1664
Brian Attwell548f5c62015-01-27 17:46:46 -08001665 final ContentResolver resolver = getContentResolver();
Brian Attwelld3946ca2015-03-03 11:13:49 -08001666
1667 final StringBuilder queryBuilder = new StringBuilder();
1668 final String stringContactIds[] = new String[contactIds.length];
1669 for (int i = 0; i < contactIds.length; i++) {
1670 queryBuilder.append(RawContacts.CONTACT_ID + "=?");
1671 stringContactIds[i] = String.valueOf(contactIds[i]);
1672 if (contactIds[i] == -1) {
1673 return null;
1674 }
1675 if (i == contactIds.length -1) {
1676 break;
1677 }
1678 queryBuilder.append(" OR ");
1679 }
1680
Brian Attwell548f5c62015-01-27 17:46:46 -08001681 final Cursor c = resolver.query(RawContacts.CONTENT_URI,
1682 JoinContactQuery.PROJECTION,
Brian Attwelld3946ca2015-03-03 11:13:49 -08001683 queryBuilder.toString(),
1684 stringContactIds, null);
Brian Attwell548f5c62015-01-27 17:46:46 -08001685 if (c == null) {
1686 Log.e(TAG, "Unable to open Contacts DB cursor");
1687 showToast(R.string.contactSavedErrorToast);
1688 return null;
1689 }
Gary Mai7efa9942016-05-12 11:26:49 -07001690 long rawContactIds[];
Brian Attwell548f5c62015-01-27 17:46:46 -08001691 try {
1692 if (c.getCount() < 2) {
Brian Attwelld3946ca2015-03-03 11:13:49 -08001693 Log.e(TAG, "Not enough raw contacts to aggregate together.");
Brian Attwell548f5c62015-01-27 17:46:46 -08001694 return null;
1695 }
1696 rawContactIds = new long[c.getCount()];
1697 for (int i = 0; i < rawContactIds.length; i++) {
1698 c.moveToPosition(i);
1699 long rawContactId = c.getLong(JoinContactQuery._ID);
1700 rawContactIds[i] = rawContactId;
1701 }
1702 } finally {
1703 c.close();
1704 }
1705 return rawContactIds;
1706 }
1707
Brian Attwelld3946ca2015-03-03 11:13:49 -08001708 private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
1709 return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
1710 }
1711
Dmitri Plotnikov2b46f032010-11-29 16:41:43 -08001712 /**
1713 * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1714 */
1715 private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1716 long rawContactId1, long rawContactId2) {
1717 Builder builder =
1718 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1719 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1720 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1721 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1722 operations.add(builder.build());
1723 }
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001724
1725 /**
Gary Maib9065dd2016-11-08 10:49:00 -08001726 * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} or a
1727 * {@link AggregationExceptions#TYPE_KEEP_SEPARATE} ContentProviderOperation if a hard split is
1728 * requested.
Gary Mai7efa9942016-05-12 11:26:49 -07001729 */
1730 private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
Gary Maib9065dd2016-11-08 10:49:00 -08001731 long rawContactId1, long rawContactId2, boolean hardSplit) {
Gary Mai7efa9942016-05-12 11:26:49 -07001732 final Builder builder =
1733 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
Gary Maib9065dd2016-11-08 10:49:00 -08001734 builder.withValue(AggregationExceptions.TYPE,
1735 hardSplit
1736 ? AggregationExceptions.TYPE_KEEP_SEPARATE
1737 : AggregationExceptions.TYPE_AUTOMATIC);
Gary Mai7efa9942016-05-12 11:26:49 -07001738 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1739 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1740 operations.add(builder.build());
1741 }
1742
Marcus Hagerott7333c372016-11-07 09:40:20 -08001743 /**
Marcus Hagerott7333c372016-11-07 09:40:20 -08001744 * Returns an intent that can start this service and cause it to sleep for the specified time.
1745 *
1746 * This exists purely for debugging and manual testing. Since this service uses a single thread
1747 * it is useful to have a way to test behavior when work is queued up and most of the other
1748 * operations complete too quickly to simulate that under normal conditions.
1749 */
1750 public static Intent createSleepIntent(Context context, long millis) {
1751 return new Intent(context, ContactSaveService.class).setAction(ACTION_SLEEP)
1752 .putExtra(EXTRA_SLEEP_DURATION, millis);
1753 }
1754
1755 private void sleepForDebugging(Intent intent) {
1756 long duration = intent.getLongExtra(EXTRA_SLEEP_DURATION, 1000);
1757 if (Log.isLoggable(TAG, Log.DEBUG)) {
1758 Log.d(TAG, "sleeping for " + duration + "ms");
1759 }
1760 try {
1761 Thread.sleep(duration);
1762 } catch (InterruptedException e) {
1763 e.printStackTrace();
1764 }
1765 if (Log.isLoggable(TAG, Log.DEBUG)) {
1766 Log.d(TAG, "finished sleeping");
Marcus Hagerott819214d2016-09-29 14:58:27 -07001767 }
1768 }
1769
Gary Mai7efa9942016-05-12 11:26:49 -07001770 /**
James Laskeyf62b4882016-10-21 11:36:40 -07001771 * Shows a toast on the UI thread by formatting messageId using args.
1772 * @param messageId id of message string
1773 * @param args args to format string
1774 */
1775 private void showToast(final int messageId, final Object... args) {
1776 final String message = getResources().getString(messageId, args);
1777 mMainHandler.post(new Runnable() {
1778 @Override
1779 public void run() {
1780 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1781 }
1782 });
1783 }
1784
1785
1786 /**
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001787 * Shows a toast on the UI thread.
1788 */
1789 private void showToast(final int message) {
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001790 mMainHandler.post(new Runnable() {
Dmitri Plotnikov886d3d62011-01-03 10:08:47 -08001791
1792 @Override
1793 public void run() {
1794 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1795 }
1796 });
1797 }
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001798
1799 private void deliverCallback(final Intent callbackIntent) {
1800 mMainHandler.post(new Runnable() {
1801
1802 @Override
1803 public void run() {
1804 deliverCallbackOnUiThread(callbackIntent);
1805 }
1806 });
1807 }
1808
1809 void deliverCallbackOnUiThread(final Intent callbackIntent) {
1810 // TODO: this assumes that if there are multiple instances of the same
1811 // activity registered, the last one registered is the one waiting for
1812 // the callback. Validity of this assumption needs to be verified.
Hugo Hudsona831c0b2011-08-13 11:50:15 +01001813 for (Listener listener : sListeners) {
1814 if (callbackIntent.getComponent().equals(
1815 ((Activity) listener).getIntent().getComponent())) {
1816 listener.onServiceCompleted(callbackIntent);
1817 return;
Dmitri Plotnikov3a6a9052011-03-02 10:14:43 -08001818 }
1819 }
1820 }
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001821
1822 public interface GroupsDao {
1823 Uri create(String title, AccountWithDataSet account);
1824 int delete(Uri groupUri);
1825 Bundle captureDeletionUndoData(Uri groupUri);
1826 Uri undoDeletion(Bundle undoData);
1827 }
1828
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001829 public static class GroupsDaoImpl implements GroupsDao {
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001830 public static final String KEY_GROUP_DATA = "groupData";
Marcus Hagerottbea2b852016-08-11 14:55:52 -07001831 public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
1832
1833 private static final String TAG = "GroupsDao";
1834 private final Context context;
1835 private final ContentResolver contentResolver;
1836
1837 public GroupsDaoImpl(Context context) {
1838 this(context, context.getContentResolver());
1839 }
1840
1841 public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
1842 this.context = context;
1843 this.contentResolver = contentResolver;
1844 }
1845
1846 public Bundle captureDeletionUndoData(Uri groupUri) {
1847 final long groupId = ContentUris.parseId(groupUri);
1848 final Bundle result = new Bundle();
1849
1850 final Cursor cursor = contentResolver.query(groupUri,
1851 new String[]{
1852 Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
1853 Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
1854 Groups.SHOULD_SYNC
1855 },
1856 Groups.DELETED + "=?", new String[] { "0" }, null);
1857 try {
1858 if (cursor.moveToFirst()) {
1859 final ContentValues groupValues = new ContentValues();
1860 DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
1861 result.putParcelable(KEY_GROUP_DATA, groupValues);
1862 } else {
1863 // Group doesn't exist.
1864 return result;
1865 }
1866 } finally {
1867 cursor.close();
1868 }
1869
1870 final Cursor membersCursor = contentResolver.query(
1871 Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
1872 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
1873 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
1874 final long[] memberIds = new long[membersCursor.getCount()];
1875 int i = 0;
1876 while (membersCursor.moveToNext()) {
1877 memberIds[i++] = membersCursor.getLong(0);
1878 }
1879 result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
1880 return result;
1881 }
1882
1883 public Uri undoDeletion(Bundle deletedGroupData) {
1884 final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
1885 if (groupData == null) {
1886 return null;
1887 }
1888 final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
1889 final long groupId = ContentUris.parseId(groupUri);
1890
1891 final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
1892 if (memberIds == null) {
1893 return groupUri;
1894 }
1895 final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
1896 for (int i = 0; i < memberIds.length; i++) {
1897 memberInsertions[i] = new ContentValues();
1898 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
1899 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1900 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
1901 }
1902 final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
1903 if (inserted != memberIds.length) {
1904 Log.e(TAG, "Could not recover some members for group deletion undo");
1905 }
1906
1907 return groupUri;
1908 }
1909
1910 public Uri create(String title, AccountWithDataSet account) {
1911 final ContentValues values = new ContentValues();
1912 values.put(Groups.TITLE, title);
1913 values.put(Groups.ACCOUNT_NAME, account.name);
1914 values.put(Groups.ACCOUNT_TYPE, account.type);
1915 values.put(Groups.DATA_SET, account.dataSet);
1916 return contentResolver.insert(Groups.CONTENT_URI, values);
1917 }
1918
1919 public int delete(Uri groupUri) {
1920 return contentResolver.delete(groupUri, null, null);
1921 }
1922 }
Marcus Hagerott7333c372016-11-07 09:40:20 -08001923
1924 /**
1925 * Keeps track of which operations have been requested but have not yet finished for this
1926 * service.
1927 */
1928 public static class State {
1929 private final CopyOnWriteArrayList<Intent> mPending;
1930
1931 public State() {
1932 mPending = new CopyOnWriteArrayList<>();
1933 }
1934
1935 public State(Collection<Intent> pendingActions) {
1936 mPending = new CopyOnWriteArrayList<>(pendingActions);
1937 }
1938
1939 public boolean isIdle() {
1940 return mPending.isEmpty();
1941 }
1942
1943 public Intent getCurrentIntent() {
1944 return mPending.isEmpty() ? null : mPending.get(0);
1945 }
1946
1947 /**
1948 * Returns the first intent requested that has the specified action or null if no intent
1949 * with that action has been requested.
1950 */
1951 public Intent getNextIntentWithAction(String action) {
1952 for (Intent intent : mPending) {
1953 if (action.equals(intent.getAction())) {
1954 return intent;
1955 }
1956 }
1957 return null;
1958 }
1959
1960 public boolean isActionPending(String action) {
1961 return getNextIntentWithAction(action) != null;
1962 }
1963
1964 private void onFinish(Intent intent) {
1965 if (mPending.isEmpty()) {
1966 return;
1967 }
1968 final String action = mPending.get(0).getAction();
1969 if (action.equals(intent.getAction())) {
1970 mPending.remove(0);
1971 }
1972 }
1973
1974 private void onStart(Intent intent) {
1975 if (intent.getAction() == null) {
1976 return;
1977 }
1978 mPending.add(intent);
1979 }
1980 }
Daniel Lehmann173ffe12010-06-14 18:19:10 -07001981}