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