Home | History | Annotate | Download | only in list
      1 /*
      2  * Copyright (C) 2009 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.list;
     18 
     19 import com.android.contacts.ContactsActivity;
     20 import com.android.contacts.R;
     21 import com.android.contacts.model.AccountType;
     22 import com.android.contacts.model.AccountTypeManager;
     23 import com.android.contacts.model.AccountWithDataSet;
     24 import com.android.contacts.model.EntityDelta.ValuesDelta;
     25 import com.android.contacts.model.GoogleAccountType;
     26 import com.android.contacts.util.EmptyService;
     27 import com.android.contacts.util.LocalizedNameResolver;
     28 import com.android.contacts.util.WeakAsyncTask;
     29 import com.google.android.collect.Lists;
     30 
     31 import android.app.ActionBar;
     32 import android.app.Activity;
     33 import android.app.AlertDialog;
     34 import android.app.LoaderManager.LoaderCallbacks;
     35 import android.app.ProgressDialog;
     36 import android.content.AsyncTaskLoader;
     37 import android.content.ContentProviderOperation;
     38 import android.content.ContentResolver;
     39 import android.content.ContentValues;
     40 import android.content.Context;
     41 import android.content.DialogInterface;
     42 import android.content.EntityIterator;
     43 import android.content.Intent;
     44 import android.content.Loader;
     45 import android.content.OperationApplicationException;
     46 import android.content.SharedPreferences;
     47 import android.database.Cursor;
     48 import android.net.Uri;
     49 import android.os.Bundle;
     50 import android.os.RemoteException;
     51 import android.preference.PreferenceManager;
     52 import android.provider.ContactsContract;
     53 import android.provider.ContactsContract.Groups;
     54 import android.provider.ContactsContract.Settings;
     55 import android.util.Log;
     56 import android.view.ContextMenu;
     57 import android.view.LayoutInflater;
     58 import android.view.MenuItem;
     59 import android.view.MenuItem.OnMenuItemClickListener;
     60 import android.view.View;
     61 import android.view.ViewGroup;
     62 import android.widget.BaseExpandableListAdapter;
     63 import android.widget.CheckBox;
     64 import android.widget.ExpandableListAdapter;
     65 import android.widget.ExpandableListView;
     66 import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
     67 import android.widget.TextView;
     68 
     69 import java.util.ArrayList;
     70 import java.util.Collections;
     71 import java.util.Comparator;
     72 import java.util.Iterator;
     73 
     74 /**
     75  * Shows a list of all available {@link Groups} available, letting the user
     76  * select which ones they want to be visible.
     77  */
     78 public class CustomContactListFilterActivity extends ContactsActivity
     79         implements View.OnClickListener, ExpandableListView.OnChildClickListener,
     80         LoaderCallbacks<CustomContactListFilterActivity.AccountSet>
     81 {
     82     private static final String TAG = "CustomContactListFilterActivity";
     83 
     84     private static final int ACCOUNT_SET_LOADER_ID = 1;
     85 
     86     private ExpandableListView mList;
     87     private DisplayAdapter mAdapter;
     88 
     89     private SharedPreferences mPrefs;
     90 
     91     @Override
     92     protected void onCreate(Bundle icicle) {
     93         super.onCreate(icicle);
     94         setContentView(R.layout.contact_list_filter_custom);
     95 
     96         mList = (ExpandableListView) findViewById(com.android.internal.R.id.list);
     97         mList.setOnChildClickListener(this);
     98         mList.setHeaderDividersEnabled(true);
     99         mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
    100         mAdapter = new DisplayAdapter(this);
    101 
    102         final LayoutInflater inflater = getLayoutInflater();
    103 
    104         findViewById(R.id.btn_done).setOnClickListener(this);
    105         findViewById(R.id.btn_discard).setOnClickListener(this);
    106 
    107         mList.setOnCreateContextMenuListener(this);
    108 
    109         mList.setAdapter(mAdapter);
    110 
    111         ActionBar actionBar = getActionBar();
    112         if (actionBar != null) {
    113             // android.R.id.home will be triggered in onOptionsItemSelected()
    114             actionBar.setDisplayHomeAsUpEnabled(true);
    115         }
    116     }
    117 
    118     public static class CustomFilterConfigurationLoader extends AsyncTaskLoader<AccountSet> {
    119 
    120         private AccountSet mAccountSet;
    121 
    122         public CustomFilterConfigurationLoader(Context context) {
    123             super(context);
    124         }
    125 
    126         @Override
    127         public AccountSet loadInBackground() {
    128             Context context = getContext();
    129             final AccountTypeManager accountTypes = AccountTypeManager.getInstance(context);
    130             final ContentResolver resolver = context.getContentResolver();
    131 
    132             final AccountSet accounts = new AccountSet();
    133             for (AccountWithDataSet account : accountTypes.getAccounts(false)) {
    134                 final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
    135                 if (accountType.isExtension() && !account.hasData(context)) {
    136                     // Extension with no data -- skip.
    137                     continue;
    138                 }
    139 
    140                 AccountDisplay accountDisplay =
    141                         new AccountDisplay(resolver, account.name, account.type, account.dataSet);
    142 
    143                 final Uri.Builder groupsUri = Groups.CONTENT_URI.buildUpon()
    144                         .appendQueryParameter(Groups.ACCOUNT_NAME, account.name)
    145                         .appendQueryParameter(Groups.ACCOUNT_TYPE, account.type);
    146                 if (account.dataSet != null) {
    147                     groupsUri.appendQueryParameter(Groups.DATA_SET, account.dataSet).build();
    148                 }
    149                 EntityIterator iterator = ContactsContract.Groups.newEntityIterator(resolver.query(
    150                         groupsUri.build(), null, null, null, null));
    151                 try {
    152                     boolean hasGroups = false;
    153 
    154                     // Create entries for each known group
    155                     while (iterator.hasNext()) {
    156                         final ContentValues values = iterator.next().getEntityValues();
    157                         final GroupDelta group = GroupDelta.fromBefore(values);
    158                         accountDisplay.addGroup(group);
    159                         hasGroups = true;
    160                     }
    161                     // Create single entry handling ungrouped status
    162                     accountDisplay.mUngrouped =
    163                         GroupDelta.fromSettings(resolver, account.name, account.type,
    164                                 account.dataSet, hasGroups);
    165                     accountDisplay.addGroup(accountDisplay.mUngrouped);
    166                 } finally {
    167                     iterator.close();
    168                 }
    169 
    170                 accounts.add(accountDisplay);
    171             }
    172 
    173             return accounts;
    174         }
    175 
    176         @Override
    177         public void deliverResult(AccountSet cursor) {
    178             if (isReset()) {
    179                 return;
    180             }
    181 
    182             mAccountSet = cursor;
    183 
    184             if (isStarted()) {
    185                 super.deliverResult(cursor);
    186             }
    187         }
    188 
    189         @Override
    190         protected void onStartLoading() {
    191             if (mAccountSet != null) {
    192                 deliverResult(mAccountSet);
    193             }
    194             if (takeContentChanged() || mAccountSet == null) {
    195                 forceLoad();
    196             }
    197         }
    198 
    199         @Override
    200         protected void onStopLoading() {
    201             cancelLoad();
    202         }
    203 
    204         @Override
    205         protected void onReset() {
    206             super.onReset();
    207             onStopLoading();
    208             mAccountSet = null;
    209         }
    210     }
    211 
    212     @Override
    213     protected void onStart() {
    214         getLoaderManager().initLoader(ACCOUNT_SET_LOADER_ID, null, this);
    215         super.onStart();
    216     }
    217 
    218     @Override
    219     public Loader<AccountSet> onCreateLoader(int id, Bundle args) {
    220         return new CustomFilterConfigurationLoader(this);
    221     }
    222 
    223     @Override
    224     public void onLoadFinished(Loader<AccountSet> loader, AccountSet data) {
    225         mAdapter.setAccounts(data);
    226     }
    227 
    228     @Override
    229     public void onLoaderReset(Loader<AccountSet> loader) {
    230         mAdapter.setAccounts(null);
    231     }
    232 
    233     private static final int DEFAULT_SHOULD_SYNC = 1;
    234     private static final int DEFAULT_VISIBLE = 0;
    235 
    236     /**
    237      * Entry holding any changes to {@link Groups} or {@link Settings} rows,
    238      * such as {@link Groups#SHOULD_SYNC} or {@link Groups#GROUP_VISIBLE}.
    239      */
    240     protected static class GroupDelta extends ValuesDelta {
    241         private boolean mUngrouped = false;
    242         private boolean mAccountHasGroups;
    243 
    244         private GroupDelta() {
    245             super();
    246         }
    247 
    248         /**
    249          * Build {@link GroupDelta} from the {@link Settings} row for the given
    250          * {@link Settings#ACCOUNT_NAME}, {@link Settings#ACCOUNT_TYPE}, and
    251          * {@link Settings#DATA_SET}.
    252          */
    253         public static GroupDelta fromSettings(ContentResolver resolver, String accountName,
    254                 String accountType, String dataSet, boolean accountHasGroups) {
    255             final Uri.Builder settingsUri = Settings.CONTENT_URI.buildUpon()
    256                     .appendQueryParameter(Settings.ACCOUNT_NAME, accountName)
    257                     .appendQueryParameter(Settings.ACCOUNT_TYPE, accountType);
    258             if (dataSet != null) {
    259                 settingsUri.appendQueryParameter(Settings.DATA_SET, dataSet);
    260             }
    261             final Cursor cursor = resolver.query(settingsUri.build(), new String[] {
    262                     Settings.SHOULD_SYNC, Settings.UNGROUPED_VISIBLE
    263             }, null, null, null);
    264 
    265             try {
    266                 final ContentValues values = new ContentValues();
    267                 values.put(Settings.ACCOUNT_NAME, accountName);
    268                 values.put(Settings.ACCOUNT_TYPE, accountType);
    269                 values.put(Settings.DATA_SET, dataSet);
    270 
    271                 if (cursor != null && cursor.moveToFirst()) {
    272                     // Read existing values when present
    273                     values.put(Settings.SHOULD_SYNC, cursor.getInt(0));
    274                     values.put(Settings.UNGROUPED_VISIBLE, cursor.getInt(1));
    275                     return fromBefore(values).setUngrouped(accountHasGroups);
    276                 } else {
    277                     // Nothing found, so treat as create
    278                     values.put(Settings.SHOULD_SYNC, DEFAULT_SHOULD_SYNC);
    279                     values.put(Settings.UNGROUPED_VISIBLE, DEFAULT_VISIBLE);
    280                     return fromAfter(values).setUngrouped(accountHasGroups);
    281                 }
    282             } finally {
    283                 if (cursor != null) cursor.close();
    284             }
    285         }
    286 
    287         public static GroupDelta fromBefore(ContentValues before) {
    288             final GroupDelta entry = new GroupDelta();
    289             entry.mBefore = before;
    290             entry.mAfter = new ContentValues();
    291             return entry;
    292         }
    293 
    294         public static GroupDelta fromAfter(ContentValues after) {
    295             final GroupDelta entry = new GroupDelta();
    296             entry.mBefore = null;
    297             entry.mAfter = after;
    298             return entry;
    299         }
    300 
    301         protected GroupDelta setUngrouped(boolean accountHasGroups) {
    302             mUngrouped = true;
    303             mAccountHasGroups = accountHasGroups;
    304             return this;
    305         }
    306 
    307         @Override
    308         public boolean beforeExists() {
    309             return mBefore != null;
    310         }
    311 
    312         public boolean getShouldSync() {
    313             return getAsInteger(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC,
    314                     DEFAULT_SHOULD_SYNC) != 0;
    315         }
    316 
    317         public boolean getVisible() {
    318             return getAsInteger(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE,
    319                     DEFAULT_VISIBLE) != 0;
    320         }
    321 
    322         public void putShouldSync(boolean shouldSync) {
    323             put(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC, shouldSync ? 1 : 0);
    324         }
    325 
    326         public void putVisible(boolean visible) {
    327             put(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE, visible ? 1 : 0);
    328         }
    329 
    330         private String getAccountType() {
    331             return (mBefore == null ? mAfter : mBefore).getAsString(Settings.ACCOUNT_TYPE);
    332         }
    333 
    334         public CharSequence getTitle(Context context) {
    335             if (mUngrouped) {
    336                 final String customAllContactsName =
    337                         LocalizedNameResolver.getAllContactsName(context, getAccountType());
    338                 if (customAllContactsName != null) {
    339                     return customAllContactsName;
    340                 }
    341                 if (mAccountHasGroups) {
    342                     return context.getText(R.string.display_ungrouped);
    343                 } else {
    344                     return context.getText(R.string.display_all_contacts);
    345                 }
    346             } else {
    347                 final Integer titleRes = getAsInteger(Groups.TITLE_RES);
    348                 if (titleRes != null) {
    349                     final String packageName = getAsString(Groups.RES_PACKAGE);
    350                     return context.getPackageManager().getText(packageName, titleRes, null);
    351                 } else {
    352                     return getAsString(Groups.TITLE);
    353                 }
    354             }
    355         }
    356 
    357         /**
    358          * Build a possible {@link ContentProviderOperation} to persist any
    359          * changes to the {@link Groups} or {@link Settings} row described by
    360          * this {@link GroupDelta}.
    361          */
    362         public ContentProviderOperation buildDiff() {
    363             if (isInsert()) {
    364                 // Only allow inserts for Settings
    365                 if (mUngrouped) {
    366                     mAfter.remove(mIdColumn);
    367                     return ContentProviderOperation.newInsert(Settings.CONTENT_URI)
    368                             .withValues(mAfter)
    369                             .build();
    370                 }
    371                 else {
    372                     throw new IllegalStateException("Unexpected diff");
    373                 }
    374             } else if (isUpdate()) {
    375                 if (mUngrouped) {
    376                     String accountName = this.getAsString(Settings.ACCOUNT_NAME);
    377                     String accountType = this.getAsString(Settings.ACCOUNT_TYPE);
    378                     String dataSet = this.getAsString(Settings.DATA_SET);
    379                     StringBuilder selection = new StringBuilder(Settings.ACCOUNT_NAME + "=? AND "
    380                             + Settings.ACCOUNT_TYPE + "=?");
    381                     String[] selectionArgs;
    382                     if (dataSet == null) {
    383                         selection.append(" AND " + Settings.DATA_SET + " IS NULL");
    384                         selectionArgs = new String[] {accountName, accountType};
    385                     } else {
    386                         selection.append(" AND " + Settings.DATA_SET + "=?");
    387                         selectionArgs = new String[] {accountName, accountType, dataSet};
    388                     }
    389                     return ContentProviderOperation.newUpdate(Settings.CONTENT_URI)
    390                             .withSelection(selection.toString(), selectionArgs)
    391                             .withValues(mAfter)
    392                             .build();
    393                 } else {
    394                     return ContentProviderOperation.newUpdate(
    395                                     addCallerIsSyncAdapterParameter(Groups.CONTENT_URI))
    396                             .withSelection(Groups._ID + "=" + this.getId(), null)
    397                             .withValues(mAfter)
    398                             .build();
    399                 }
    400             } else {
    401                 return null;
    402             }
    403         }
    404     }
    405 
    406     private static Uri addCallerIsSyncAdapterParameter(Uri uri) {
    407         return uri.buildUpon()
    408             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
    409             .build();
    410     }
    411 
    412     /**
    413      * {@link Comparator} to sort by {@link Groups#_ID}.
    414      */
    415     private static Comparator<GroupDelta> sIdComparator = new Comparator<GroupDelta>() {
    416         public int compare(GroupDelta object1, GroupDelta object2) {
    417             final Long id1 = object1.getId();
    418             final Long id2 = object2.getId();
    419             if (id1 == null && id2 == null) {
    420                 return 0;
    421             } else if (id1 == null) {
    422                 return -1;
    423             } else if (id2 == null) {
    424                 return 1;
    425             } else if (id1 < id2) {
    426                 return -1;
    427             } else if (id1 > id2) {
    428                 return 1;
    429             } else {
    430                 return 0;
    431             }
    432         }
    433     };
    434 
    435     /**
    436      * Set of all {@link AccountDisplay} entries, one for each source.
    437      */
    438     protected static class AccountSet extends ArrayList<AccountDisplay> {
    439         public ArrayList<ContentProviderOperation> buildDiff() {
    440             final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
    441             for (AccountDisplay account : this) {
    442                 account.buildDiff(diff);
    443             }
    444             return diff;
    445         }
    446     }
    447 
    448     /**
    449      * {@link GroupDelta} details for a single {@link AccountWithDataSet}, usually shown as
    450      * children under a single expandable group.
    451      */
    452     protected static class AccountDisplay {
    453         public final String mName;
    454         public final String mType;
    455         public final String mDataSet;
    456 
    457         public GroupDelta mUngrouped;
    458         public ArrayList<GroupDelta> mSyncedGroups = Lists.newArrayList();
    459         public ArrayList<GroupDelta> mUnsyncedGroups = Lists.newArrayList();
    460 
    461         /**
    462          * Build an {@link AccountDisplay} covering all {@link Groups} under the
    463          * given {@link AccountWithDataSet}.
    464          */
    465         public AccountDisplay(ContentResolver resolver, String accountName, String accountType,
    466                 String dataSet) {
    467             mName = accountName;
    468             mType = accountType;
    469             mDataSet = dataSet;
    470         }
    471 
    472         /**
    473          * Add the given {@link GroupDelta} internally, filing based on its
    474          * {@link GroupDelta#getShouldSync()} status.
    475          */
    476         private void addGroup(GroupDelta group) {
    477             if (group.getShouldSync()) {
    478                 mSyncedGroups.add(group);
    479             } else {
    480                 mUnsyncedGroups.add(group);
    481             }
    482         }
    483 
    484         /**
    485          * Set the {@link GroupDelta#putShouldSync(boolean)} value for all
    486          * children {@link GroupDelta} rows.
    487          */
    488         public void setShouldSync(boolean shouldSync) {
    489             final Iterator<GroupDelta> oppositeChildren = shouldSync ?
    490                     mUnsyncedGroups.iterator() : mSyncedGroups.iterator();
    491             while (oppositeChildren.hasNext()) {
    492                 final GroupDelta child = oppositeChildren.next();
    493                 setShouldSync(child, shouldSync, false);
    494                 oppositeChildren.remove();
    495             }
    496         }
    497 
    498         public void setShouldSync(GroupDelta child, boolean shouldSync) {
    499             setShouldSync(child, shouldSync, true);
    500         }
    501 
    502         /**
    503          * Set {@link GroupDelta#putShouldSync(boolean)}, and file internally
    504          * based on updated state.
    505          */
    506         public void setShouldSync(GroupDelta child, boolean shouldSync, boolean attemptRemove) {
    507             child.putShouldSync(shouldSync);
    508             if (shouldSync) {
    509                 if (attemptRemove) {
    510                     mUnsyncedGroups.remove(child);
    511                 }
    512                 mSyncedGroups.add(child);
    513                 Collections.sort(mSyncedGroups, sIdComparator);
    514             } else {
    515                 if (attemptRemove) {
    516                     mSyncedGroups.remove(child);
    517                 }
    518                 mUnsyncedGroups.add(child);
    519             }
    520         }
    521 
    522         /**
    523          * Build set of {@link ContentProviderOperation} to persist any user
    524          * changes to {@link GroupDelta} rows under this {@link AccountWithDataSet}.
    525          */
    526         public void buildDiff(ArrayList<ContentProviderOperation> diff) {
    527             for (GroupDelta group : mSyncedGroups) {
    528                 final ContentProviderOperation oper = group.buildDiff();
    529                 if (oper != null) diff.add(oper);
    530             }
    531             for (GroupDelta group : mUnsyncedGroups) {
    532                 final ContentProviderOperation oper = group.buildDiff();
    533                 if (oper != null) diff.add(oper);
    534             }
    535         }
    536     }
    537 
    538     /**
    539      * {@link ExpandableListAdapter} that shows {@link GroupDelta} settings,
    540      * grouped by {@link AccountWithDataSet} type. Shows footer row when any groups are
    541      * unsynced, as determined through {@link AccountDisplay#mUnsyncedGroups}.
    542      */
    543     protected static class DisplayAdapter extends BaseExpandableListAdapter {
    544         private Context mContext;
    545         private LayoutInflater mInflater;
    546         private AccountTypeManager mAccountTypes;
    547         private AccountSet mAccounts;
    548 
    549         private boolean mChildWithPhones = false;
    550 
    551         public DisplayAdapter(Context context) {
    552             mContext = context;
    553             mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    554             mAccountTypes = AccountTypeManager.getInstance(context);
    555         }
    556 
    557         public void setAccounts(AccountSet accounts) {
    558             mAccounts = accounts;
    559             notifyDataSetChanged();
    560         }
    561 
    562         /**
    563          * In group descriptions, show the number of contacts with phone
    564          * numbers, in addition to the total contacts.
    565          */
    566         public void setChildDescripWithPhones(boolean withPhones) {
    567             mChildWithPhones = withPhones;
    568         }
    569 
    570         @Override
    571         public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
    572                 ViewGroup parent) {
    573             if (convertView == null) {
    574                 convertView = mInflater.inflate(
    575                         R.layout.custom_contact_list_filter_account, parent, false);
    576             }
    577 
    578             final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
    579             final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
    580 
    581             final AccountDisplay account = (AccountDisplay)this.getGroup(groupPosition);
    582 
    583             final AccountType accountType = mAccountTypes.getAccountType(
    584                     account.mType, account.mDataSet);
    585 
    586             text1.setText(account.mName);
    587             text1.setVisibility(account.mName == null ? View.GONE : View.VISIBLE);
    588             text2.setText(accountType.getDisplayLabel(mContext));
    589 
    590             return convertView;
    591         }
    592 
    593         @Override
    594         public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
    595                 View convertView, ViewGroup parent) {
    596             if (convertView == null) {
    597                 convertView = mInflater.inflate(
    598                         R.layout.custom_contact_list_filter_group, parent, false);
    599             }
    600 
    601             final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
    602             final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
    603             final CheckBox checkbox = (CheckBox)convertView.findViewById(android.R.id.checkbox);
    604 
    605             final AccountDisplay account = mAccounts.get(groupPosition);
    606             final GroupDelta child = (GroupDelta)this.getChild(groupPosition, childPosition);
    607             if (child != null) {
    608                 // Handle normal group, with title and checkbox
    609                 final boolean groupVisible = child.getVisible();
    610                 checkbox.setVisibility(View.VISIBLE);
    611                 checkbox.setChecked(groupVisible);
    612 
    613                 final CharSequence groupTitle = child.getTitle(mContext);
    614                 text1.setText(groupTitle);
    615                 text2.setVisibility(View.GONE);
    616             } else {
    617                 // When unknown child, this is "more" footer view
    618                 checkbox.setVisibility(View.GONE);
    619                 text1.setText(R.string.display_more_groups);
    620                 text2.setVisibility(View.GONE);
    621             }
    622 
    623             return convertView;
    624         }
    625 
    626         @Override
    627         public Object getChild(int groupPosition, int childPosition) {
    628             final AccountDisplay account = mAccounts.get(groupPosition);
    629             final boolean validChild = childPosition >= 0
    630                     && childPosition < account.mSyncedGroups.size();
    631             if (validChild) {
    632                 return account.mSyncedGroups.get(childPosition);
    633             } else {
    634                 return null;
    635             }
    636         }
    637 
    638         @Override
    639         public long getChildId(int groupPosition, int childPosition) {
    640             final GroupDelta child = (GroupDelta)getChild(groupPosition, childPosition);
    641             if (child != null) {
    642                 final Long childId = child.getId();
    643                 return childId != null ? childId : Long.MIN_VALUE;
    644             } else {
    645                 return Long.MIN_VALUE;
    646             }
    647         }
    648 
    649         @Override
    650         public int getChildrenCount(int groupPosition) {
    651             // Count is any synced groups, plus possible footer
    652             final AccountDisplay account = mAccounts.get(groupPosition);
    653             final boolean anyHidden = account.mUnsyncedGroups.size() > 0;
    654             return account.mSyncedGroups.size() + (anyHidden ? 1 : 0);
    655         }
    656 
    657         @Override
    658         public Object getGroup(int groupPosition) {
    659             return mAccounts.get(groupPosition);
    660         }
    661 
    662         @Override
    663         public int getGroupCount() {
    664             if (mAccounts == null) {
    665                 return 0;
    666             }
    667             return mAccounts.size();
    668         }
    669 
    670         @Override
    671         public long getGroupId(int groupPosition) {
    672             return groupPosition;
    673         }
    674 
    675         @Override
    676         public boolean hasStableIds() {
    677             return true;
    678         }
    679 
    680         @Override
    681         public boolean isChildSelectable(int groupPosition, int childPosition) {
    682             return true;
    683         }
    684     }
    685 
    686     /** {@inheritDoc} */
    687     public void onClick(View view) {
    688         switch (view.getId()) {
    689             case R.id.btn_done: {
    690                 this.doSaveAction();
    691                 break;
    692             }
    693             case R.id.btn_discard: {
    694                 this.finish();
    695                 break;
    696             }
    697         }
    698     }
    699 
    700     /**
    701      * Handle any clicks on {@link ExpandableListAdapter} children, which
    702      * usually mean toggling its visible state.
    703      */
    704     @Override
    705     public boolean onChildClick(ExpandableListView parent, View view, int groupPosition,
    706             int childPosition, long id) {
    707         final CheckBox checkbox = (CheckBox)view.findViewById(android.R.id.checkbox);
    708 
    709         final AccountDisplay account = (AccountDisplay)mAdapter.getGroup(groupPosition);
    710         final GroupDelta child = (GroupDelta)mAdapter.getChild(groupPosition, childPosition);
    711         if (child != null) {
    712             checkbox.toggle();
    713             child.putVisible(checkbox.isChecked());
    714         } else {
    715             // Open context menu for bringing back unsynced
    716             this.openContextMenu(view);
    717         }
    718         return true;
    719     }
    720 
    721     // TODO: move these definitions to framework constants when we begin
    722     // defining this mode through <sync-adapter> tags
    723     private static final int SYNC_MODE_UNSUPPORTED = 0;
    724     private static final int SYNC_MODE_UNGROUPED = 1;
    725     private static final int SYNC_MODE_EVERYTHING = 2;
    726 
    727     protected int getSyncMode(AccountDisplay account) {
    728         // TODO: read sync mode through <sync-adapter> definition
    729         if (GoogleAccountType.ACCOUNT_TYPE.equals(account.mType) && account.mDataSet == null) {
    730             return SYNC_MODE_EVERYTHING;
    731         } else {
    732             return SYNC_MODE_UNSUPPORTED;
    733         }
    734     }
    735 
    736     @Override
    737     public void onCreateContextMenu(ContextMenu menu, View view,
    738             ContextMenu.ContextMenuInfo menuInfo) {
    739         super.onCreateContextMenu(menu, view, menuInfo);
    740 
    741         // Bail if not working with expandable long-press, or if not child
    742         if (!(menuInfo instanceof ExpandableListContextMenuInfo)) return;
    743 
    744         final ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuInfo;
    745         final int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);
    746         final int childPosition = ExpandableListView.getPackedPositionChild(info.packedPosition);
    747 
    748         // Skip long-press on expandable parents
    749         if (childPosition == -1) return;
    750 
    751         final AccountDisplay account = (AccountDisplay)mAdapter.getGroup(groupPosition);
    752         final GroupDelta child = (GroupDelta)mAdapter.getChild(groupPosition, childPosition);
    753 
    754         // Ignore when selective syncing unsupported
    755         final int syncMode = getSyncMode(account);
    756         if (syncMode == SYNC_MODE_UNSUPPORTED) return;
    757 
    758         if (child != null) {
    759             showRemoveSync(menu, account, child, syncMode);
    760         } else {
    761             showAddSync(menu, account, syncMode);
    762         }
    763     }
    764 
    765     protected void showRemoveSync(ContextMenu menu, final AccountDisplay account,
    766             final GroupDelta child, final int syncMode) {
    767         final CharSequence title = child.getTitle(this);
    768 
    769         menu.setHeaderTitle(title);
    770         menu.add(R.string.menu_sync_remove).setOnMenuItemClickListener(
    771                 new OnMenuItemClickListener() {
    772                     public boolean onMenuItemClick(MenuItem item) {
    773                         handleRemoveSync(account, child, syncMode, title);
    774                         return true;
    775                     }
    776                 });
    777     }
    778 
    779     protected void handleRemoveSync(final AccountDisplay account, final GroupDelta child,
    780             final int syncMode, CharSequence title) {
    781         final boolean shouldSyncUngrouped = account.mUngrouped.getShouldSync();
    782         if (syncMode == SYNC_MODE_EVERYTHING && shouldSyncUngrouped
    783                 && !child.equals(account.mUngrouped)) {
    784             // Warn before removing this group when it would cause ungrouped to stop syncing
    785             final AlertDialog.Builder builder = new AlertDialog.Builder(this);
    786             final CharSequence removeMessage = this.getString(
    787                     R.string.display_warn_remove_ungrouped, title);
    788             builder.setTitle(R.string.menu_sync_remove);
    789             builder.setMessage(removeMessage);
    790             builder.setNegativeButton(android.R.string.cancel, null);
    791             builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
    792                 public void onClick(DialogInterface dialog, int which) {
    793                     // Mark both this group and ungrouped to stop syncing
    794                     account.setShouldSync(account.mUngrouped, false);
    795                     account.setShouldSync(child, false);
    796                     mAdapter.notifyDataSetChanged();
    797                 }
    798             });
    799             builder.show();
    800         } else {
    801             // Mark this group to not sync
    802             account.setShouldSync(child, false);
    803             mAdapter.notifyDataSetChanged();
    804         }
    805     }
    806 
    807     protected void showAddSync(ContextMenu menu, final AccountDisplay account, final int syncMode) {
    808         menu.setHeaderTitle(R.string.dialog_sync_add);
    809 
    810         // Create item for each available, unsynced group
    811         for (final GroupDelta child : account.mUnsyncedGroups) {
    812             if (!child.getShouldSync()) {
    813                 final CharSequence title = child.getTitle(this);
    814                 menu.add(title).setOnMenuItemClickListener(new OnMenuItemClickListener() {
    815                     public boolean onMenuItemClick(MenuItem item) {
    816                         // Adding specific group for syncing
    817                         if (child.mUngrouped && syncMode == SYNC_MODE_EVERYTHING) {
    818                             account.setShouldSync(true);
    819                         } else {
    820                             account.setShouldSync(child, true);
    821                         }
    822                         mAdapter.notifyDataSetChanged();
    823                         return true;
    824                     }
    825                 });
    826             }
    827         }
    828     }
    829 
    830     @SuppressWarnings("unchecked")
    831     private void doSaveAction() {
    832         if (mAdapter == null || mAdapter.mAccounts == null) {
    833             finish();
    834             return;
    835         }
    836 
    837         setResult(RESULT_OK);
    838 
    839         final ArrayList<ContentProviderOperation> diff = mAdapter.mAccounts.buildDiff();
    840         if (diff.isEmpty()) {
    841             finish();
    842             return;
    843         }
    844 
    845         new UpdateTask(this).execute(diff);
    846     }
    847 
    848     /**
    849      * Background task that persists changes to {@link Groups#GROUP_VISIBLE},
    850      * showing spinner dialog to user while updating.
    851      */
    852     public static class UpdateTask extends
    853             WeakAsyncTask<ArrayList<ContentProviderOperation>, Void, Void, Activity> {
    854         private ProgressDialog mProgress;
    855 
    856         public UpdateTask(Activity target) {
    857             super(target);
    858         }
    859 
    860         /** {@inheritDoc} */
    861         @Override
    862         protected void onPreExecute(Activity target) {
    863             final Context context = target;
    864 
    865             mProgress = ProgressDialog.show(
    866                     context, null, context.getText(R.string.savingDisplayGroups));
    867 
    868             // Before starting this task, start an empty service to protect our
    869             // process from being reclaimed by the system.
    870             context.startService(new Intent(context, EmptyService.class));
    871         }
    872 
    873         /** {@inheritDoc} */
    874         @Override
    875         protected Void doInBackground(
    876                 Activity target, ArrayList<ContentProviderOperation>... params) {
    877             final Context context = target;
    878             final ContentValues values = new ContentValues();
    879             final ContentResolver resolver = context.getContentResolver();
    880 
    881             try {
    882                 final ArrayList<ContentProviderOperation> diff = params[0];
    883                 resolver.applyBatch(ContactsContract.AUTHORITY, diff);
    884             } catch (RemoteException e) {
    885                 Log.e(TAG, "Problem saving display groups", e);
    886             } catch (OperationApplicationException e) {
    887                 Log.e(TAG, "Problem saving display groups", e);
    888             }
    889 
    890             return null;
    891         }
    892 
    893         /** {@inheritDoc} */
    894         @Override
    895         protected void onPostExecute(Activity target, Void result) {
    896             final Context context = target;
    897 
    898             try {
    899                 mProgress.dismiss();
    900             } catch (Exception e) {
    901                 Log.e(TAG, "Error dismissing progress dialog", e);
    902             }
    903 
    904             target.finish();
    905 
    906             // Stop the service that was protecting us
    907             context.stopService(new Intent(context, EmptyService.class));
    908         }
    909     }
    910 
    911     @Override
    912     public boolean onOptionsItemSelected(MenuItem item) {
    913         switch (item.getItemId()) {
    914             case android.R.id.home:
    915                 // Pretend cancel.
    916                 setResult(Activity.RESULT_CANCELED);
    917                 finish();
    918                 return true;
    919             default:
    920                 break;
    921         }
    922         return super.onOptionsItemSelected(item);
    923     }
    924 }
    925