1/*
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
17package com.android.contacts;
18
19import android.app.Activity;
20import android.app.IntentService;
21import android.content.ContentProviderOperation;
22import android.content.ContentProviderOperation.Builder;
23import android.content.ContentProviderResult;
24import android.content.ContentResolver;
25import android.content.ContentUris;
26import android.content.ContentValues;
27import android.content.Context;
28import android.content.Intent;
29import android.content.OperationApplicationException;
30import android.database.Cursor;
31import android.net.Uri;
32import android.os.Bundle;
33import android.os.Handler;
34import android.os.Looper;
35import android.os.Parcelable;
36import android.os.RemoteException;
37import android.provider.ContactsContract;
38import android.provider.ContactsContract.AggregationExceptions;
39import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
40import android.provider.ContactsContract.Contacts;
41import android.provider.ContactsContract.Data;
42import android.provider.ContactsContract.Groups;
43import android.provider.ContactsContract.PinnedPositions;
44import android.provider.ContactsContract.Profile;
45import android.provider.ContactsContract.RawContacts;
46import android.provider.ContactsContract.RawContactsEntity;
47import android.util.Log;
48import android.widget.Toast;
49
50import com.android.contacts.common.database.ContactUpdateUtils;
51import com.android.contacts.common.model.AccountTypeManager;
52import com.android.contacts.common.model.RawContactDelta;
53import com.android.contacts.common.model.RawContactDeltaList;
54import com.android.contacts.common.model.RawContactModifier;
55import com.android.contacts.common.model.account.AccountWithDataSet;
56import com.android.contacts.util.CallerInfoCacheUtils;
57import com.android.contacts.util.ContactPhotoUtils;
58
59import com.google.common.collect.Lists;
60import com.google.common.collect.Sets;
61
62import java.io.File;
63import java.io.FileInputStream;
64import java.io.FileOutputStream;
65import java.io.IOException;
66import java.io.InputStream;
67import java.util.ArrayList;
68import java.util.HashSet;
69import java.util.List;
70import java.util.concurrent.CopyOnWriteArrayList;
71
72/**
73 * A service responsible for saving changes to the content provider.
74 */
75public class ContactSaveService extends IntentService {
76    private static final String TAG = "ContactSaveService";
77
78    /** Set to true in order to view logs on content provider operations */
79    private static final boolean DEBUG = false;
80
81    public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
82
83    public static final String EXTRA_ACCOUNT_NAME = "accountName";
84    public static final String EXTRA_ACCOUNT_TYPE = "accountType";
85    public static final String EXTRA_DATA_SET = "dataSet";
86    public static final String EXTRA_CONTENT_VALUES = "contentValues";
87    public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
88
89    public static final String ACTION_SAVE_CONTACT = "saveContact";
90    public static final String EXTRA_CONTACT_STATE = "state";
91    public static final String EXTRA_SAVE_MODE = "saveMode";
92    public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
93    public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
94    public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
95
96    public static final String ACTION_CREATE_GROUP = "createGroup";
97    public static final String ACTION_RENAME_GROUP = "renameGroup";
98    public static final String ACTION_DELETE_GROUP = "deleteGroup";
99    public static final String ACTION_UPDATE_GROUP = "updateGroup";
100    public static final String EXTRA_GROUP_ID = "groupId";
101    public static final String EXTRA_GROUP_LABEL = "groupLabel";
102    public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
103    public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
104
105    public static final String ACTION_SET_STARRED = "setStarred";
106    public static final String ACTION_DELETE_CONTACT = "delete";
107    public static final String EXTRA_CONTACT_URI = "contactUri";
108    public static final String EXTRA_STARRED_FLAG = "starred";
109
110    public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
111    public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
112    public static final String EXTRA_DATA_ID = "dataId";
113
114    public static final String ACTION_JOIN_CONTACTS = "joinContacts";
115    public static final String EXTRA_CONTACT_ID1 = "contactId1";
116    public static final String EXTRA_CONTACT_ID2 = "contactId2";
117    public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
118
119    public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
120    public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
121
122    public static final String ACTION_SET_RINGTONE = "setRingtone";
123    public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
124
125    private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
126        Data.MIMETYPE,
127        Data.IS_PRIMARY,
128        Data.DATA1,
129        Data.DATA2,
130        Data.DATA3,
131        Data.DATA4,
132        Data.DATA5,
133        Data.DATA6,
134        Data.DATA7,
135        Data.DATA8,
136        Data.DATA9,
137        Data.DATA10,
138        Data.DATA11,
139        Data.DATA12,
140        Data.DATA13,
141        Data.DATA14,
142        Data.DATA15
143    );
144
145    private static final int PERSIST_TRIES = 3;
146
147    public interface Listener {
148        public void onServiceCompleted(Intent callbackIntent);
149    }
150
151    private static final CopyOnWriteArrayList<Listener> sListeners =
152            new CopyOnWriteArrayList<Listener>();
153
154    private Handler mMainHandler;
155
156    public ContactSaveService() {
157        super(TAG);
158        setIntentRedelivery(true);
159        mMainHandler = new Handler(Looper.getMainLooper());
160    }
161
162    public static void registerListener(Listener listener) {
163        if (!(listener instanceof Activity)) {
164            throw new ClassCastException("Only activities can be registered to"
165                    + " receive callback from " + ContactSaveService.class.getName());
166        }
167        sListeners.add(0, listener);
168    }
169
170    public static void unregisterListener(Listener listener) {
171        sListeners.remove(listener);
172    }
173
174    @Override
175    public Object getSystemService(String name) {
176        Object service = super.getSystemService(name);
177        if (service != null) {
178            return service;
179        }
180
181        return getApplicationContext().getSystemService(name);
182    }
183
184    @Override
185    protected void onHandleIntent(Intent intent) {
186        // Call an appropriate method. If we're sure it affects how incoming phone calls are
187        // handled, then notify the fact to in-call screen.
188        String action = intent.getAction();
189        if (ACTION_NEW_RAW_CONTACT.equals(action)) {
190            createRawContact(intent);
191            CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
192        } else if (ACTION_SAVE_CONTACT.equals(action)) {
193            saveContact(intent);
194            CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
195        } else if (ACTION_CREATE_GROUP.equals(action)) {
196            createGroup(intent);
197        } else if (ACTION_RENAME_GROUP.equals(action)) {
198            renameGroup(intent);
199        } else if (ACTION_DELETE_GROUP.equals(action)) {
200            deleteGroup(intent);
201        } else if (ACTION_UPDATE_GROUP.equals(action)) {
202            updateGroup(intent);
203        } else if (ACTION_SET_STARRED.equals(action)) {
204            setStarred(intent);
205        } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
206            setSuperPrimary(intent);
207        } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
208            clearPrimary(intent);
209        } else if (ACTION_DELETE_CONTACT.equals(action)) {
210            deleteContact(intent);
211            CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
212        } else if (ACTION_JOIN_CONTACTS.equals(action)) {
213            joinContacts(intent);
214            CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
215        } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
216            setSendToVoicemail(intent);
217            CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
218        } else if (ACTION_SET_RINGTONE.equals(action)) {
219            setRingtone(intent);
220            CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
221        }
222    }
223
224    /**
225     * Creates an intent that can be sent to this service to create a new raw contact
226     * using data presented as a set of ContentValues.
227     */
228    public static Intent createNewRawContactIntent(Context context,
229            ArrayList<ContentValues> values, AccountWithDataSet account,
230            Class<? extends Activity> callbackActivity, String callbackAction) {
231        Intent serviceIntent = new Intent(
232                context, ContactSaveService.class);
233        serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
234        if (account != null) {
235            serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
236            serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
237            serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
238        }
239        serviceIntent.putParcelableArrayListExtra(
240                ContactSaveService.EXTRA_CONTENT_VALUES, values);
241
242        // Callback intent will be invoked by the service once the new contact is
243        // created.  The service will put the URI of the new contact as "data" on
244        // the callback intent.
245        Intent callbackIntent = new Intent(context, callbackActivity);
246        callbackIntent.setAction(callbackAction);
247        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
248        return serviceIntent;
249    }
250
251    private void createRawContact(Intent intent) {
252        String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
253        String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
254        String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
255        List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
256        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
257
258        ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
259        operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
260                .withValue(RawContacts.ACCOUNT_NAME, accountName)
261                .withValue(RawContacts.ACCOUNT_TYPE, accountType)
262                .withValue(RawContacts.DATA_SET, dataSet)
263                .build());
264
265        int size = valueList.size();
266        for (int i = 0; i < size; i++) {
267            ContentValues values = valueList.get(i);
268            values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
269            operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
270                    .withValueBackReference(Data.RAW_CONTACT_ID, 0)
271                    .withValues(values)
272                    .build());
273        }
274
275        ContentResolver resolver = getContentResolver();
276        ContentProviderResult[] results;
277        try {
278            results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
279        } catch (Exception e) {
280            throw new RuntimeException("Failed to store new contact", e);
281        }
282
283        Uri rawContactUri = results[0].uri;
284        callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
285
286        deliverCallback(callbackIntent);
287    }
288
289    /**
290     * Creates an intent that can be sent to this service to create a new raw contact
291     * using data presented as a set of ContentValues.
292     * This variant is more convenient to use when there is only one photo that can
293     * possibly be updated, as in the Contact Details screen.
294     * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
295     * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
296     */
297    public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
298            String saveModeExtraKey, int saveMode, boolean isProfile,
299            Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
300            Uri updatedPhotoPath) {
301        Bundle bundle = new Bundle();
302        bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
303        return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
304                callbackActivity, callbackAction, bundle);
305    }
306
307    /**
308     * Creates an intent that can be sent to this service to create a new raw contact
309     * using data presented as a set of ContentValues.
310     * This variant is used when multiple contacts' photos may be updated, as in the
311     * Contact Editor.
312     * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
313     */
314    public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
315            String saveModeExtraKey, int saveMode, boolean isProfile,
316            Class<? extends Activity> callbackActivity, String callbackAction,
317            Bundle updatedPhotos) {
318        Intent serviceIntent = new Intent(
319                context, ContactSaveService.class);
320        serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
321        serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
322        serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
323        if (updatedPhotos != null) {
324            serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
325        }
326
327        if (callbackActivity != null) {
328            // Callback intent will be invoked by the service once the contact is
329            // saved.  The service will put the URI of the new contact as "data" on
330            // the callback intent.
331            Intent callbackIntent = new Intent(context, callbackActivity);
332            callbackIntent.putExtra(saveModeExtraKey, saveMode);
333            callbackIntent.setAction(callbackAction);
334            serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
335        }
336        return serviceIntent;
337    }
338
339    private void saveContact(Intent intent) {
340        RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
341        boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
342        Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
343
344        // Trim any empty fields, and RawContacts, before persisting
345        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
346        RawContactModifier.trimEmpty(state, accountTypes);
347
348        Uri lookupUri = null;
349
350        final ContentResolver resolver = getContentResolver();
351        boolean succeeded = false;
352
353        // Keep track of the id of a newly raw-contact (if any... there can be at most one).
354        long insertedRawContactId = -1;
355
356        // Attempt to persist changes
357        int tries = 0;
358        while (tries++ < PERSIST_TRIES) {
359            try {
360                // Build operations and try applying
361                final ArrayList<ContentProviderOperation> diff = state.buildDiff();
362                if (DEBUG) {
363                    Log.v(TAG, "Content Provider Operations:");
364                    for (ContentProviderOperation operation : diff) {
365                        Log.v(TAG, operation.toString());
366                    }
367                }
368
369                ContentProviderResult[] results = null;
370                if (!diff.isEmpty()) {
371                    results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
372                }
373
374                final long rawContactId = getRawContactId(state, diff, results);
375                if (rawContactId == -1) {
376                    throw new IllegalStateException("Could not determine RawContact ID after save");
377                }
378                // We don't have to check to see if the value is still -1.  If we reach here,
379                // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
380                insertedRawContactId = getInsertedRawContactId(diff, results);
381                if (isProfile) {
382                    // Since the profile supports local raw contacts, which may have been completely
383                    // removed if all information was removed, we need to do a special query to
384                    // get the lookup URI for the profile contact (if it still exists).
385                    Cursor c = resolver.query(Profile.CONTENT_URI,
386                            new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
387                            null, null, null);
388                    try {
389                        if (c.moveToFirst()) {
390                            final long contactId = c.getLong(0);
391                            final String lookupKey = c.getString(1);
392                            lookupUri = Contacts.getLookupUri(contactId, lookupKey);
393                        }
394                    } finally {
395                        c.close();
396                    }
397                } else {
398                    final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
399                                    rawContactId);
400                    lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
401                }
402                Log.v(TAG, "Saved contact. New URI: " + lookupUri);
403
404                // We can change this back to false later, if we fail to save the contact photo.
405                succeeded = true;
406                break;
407
408            } catch (RemoteException e) {
409                // Something went wrong, bail without success
410                Log.e(TAG, "Problem persisting user edits", e);
411                break;
412
413            } catch (OperationApplicationException e) {
414                // Version consistency failed, re-parent change and try again
415                Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
416                final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
417                boolean first = true;
418                final int count = state.size();
419                for (int i = 0; i < count; i++) {
420                    Long rawContactId = state.getRawContactId(i);
421                    if (rawContactId != null && rawContactId != -1) {
422                        if (!first) {
423                            sb.append(',');
424                        }
425                        sb.append(rawContactId);
426                        first = false;
427                    }
428                }
429                sb.append(")");
430
431                if (first) {
432                    throw new IllegalStateException("Version consistency failed for a new contact");
433                }
434
435                final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
436                        isProfile
437                                ? RawContactsEntity.PROFILE_CONTENT_URI
438                                : RawContactsEntity.CONTENT_URI,
439                        resolver, sb.toString(), null, null);
440                state = RawContactDeltaList.mergeAfter(newState, state);
441
442                // Update the new state to use profile URIs if appropriate.
443                if (isProfile) {
444                    for (RawContactDelta delta : state) {
445                        delta.setProfileQueryUri();
446                    }
447                }
448            }
449        }
450
451        // Now save any updated photos.  We do this at the end to ensure that
452        // the ContactProvider already knows about newly-created contacts.
453        if (updatedPhotos != null) {
454            for (String key : updatedPhotos.keySet()) {
455                Uri photoUri = updatedPhotos.getParcelable(key);
456                long rawContactId = Long.parseLong(key);
457
458                // If the raw-contact ID is negative, we are saving a new raw-contact;
459                // replace the bogus ID with the new one that we actually saved the contact at.
460                if (rawContactId < 0) {
461                    rawContactId = insertedRawContactId;
462                    if (rawContactId == -1) {
463                        throw new IllegalStateException(
464                                "Could not determine RawContact ID for image insertion");
465                    }
466                }
467
468                if (!saveUpdatedPhoto(rawContactId, photoUri)) succeeded = false;
469            }
470        }
471
472        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
473        if (callbackIntent != null) {
474            if (succeeded) {
475                // Mark the intent to indicate that the save was successful (even if the lookup URI
476                // is now null).  For local contacts or the local profile, it's possible that the
477                // save triggered removal of the contact, so no lookup URI would exist..
478                callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
479            }
480            callbackIntent.setData(lookupUri);
481            deliverCallback(callbackIntent);
482        }
483    }
484
485    /**
486     * Save updated photo for the specified raw-contact.
487     * @return true for success, false for failure
488     */
489    private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
490        final Uri outputUri = Uri.withAppendedPath(
491                ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
492                RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
493
494        return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
495    }
496
497    /**
498     * Find the ID of an existing or newly-inserted raw-contact.  If none exists, return -1.
499     */
500    private long getRawContactId(RawContactDeltaList state,
501            final ArrayList<ContentProviderOperation> diff,
502            final ContentProviderResult[] results) {
503        long existingRawContactId = state.findRawContactId();
504        if (existingRawContactId != -1) {
505            return existingRawContactId;
506        }
507
508        return getInsertedRawContactId(diff, results);
509    }
510
511    /**
512     * Find the ID of a newly-inserted raw-contact.  If none exists, return -1.
513     */
514    private long getInsertedRawContactId(
515            final ArrayList<ContentProviderOperation> diff,
516            final ContentProviderResult[] results) {
517        final int diffSize = diff.size();
518        for (int i = 0; i < diffSize; i++) {
519            ContentProviderOperation operation = diff.get(i);
520            if (operation.getType() == ContentProviderOperation.TYPE_INSERT
521                    && operation.getUri().getEncodedPath().contains(
522                            RawContacts.CONTENT_URI.getEncodedPath())) {
523                return ContentUris.parseId(results[i].uri);
524            }
525        }
526        return -1;
527    }
528
529    /**
530     * Creates an intent that can be sent to this service to create a new group as
531     * well as add new members at the same time.
532     *
533     * @param context of the application
534     * @param account in which the group should be created
535     * @param label is the name of the group (cannot be null)
536     * @param rawContactsToAdd is an array of raw contact IDs for contacts that
537     *            should be added to the group
538     * @param callbackActivity is the activity to send the callback intent to
539     * @param callbackAction is the intent action for the callback intent
540     */
541    public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
542            String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
543            String callbackAction) {
544        Intent serviceIntent = new Intent(context, ContactSaveService.class);
545        serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
546        serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
547        serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
548        serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
549        serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
550        serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
551
552        // Callback intent will be invoked by the service once the new group is
553        // created.
554        Intent callbackIntent = new Intent(context, callbackActivity);
555        callbackIntent.setAction(callbackAction);
556        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
557
558        return serviceIntent;
559    }
560
561    private void createGroup(Intent intent) {
562        String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
563        String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
564        String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
565        String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
566        final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
567
568        ContentValues values = new ContentValues();
569        values.put(Groups.ACCOUNT_TYPE, accountType);
570        values.put(Groups.ACCOUNT_NAME, accountName);
571        values.put(Groups.DATA_SET, dataSet);
572        values.put(Groups.TITLE, label);
573
574        final ContentResolver resolver = getContentResolver();
575
576        // Create the new group
577        final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
578
579        // If there's no URI, then the insertion failed. Abort early because group members can't be
580        // added if the group doesn't exist
581        if (groupUri == null) {
582            Log.e(TAG, "Couldn't create group with label " + label);
583            return;
584        }
585
586        // Add new group members
587        addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
588
589        // TODO: Move this into the contact editor where it belongs. This needs to be integrated
590        // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
591        values.clear();
592        values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
593        values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
594
595        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
596        callbackIntent.setData(groupUri);
597        // TODO: This can be taken out when the above TODO is addressed
598        callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
599        deliverCallback(callbackIntent);
600    }
601
602    /**
603     * Creates an intent that can be sent to this service to rename a group.
604     */
605    public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
606            Class<? extends Activity> callbackActivity, String callbackAction) {
607        Intent serviceIntent = new Intent(context, ContactSaveService.class);
608        serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
609        serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
610        serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
611
612        // Callback intent will be invoked by the service once the group is renamed.
613        Intent callbackIntent = new Intent(context, callbackActivity);
614        callbackIntent.setAction(callbackAction);
615        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
616
617        return serviceIntent;
618    }
619
620    private void renameGroup(Intent intent) {
621        long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
622        String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
623
624        if (groupId == -1) {
625            Log.e(TAG, "Invalid arguments for renameGroup request");
626            return;
627        }
628
629        ContentValues values = new ContentValues();
630        values.put(Groups.TITLE, label);
631        final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
632        getContentResolver().update(groupUri, values, null, null);
633
634        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
635        callbackIntent.setData(groupUri);
636        deliverCallback(callbackIntent);
637    }
638
639    /**
640     * Creates an intent that can be sent to this service to delete a group.
641     */
642    public static Intent createGroupDeletionIntent(Context context, long groupId) {
643        Intent serviceIntent = new Intent(context, ContactSaveService.class);
644        serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
645        serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
646        return serviceIntent;
647    }
648
649    private void deleteGroup(Intent intent) {
650        long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
651        if (groupId == -1) {
652            Log.e(TAG, "Invalid arguments for deleteGroup request");
653            return;
654        }
655
656        getContentResolver().delete(
657                ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
658    }
659
660    /**
661     * Creates an intent that can be sent to this service to rename a group as
662     * well as add and remove members from the group.
663     *
664     * @param context of the application
665     * @param groupId of the group that should be modified
666     * @param newLabel is the updated name of the group (can be null if the name
667     *            should not be updated)
668     * @param rawContactsToAdd is an array of raw contact IDs for contacts that
669     *            should be added to the group
670     * @param rawContactsToRemove is an array of raw contact IDs for contacts
671     *            that should be removed from the group
672     * @param callbackActivity is the activity to send the callback intent to
673     * @param callbackAction is the intent action for the callback intent
674     */
675    public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
676            long[] rawContactsToAdd, long[] rawContactsToRemove,
677            Class<? extends Activity> callbackActivity, String callbackAction) {
678        Intent serviceIntent = new Intent(context, ContactSaveService.class);
679        serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
680        serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
681        serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
682        serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
683        serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
684                rawContactsToRemove);
685
686        // Callback intent will be invoked by the service once the group is updated
687        Intent callbackIntent = new Intent(context, callbackActivity);
688        callbackIntent.setAction(callbackAction);
689        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
690
691        return serviceIntent;
692    }
693
694    private void updateGroup(Intent intent) {
695        long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
696        String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
697        long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
698        long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
699
700        if (groupId == -1) {
701            Log.e(TAG, "Invalid arguments for updateGroup request");
702            return;
703        }
704
705        final ContentResolver resolver = getContentResolver();
706        final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
707
708        // Update group name if necessary
709        if (label != null) {
710            ContentValues values = new ContentValues();
711            values.put(Groups.TITLE, label);
712            resolver.update(groupUri, values, null, null);
713        }
714
715        // Add and remove members if necessary
716        addMembersToGroup(resolver, rawContactsToAdd, groupId);
717        removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
718
719        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
720        callbackIntent.setData(groupUri);
721        deliverCallback(callbackIntent);
722    }
723
724    private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
725            long groupId) {
726        if (rawContactsToAdd == null) {
727            return;
728        }
729        for (long rawContactId : rawContactsToAdd) {
730            try {
731                final ArrayList<ContentProviderOperation> rawContactOperations =
732                        new ArrayList<ContentProviderOperation>();
733
734                // Build an assert operation to ensure the contact is not already in the group
735                final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
736                        .newAssertQuery(Data.CONTENT_URI);
737                assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
738                        Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
739                        new String[] { String.valueOf(rawContactId),
740                        GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
741                assertBuilder.withExpectedCount(0);
742                rawContactOperations.add(assertBuilder.build());
743
744                // Build an insert operation to add the contact to the group
745                final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
746                        .newInsert(Data.CONTENT_URI);
747                insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
748                insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
749                insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
750                rawContactOperations.add(insertBuilder.build());
751
752                if (DEBUG) {
753                    for (ContentProviderOperation operation : rawContactOperations) {
754                        Log.v(TAG, operation.toString());
755                    }
756                }
757
758                // Apply batch
759                if (!rawContactOperations.isEmpty()) {
760                    resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
761                }
762            } catch (RemoteException e) {
763                // Something went wrong, bail without success
764                Log.e(TAG, "Problem persisting user edits for raw contact ID " +
765                        String.valueOf(rawContactId), e);
766            } catch (OperationApplicationException e) {
767                // The assert could have failed because the contact is already in the group,
768                // just continue to the next contact
769                Log.w(TAG, "Assert failed in adding raw contact ID " +
770                        String.valueOf(rawContactId) + ". Already exists in group " +
771                        String.valueOf(groupId), e);
772            }
773        }
774    }
775
776    private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
777            long groupId) {
778        if (rawContactsToRemove == null) {
779            return;
780        }
781        for (long rawContactId : rawContactsToRemove) {
782            // Apply the delete operation on the data row for the given raw contact's
783            // membership in the given group. If no contact matches the provided selection, then
784            // nothing will be done. Just continue to the next contact.
785            resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
786                    Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
787                    new String[] { String.valueOf(rawContactId),
788                    GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
789        }
790    }
791
792    /**
793     * Creates an intent that can be sent to this service to star or un-star a contact.
794     */
795    public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
796        Intent serviceIntent = new Intent(context, ContactSaveService.class);
797        serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
798        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
799        serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
800
801        return serviceIntent;
802    }
803
804    private void setStarred(Intent intent) {
805        Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
806        boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
807        if (contactUri == null) {
808            Log.e(TAG, "Invalid arguments for setStarred request");
809            return;
810        }
811
812        final ContentValues values = new ContentValues(1);
813        values.put(Contacts.STARRED, value);
814        getContentResolver().update(contactUri, values, null, null);
815
816        // Undemote the contact if necessary
817        final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
818                null, null, null);
819        try {
820            if (c.moveToFirst()) {
821                final long id = c.getLong(0);
822
823                // Don't bother undemoting if this contact is the user's profile.
824                if (id < Profile.MIN_ID) {
825                    values.clear();
826                    values.put(String.valueOf(id), PinnedPositions.UNDEMOTE);
827                    getContentResolver().update(PinnedPositions.UPDATE_URI, values, null, null);
828                }
829            }
830        } finally {
831            c.close();
832        }
833    }
834
835    /**
836     * Creates an intent that can be sent to this service to set the redirect to voicemail.
837     */
838    public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
839            boolean value) {
840        Intent serviceIntent = new Intent(context, ContactSaveService.class);
841        serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
842        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
843        serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
844
845        return serviceIntent;
846    }
847
848    private void setSendToVoicemail(Intent intent) {
849        Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
850        boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
851        if (contactUri == null) {
852            Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
853            return;
854        }
855
856        final ContentValues values = new ContentValues(1);
857        values.put(Contacts.SEND_TO_VOICEMAIL, value);
858        getContentResolver().update(contactUri, values, null, null);
859    }
860
861    /**
862     * Creates an intent that can be sent to this service to save the contact's ringtone.
863     */
864    public static Intent createSetRingtone(Context context, Uri contactUri,
865            String value) {
866        Intent serviceIntent = new Intent(context, ContactSaveService.class);
867        serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
868        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
869        serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
870
871        return serviceIntent;
872    }
873
874    private void setRingtone(Intent intent) {
875        Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
876        String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
877        if (contactUri == null) {
878            Log.e(TAG, "Invalid arguments for setRingtone");
879            return;
880        }
881        ContentValues values = new ContentValues(1);
882        values.put(Contacts.CUSTOM_RINGTONE, value);
883        getContentResolver().update(contactUri, values, null, null);
884    }
885
886    /**
887     * Creates an intent that sets the selected data item as super primary (default)
888     */
889    public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
890        Intent serviceIntent = new Intent(context, ContactSaveService.class);
891        serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
892        serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
893        return serviceIntent;
894    }
895
896    private void setSuperPrimary(Intent intent) {
897        long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
898        if (dataId == -1) {
899            Log.e(TAG, "Invalid arguments for setSuperPrimary request");
900            return;
901        }
902
903        ContactUpdateUtils.setSuperPrimary(this, dataId);
904    }
905
906    /**
907     * Creates an intent that clears the primary flag of all data items that belong to the same
908     * raw_contact as the given data item. Will only clear, if the data item was primary before
909     * this call
910     */
911    public static Intent createClearPrimaryIntent(Context context, long dataId) {
912        Intent serviceIntent = new Intent(context, ContactSaveService.class);
913        serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
914        serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
915        return serviceIntent;
916    }
917
918    private void clearPrimary(Intent intent) {
919        long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
920        if (dataId == -1) {
921            Log.e(TAG, "Invalid arguments for clearPrimary request");
922            return;
923        }
924
925        // Update the primary values in the data record.
926        ContentValues values = new ContentValues(1);
927        values.put(Data.IS_SUPER_PRIMARY, 0);
928        values.put(Data.IS_PRIMARY, 0);
929
930        getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
931                values, null, null);
932    }
933
934    /**
935     * Creates an intent that can be sent to this service to delete a contact.
936     */
937    public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
938        Intent serviceIntent = new Intent(context, ContactSaveService.class);
939        serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
940        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
941        return serviceIntent;
942    }
943
944    private void deleteContact(Intent intent) {
945        Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
946        if (contactUri == null) {
947            Log.e(TAG, "Invalid arguments for deleteContact request");
948            return;
949        }
950
951        getContentResolver().delete(contactUri, null, null);
952    }
953
954    /**
955     * Creates an intent that can be sent to this service to join two contacts.
956     */
957    public static Intent createJoinContactsIntent(Context context, long contactId1,
958            long contactId2, boolean contactWritable,
959            Class<? extends Activity> callbackActivity, String callbackAction) {
960        Intent serviceIntent = new Intent(context, ContactSaveService.class);
961        serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
962        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
963        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
964        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
965
966        // Callback intent will be invoked by the service once the contacts are joined.
967        Intent callbackIntent = new Intent(context, callbackActivity);
968        callbackIntent.setAction(callbackAction);
969        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
970
971        return serviceIntent;
972    }
973
974
975    private interface JoinContactQuery {
976        String[] PROJECTION = {
977                RawContacts._ID,
978                RawContacts.CONTACT_ID,
979                RawContacts.NAME_VERIFIED,
980                RawContacts.DISPLAY_NAME_SOURCE,
981        };
982
983        String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
984
985        int _ID = 0;
986        int CONTACT_ID = 1;
987        int NAME_VERIFIED = 2;
988        int DISPLAY_NAME_SOURCE = 3;
989    }
990
991    private void joinContacts(Intent intent) {
992        long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
993        long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
994        boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
995        if (contactId1 == -1 || contactId2 == -1) {
996            Log.e(TAG, "Invalid arguments for joinContacts request");
997            return;
998        }
999
1000        final ContentResolver resolver = getContentResolver();
1001
1002        // Load raw contact IDs for all raw contacts involved - currently edited and selected
1003        // in the join UIs
1004        Cursor c = resolver.query(RawContacts.CONTENT_URI,
1005                JoinContactQuery.PROJECTION,
1006                JoinContactQuery.SELECTION,
1007                new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1008
1009        long rawContactIds[];
1010        long verifiedNameRawContactId = -1;
1011        try {
1012            if (c.getCount() == 0) {
1013                return;
1014            }
1015            int maxDisplayNameSource = -1;
1016            rawContactIds = new long[c.getCount()];
1017            for (int i = 0; i < rawContactIds.length; i++) {
1018                c.moveToPosition(i);
1019                long rawContactId = c.getLong(JoinContactQuery._ID);
1020                rawContactIds[i] = rawContactId;
1021                int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1022                if (nameSource > maxDisplayNameSource) {
1023                    maxDisplayNameSource = nameSource;
1024                }
1025            }
1026
1027            // Find an appropriate display name for the joined contact:
1028            // if should have a higher DisplayNameSource or be the name
1029            // of the original contact that we are joining with another.
1030            if (writable) {
1031                for (int i = 0; i < rawContactIds.length; i++) {
1032                    c.moveToPosition(i);
1033                    if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1034                        int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1035                        if (nameSource == maxDisplayNameSource
1036                                && (verifiedNameRawContactId == -1
1037                                        || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1038                            verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1039                        }
1040                    }
1041                }
1042            }
1043        } finally {
1044            c.close();
1045        }
1046
1047        // For each pair of raw contacts, insert an aggregation exception
1048        ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1049        for (int i = 0; i < rawContactIds.length; i++) {
1050            for (int j = 0; j < rawContactIds.length; j++) {
1051                if (i != j) {
1052                    buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1053                }
1054            }
1055        }
1056
1057        // Mark the original contact as "name verified" to make sure that the contact
1058        // display name does not change as a result of the join
1059        if (verifiedNameRawContactId != -1) {
1060            Builder builder = ContentProviderOperation.newUpdate(
1061                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1062            builder.withValue(RawContacts.NAME_VERIFIED, 1);
1063            operations.add(builder.build());
1064        }
1065
1066        boolean success = false;
1067        // Apply all aggregation exceptions as one batch
1068        try {
1069            resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1070            showToast(R.string.contactsJoinedMessage);
1071            success = true;
1072        } catch (RemoteException e) {
1073            Log.e(TAG, "Failed to apply aggregation exception batch", e);
1074            showToast(R.string.contactSavedErrorToast);
1075        } catch (OperationApplicationException e) {
1076            Log.e(TAG, "Failed to apply aggregation exception batch", e);
1077            showToast(R.string.contactSavedErrorToast);
1078        }
1079
1080        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1081        if (success) {
1082            Uri uri = RawContacts.getContactLookupUri(resolver,
1083                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1084            callbackIntent.setData(uri);
1085        }
1086        deliverCallback(callbackIntent);
1087    }
1088
1089    /**
1090     * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1091     */
1092    private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1093            long rawContactId1, long rawContactId2) {
1094        Builder builder =
1095                ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1096        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1097        builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1098        builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1099        operations.add(builder.build());
1100    }
1101
1102    /**
1103     * Shows a toast on the UI thread.
1104     */
1105    private void showToast(final int message) {
1106        mMainHandler.post(new Runnable() {
1107
1108            @Override
1109            public void run() {
1110                Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1111            }
1112        });
1113    }
1114
1115    private void deliverCallback(final Intent callbackIntent) {
1116        mMainHandler.post(new Runnable() {
1117
1118            @Override
1119            public void run() {
1120                deliverCallbackOnUiThread(callbackIntent);
1121            }
1122        });
1123    }
1124
1125    void deliverCallbackOnUiThread(final Intent callbackIntent) {
1126        // TODO: this assumes that if there are multiple instances of the same
1127        // activity registered, the last one registered is the one waiting for
1128        // the callback. Validity of this assumption needs to be verified.
1129        for (Listener listener : sListeners) {
1130            if (callbackIntent.getComponent().equals(
1131                    ((Activity) listener).getIntent().getComponent())) {
1132                listener.onServiceCompleted(callbackIntent);
1133                return;
1134            }
1135        }
1136    }
1137}
1138