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.ContactPhotoUtils;
57
58import com.google.common.collect.Lists;
59import com.google.common.collect.Sets;
60
61import java.io.File;
62import java.io.FileInputStream;
63import java.io.FileOutputStream;
64import java.io.IOException;
65import java.io.InputStream;
66import java.util.ArrayList;
67import java.util.HashSet;
68import java.util.List;
69import java.util.concurrent.CopyOnWriteArrayList;
70
71/**
72 * A service responsible for saving changes to the content provider.
73 */
74public class ContactSaveService extends IntentService {
75    private static final String TAG = "ContactSaveService";
76
77    /** Set to true in order to view logs on content provider operations */
78    private static final boolean DEBUG = false;
79
80    public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
81
82    public static final String EXTRA_ACCOUNT_NAME = "accountName";
83    public static final String EXTRA_ACCOUNT_TYPE = "accountType";
84    public static final String EXTRA_DATA_SET = "dataSet";
85    public static final String EXTRA_CONTENT_VALUES = "contentValues";
86    public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
87
88    public static final String ACTION_SAVE_CONTACT = "saveContact";
89    public static final String EXTRA_CONTACT_STATE = "state";
90    public static final String EXTRA_SAVE_MODE = "saveMode";
91    public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
92    public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
93    public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
94
95    public static final String ACTION_CREATE_GROUP = "createGroup";
96    public static final String ACTION_RENAME_GROUP = "renameGroup";
97    public static final String ACTION_DELETE_GROUP = "deleteGroup";
98    public static final String ACTION_UPDATE_GROUP = "updateGroup";
99    public static final String EXTRA_GROUP_ID = "groupId";
100    public static final String EXTRA_GROUP_LABEL = "groupLabel";
101    public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
102    public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
103
104    public static final String ACTION_SET_STARRED = "setStarred";
105    public static final String ACTION_DELETE_CONTACT = "delete";
106    public static final String EXTRA_CONTACT_URI = "contactUri";
107    public static final String EXTRA_STARRED_FLAG = "starred";
108
109    public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
110    public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
111    public static final String EXTRA_DATA_ID = "dataId";
112
113    public static final String ACTION_JOIN_CONTACTS = "joinContacts";
114    public static final String EXTRA_CONTACT_ID1 = "contactId1";
115    public static final String EXTRA_CONTACT_ID2 = "contactId2";
116    public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
117
118    public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
119    public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
120
121    public static final String ACTION_SET_RINGTONE = "setRingtone";
122    public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
123
124    private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
125        Data.MIMETYPE,
126        Data.IS_PRIMARY,
127        Data.DATA1,
128        Data.DATA2,
129        Data.DATA3,
130        Data.DATA4,
131        Data.DATA5,
132        Data.DATA6,
133        Data.DATA7,
134        Data.DATA8,
135        Data.DATA9,
136        Data.DATA10,
137        Data.DATA11,
138        Data.DATA12,
139        Data.DATA13,
140        Data.DATA14,
141        Data.DATA15
142    );
143
144    private static final int PERSIST_TRIES = 3;
145
146    public interface Listener {
147        public void onServiceCompleted(Intent callbackIntent);
148    }
149
150    private static final CopyOnWriteArrayList<Listener> sListeners =
151            new CopyOnWriteArrayList<Listener>();
152
153    private Handler mMainHandler;
154
155    public ContactSaveService() {
156        super(TAG);
157        setIntentRedelivery(true);
158        mMainHandler = new Handler(Looper.getMainLooper());
159    }
160
161    public static void registerListener(Listener listener) {
162        if (!(listener instanceof Activity)) {
163            throw new ClassCastException("Only activities can be registered to"
164                    + " receive callback from " + ContactSaveService.class.getName());
165        }
166        sListeners.add(0, listener);
167    }
168
169    public static void unregisterListener(Listener listener) {
170        sListeners.remove(listener);
171    }
172
173    @Override
174    public Object getSystemService(String name) {
175        Object service = super.getSystemService(name);
176        if (service != null) {
177            return service;
178        }
179
180        return getApplicationContext().getSystemService(name);
181    }
182
183    @Override
184    protected void onHandleIntent(Intent intent) {
185        // Call an appropriate method. If we're sure it affects how incoming phone calls are
186        // handled, then notify the fact to in-call screen.
187        String action = intent.getAction();
188        if (ACTION_NEW_RAW_CONTACT.equals(action)) {
189            createRawContact(intent);
190        } else if (ACTION_SAVE_CONTACT.equals(action)) {
191            saveContact(intent);
192        } else if (ACTION_CREATE_GROUP.equals(action)) {
193            createGroup(intent);
194        } else if (ACTION_RENAME_GROUP.equals(action)) {
195            renameGroup(intent);
196        } else if (ACTION_DELETE_GROUP.equals(action)) {
197            deleteGroup(intent);
198        } else if (ACTION_UPDATE_GROUP.equals(action)) {
199            updateGroup(intent);
200        } else if (ACTION_SET_STARRED.equals(action)) {
201            setStarred(intent);
202        } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
203            setSuperPrimary(intent);
204        } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
205            clearPrimary(intent);
206        } else if (ACTION_DELETE_CONTACT.equals(action)) {
207            deleteContact(intent);
208        } else if (ACTION_JOIN_CONTACTS.equals(action)) {
209            joinContacts(intent);
210        } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
211            setSendToVoicemail(intent);
212        } else if (ACTION_SET_RINGTONE.equals(action)) {
213            setRingtone(intent);
214        }
215    }
216
217    /**
218     * Creates an intent that can be sent to this service to create a new raw contact
219     * using data presented as a set of ContentValues.
220     */
221    public static Intent createNewRawContactIntent(Context context,
222            ArrayList<ContentValues> values, AccountWithDataSet account,
223            Class<? extends Activity> callbackActivity, String callbackAction) {
224        Intent serviceIntent = new Intent(
225                context, ContactSaveService.class);
226        serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
227        if (account != null) {
228            serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
229            serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
230            serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
231        }
232        serviceIntent.putParcelableArrayListExtra(
233                ContactSaveService.EXTRA_CONTENT_VALUES, values);
234
235        // Callback intent will be invoked by the service once the new contact is
236        // created.  The service will put the URI of the new contact as "data" on
237        // the callback intent.
238        Intent callbackIntent = new Intent(context, callbackActivity);
239        callbackIntent.setAction(callbackAction);
240        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
241        return serviceIntent;
242    }
243
244    private void createRawContact(Intent intent) {
245        String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
246        String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
247        String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
248        List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
249        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
250
251        ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
252        operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
253                .withValue(RawContacts.ACCOUNT_NAME, accountName)
254                .withValue(RawContacts.ACCOUNT_TYPE, accountType)
255                .withValue(RawContacts.DATA_SET, dataSet)
256                .build());
257
258        int size = valueList.size();
259        for (int i = 0; i < size; i++) {
260            ContentValues values = valueList.get(i);
261            values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
262            operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
263                    .withValueBackReference(Data.RAW_CONTACT_ID, 0)
264                    .withValues(values)
265                    .build());
266        }
267
268        ContentResolver resolver = getContentResolver();
269        ContentProviderResult[] results;
270        try {
271            results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
272        } catch (Exception e) {
273            throw new RuntimeException("Failed to store new contact", e);
274        }
275
276        Uri rawContactUri = results[0].uri;
277        callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
278
279        deliverCallback(callbackIntent);
280    }
281
282    /**
283     * Creates an intent that can be sent to this service to create a new raw contact
284     * using data presented as a set of ContentValues.
285     * This variant is more convenient to use when there is only one photo that can
286     * possibly be updated, as in the Contact Details screen.
287     * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
288     * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
289     */
290    public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
291            String saveModeExtraKey, int saveMode, boolean isProfile,
292            Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
293            Uri updatedPhotoPath) {
294        Bundle bundle = new Bundle();
295        bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
296        return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
297                callbackActivity, callbackAction, bundle);
298    }
299
300    /**
301     * Creates an intent that can be sent to this service to create a new raw contact
302     * using data presented as a set of ContentValues.
303     * This variant is used when multiple contacts' photos may be updated, as in the
304     * Contact Editor.
305     * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
306     */
307    public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
308            String saveModeExtraKey, int saveMode, boolean isProfile,
309            Class<? extends Activity> callbackActivity, String callbackAction,
310            Bundle updatedPhotos) {
311        Intent serviceIntent = new Intent(
312                context, ContactSaveService.class);
313        serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
314        serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
315        serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
316        if (updatedPhotos != null) {
317            serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
318        }
319
320        if (callbackActivity != null) {
321            // Callback intent will be invoked by the service once the contact is
322            // saved.  The service will put the URI of the new contact as "data" on
323            // the callback intent.
324            Intent callbackIntent = new Intent(context, callbackActivity);
325            callbackIntent.putExtra(saveModeExtraKey, saveMode);
326            callbackIntent.setAction(callbackAction);
327            serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
328        }
329        return serviceIntent;
330    }
331
332    private void saveContact(Intent intent) {
333        RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
334        boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
335        Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
336
337        // Trim any empty fields, and RawContacts, before persisting
338        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
339        RawContactModifier.trimEmpty(state, accountTypes);
340
341        Uri lookupUri = null;
342
343        final ContentResolver resolver = getContentResolver();
344        boolean succeeded = false;
345
346        // Keep track of the id of a newly raw-contact (if any... there can be at most one).
347        long insertedRawContactId = -1;
348
349        // Attempt to persist changes
350        int tries = 0;
351        while (tries++ < PERSIST_TRIES) {
352            try {
353                // Build operations and try applying
354                final ArrayList<ContentProviderOperation> diff = state.buildDiff();
355                if (DEBUG) {
356                    Log.v(TAG, "Content Provider Operations:");
357                    for (ContentProviderOperation operation : diff) {
358                        Log.v(TAG, operation.toString());
359                    }
360                }
361
362                ContentProviderResult[] results = null;
363                if (!diff.isEmpty()) {
364                    results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
365                }
366
367                final long rawContactId = getRawContactId(state, diff, results);
368                if (rawContactId == -1) {
369                    throw new IllegalStateException("Could not determine RawContact ID after save");
370                }
371                // We don't have to check to see if the value is still -1.  If we reach here,
372                // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
373                insertedRawContactId = getInsertedRawContactId(diff, results);
374                if (isProfile) {
375                    // Since the profile supports local raw contacts, which may have been completely
376                    // removed if all information was removed, we need to do a special query to
377                    // get the lookup URI for the profile contact (if it still exists).
378                    Cursor c = resolver.query(Profile.CONTENT_URI,
379                            new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
380                            null, null, null);
381                    try {
382                        if (c.moveToFirst()) {
383                            final long contactId = c.getLong(0);
384                            final String lookupKey = c.getString(1);
385                            lookupUri = Contacts.getLookupUri(contactId, lookupKey);
386                        }
387                    } finally {
388                        c.close();
389                    }
390                } else {
391                    final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
392                                    rawContactId);
393                    lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
394                }
395                Log.v(TAG, "Saved contact. New URI: " + lookupUri);
396
397                // We can change this back to false later, if we fail to save the contact photo.
398                succeeded = true;
399                break;
400
401            } catch (RemoteException e) {
402                // Something went wrong, bail without success
403                Log.e(TAG, "Problem persisting user edits", e);
404                break;
405
406            } catch (IllegalArgumentException e) {
407                // This is thrown by applyBatch on malformed requests
408                Log.e(TAG, "Problem persisting user edits", e);
409                showToast(R.string.contactSavedErrorToast);
410                break;
411
412            } catch (OperationApplicationException e) {
413                // Version consistency failed, re-parent change and try again
414                Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
415                final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
416                boolean first = true;
417                final int count = state.size();
418                for (int i = 0; i < count; i++) {
419                    Long rawContactId = state.getRawContactId(i);
420                    if (rawContactId != null && rawContactId != -1) {
421                        if (!first) {
422                            sb.append(',');
423                        }
424                        sb.append(rawContactId);
425                        first = false;
426                    }
427                }
428                sb.append(")");
429
430                if (first) {
431                    throw new IllegalStateException(
432                            "Version consistency failed for a new contact", e);
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                    getContentResolver().call(ContactsContract.AUTHORITY_URI,
826                            PinnedPositions.UNDEMOTE_METHOD, String.valueOf(id), null);
827                }
828            }
829        } finally {
830            c.close();
831        }
832    }
833
834    /**
835     * Creates an intent that can be sent to this service to set the redirect to voicemail.
836     */
837    public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
838            boolean value) {
839        Intent serviceIntent = new Intent(context, ContactSaveService.class);
840        serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
841        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
842        serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
843
844        return serviceIntent;
845    }
846
847    private void setSendToVoicemail(Intent intent) {
848        Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
849        boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
850        if (contactUri == null) {
851            Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
852            return;
853        }
854
855        final ContentValues values = new ContentValues(1);
856        values.put(Contacts.SEND_TO_VOICEMAIL, value);
857        getContentResolver().update(contactUri, values, null, null);
858    }
859
860    /**
861     * Creates an intent that can be sent to this service to save the contact's ringtone.
862     */
863    public static Intent createSetRingtone(Context context, Uri contactUri,
864            String value) {
865        Intent serviceIntent = new Intent(context, ContactSaveService.class);
866        serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
867        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
868        serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
869
870        return serviceIntent;
871    }
872
873    private void setRingtone(Intent intent) {
874        Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
875        String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
876        if (contactUri == null) {
877            Log.e(TAG, "Invalid arguments for setRingtone");
878            return;
879        }
880        ContentValues values = new ContentValues(1);
881        values.put(Contacts.CUSTOM_RINGTONE, value);
882        getContentResolver().update(contactUri, values, null, null);
883    }
884
885    /**
886     * Creates an intent that sets the selected data item as super primary (default)
887     */
888    public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
889        Intent serviceIntent = new Intent(context, ContactSaveService.class);
890        serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
891        serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
892        return serviceIntent;
893    }
894
895    private void setSuperPrimary(Intent intent) {
896        long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
897        if (dataId == -1) {
898            Log.e(TAG, "Invalid arguments for setSuperPrimary request");
899            return;
900        }
901
902        ContactUpdateUtils.setSuperPrimary(this, dataId);
903    }
904
905    /**
906     * Creates an intent that clears the primary flag of all data items that belong to the same
907     * raw_contact as the given data item. Will only clear, if the data item was primary before
908     * this call
909     */
910    public static Intent createClearPrimaryIntent(Context context, long dataId) {
911        Intent serviceIntent = new Intent(context, ContactSaveService.class);
912        serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
913        serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
914        return serviceIntent;
915    }
916
917    private void clearPrimary(Intent intent) {
918        long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
919        if (dataId == -1) {
920            Log.e(TAG, "Invalid arguments for clearPrimary request");
921            return;
922        }
923
924        // Update the primary values in the data record.
925        ContentValues values = new ContentValues(1);
926        values.put(Data.IS_SUPER_PRIMARY, 0);
927        values.put(Data.IS_PRIMARY, 0);
928
929        getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
930                values, null, null);
931    }
932
933    /**
934     * Creates an intent that can be sent to this service to delete a contact.
935     */
936    public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
937        Intent serviceIntent = new Intent(context, ContactSaveService.class);
938        serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
939        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
940        return serviceIntent;
941    }
942
943    private void deleteContact(Intent intent) {
944        Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
945        if (contactUri == null) {
946            Log.e(TAG, "Invalid arguments for deleteContact request");
947            return;
948        }
949
950        getContentResolver().delete(contactUri, null, null);
951    }
952
953    /**
954     * Creates an intent that can be sent to this service to join two contacts.
955     */
956    public static Intent createJoinContactsIntent(Context context, long contactId1,
957            long contactId2, boolean contactWritable,
958            Class<? extends Activity> callbackActivity, String callbackAction) {
959        Intent serviceIntent = new Intent(context, ContactSaveService.class);
960        serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
961        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
962        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
963        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
964
965        // Callback intent will be invoked by the service once the contacts are joined.
966        Intent callbackIntent = new Intent(context, callbackActivity);
967        callbackIntent.setAction(callbackAction);
968        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
969
970        return serviceIntent;
971    }
972
973
974    private interface JoinContactQuery {
975        String[] PROJECTION = {
976                RawContacts._ID,
977                RawContacts.CONTACT_ID,
978                RawContacts.NAME_VERIFIED,
979                RawContacts.DISPLAY_NAME_SOURCE,
980        };
981
982        String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
983
984        int _ID = 0;
985        int CONTACT_ID = 1;
986        int NAME_VERIFIED = 2;
987        int DISPLAY_NAME_SOURCE = 3;
988    }
989
990    private void joinContacts(Intent intent) {
991        long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
992        long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
993        boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
994        if (contactId1 == -1 || contactId2 == -1) {
995            Log.e(TAG, "Invalid arguments for joinContacts request");
996            return;
997        }
998
999        final ContentResolver resolver = getContentResolver();
1000
1001        // Load raw contact IDs for all raw contacts involved - currently edited and selected
1002        // in the join UIs
1003        Cursor c = resolver.query(RawContacts.CONTENT_URI,
1004                JoinContactQuery.PROJECTION,
1005                JoinContactQuery.SELECTION,
1006                new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
1007
1008        long rawContactIds[];
1009        long verifiedNameRawContactId = -1;
1010        try {
1011            if (c.getCount() == 0) {
1012                return;
1013            }
1014            int maxDisplayNameSource = -1;
1015            rawContactIds = new long[c.getCount()];
1016            for (int i = 0; i < rawContactIds.length; i++) {
1017                c.moveToPosition(i);
1018                long rawContactId = c.getLong(JoinContactQuery._ID);
1019                rawContactIds[i] = rawContactId;
1020                int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1021                if (nameSource > maxDisplayNameSource) {
1022                    maxDisplayNameSource = nameSource;
1023                }
1024            }
1025
1026            // Find an appropriate display name for the joined contact:
1027            // if should have a higher DisplayNameSource or be the name
1028            // of the original contact that we are joining with another.
1029            if (writable) {
1030                for (int i = 0; i < rawContactIds.length; i++) {
1031                    c.moveToPosition(i);
1032                    if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
1033                        int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
1034                        if (nameSource == maxDisplayNameSource
1035                                && (verifiedNameRawContactId == -1
1036                                        || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
1037                            verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
1038                        }
1039                    }
1040                }
1041            }
1042        } finally {
1043            c.close();
1044        }
1045
1046        // For each pair of raw contacts, insert an aggregation exception
1047        ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
1048        for (int i = 0; i < rawContactIds.length; i++) {
1049            for (int j = 0; j < rawContactIds.length; j++) {
1050                if (i != j) {
1051                    buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
1052                }
1053            }
1054        }
1055
1056        // Mark the original contact as "name verified" to make sure that the contact
1057        // display name does not change as a result of the join
1058        if (verifiedNameRawContactId != -1) {
1059            Builder builder = ContentProviderOperation.newUpdate(
1060                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
1061            builder.withValue(RawContacts.NAME_VERIFIED, 1);
1062            operations.add(builder.build());
1063        }
1064
1065        boolean success = false;
1066        // Apply all aggregation exceptions as one batch
1067        try {
1068            resolver.applyBatch(ContactsContract.AUTHORITY, operations);
1069            showToast(R.string.contactsJoinedMessage);
1070            success = true;
1071        } catch (RemoteException e) {
1072            Log.e(TAG, "Failed to apply aggregation exception batch", e);
1073            showToast(R.string.contactSavedErrorToast);
1074        } catch (OperationApplicationException e) {
1075            Log.e(TAG, "Failed to apply aggregation exception batch", e);
1076            showToast(R.string.contactSavedErrorToast);
1077        }
1078
1079        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
1080        if (success) {
1081            Uri uri = RawContacts.getContactLookupUri(resolver,
1082                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
1083            callbackIntent.setData(uri);
1084        }
1085        deliverCallback(callbackIntent);
1086    }
1087
1088    /**
1089     * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
1090     */
1091    private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
1092            long rawContactId1, long rawContactId2) {
1093        Builder builder =
1094                ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
1095        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
1096        builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
1097        builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
1098        operations.add(builder.build());
1099    }
1100
1101    /**
1102     * Shows a toast on the UI thread.
1103     */
1104    private void showToast(final int message) {
1105        mMainHandler.post(new Runnable() {
1106
1107            @Override
1108            public void run() {
1109                Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
1110            }
1111        });
1112    }
1113
1114    private void deliverCallback(final Intent callbackIntent) {
1115        mMainHandler.post(new Runnable() {
1116
1117            @Override
1118            public void run() {
1119                deliverCallbackOnUiThread(callbackIntent);
1120            }
1121        });
1122    }
1123
1124    void deliverCallbackOnUiThread(final Intent callbackIntent) {
1125        // TODO: this assumes that if there are multiple instances of the same
1126        // activity registered, the last one registered is the one waiting for
1127        // the callback. Validity of this assumption needs to be verified.
1128        for (Listener listener : sListeners) {
1129            if (callbackIntent.getComponent().equals(
1130                    ((Activity) listener).getIntent().getComponent())) {
1131                listener.onServiceCompleted(callbackIntent);
1132                return;
1133            }
1134        }
1135    }
1136}
1137