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