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