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 android.app.ActionBar;
     20 import android.app.Activity;
     21 import android.app.AlertDialog;
     22 import android.app.Dialog;
     23 import android.app.DialogFragment;
     24 import android.app.LoaderManager.LoaderCallbacks;
     25 import android.app.ProgressDialog;
     26 import android.content.ContentProviderOperation;
     27 import android.content.ContentResolver;
     28 import android.content.ContentValues;
     29 import android.content.Context;
     30 import android.content.DialogInterface;
     31 import android.content.Intent;
     32 import android.content.IntentFilter;
     33 import android.content.Loader;
     34 import android.content.OperationApplicationException;
     35 import android.database.Cursor;
     36 import android.graphics.Color;
     37 import android.graphics.drawable.ColorDrawable;
     38 import android.net.Uri;
     39 import android.os.Bundle;
     40 import android.os.RemoteException;
     41 import android.provider.ContactsContract;
     42 import android.provider.ContactsContract.Groups;
     43 import android.provider.ContactsContract.Settings;
     44 import android.util.Log;
     45 import android.view.ContextMenu;
     46 import android.view.LayoutInflater;
     47 import android.view.Menu;
     48 import android.view.MenuItem;
     49 import android.view.MenuItem.OnMenuItemClickListener;
     50 import android.view.View;
     51 import android.view.ViewGroup;
     52 import android.widget.BaseExpandableListAdapter;
     53 import android.widget.CheckBox;
     54 import android.widget.ExpandableListAdapter;
     55 import android.widget.ExpandableListView;
     56 import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
     57 import android.widget.TextView;
     58 
     59 import com.android.contacts.R;
     60 import com.android.contacts.model.AccountTypeManager;
     61 import com.android.contacts.model.ValuesDelta;
     62 import com.android.contacts.model.account.AccountInfo;
     63 import com.android.contacts.model.account.AccountWithDataSet;
     64 import com.android.contacts.model.account.GoogleAccountType;
     65 import com.android.contacts.util.EmptyService;
     66 import com.android.contacts.util.LocalizedNameResolver;
     67 import com.android.contacts.util.WeakAsyncTask;
     68 import com.android.contacts.util.concurrent.ContactsExecutors;
     69 import com.android.contacts.util.concurrent.ListenableFutureLoader;
     70 import com.google.common.base.Function;
     71 import com.google.common.collect.Lists;
     72 import com.google.common.util.concurrent.Futures;
     73 import com.google.common.util.concurrent.ListenableFuture;
     74 
     75 import java.util.ArrayList;
     76 import java.util.Collections;
     77 import java.util.Comparator;
     78 import java.util.Iterator;
     79 import java.util.List;
     80 
     81 import javax.annotation.Nullable;
     82 
     83 /**
     84  * Shows a list of all available {@link Groups} available, letting the user
     85  * select which ones they want to be visible.
     86  */
     87 public class CustomContactListFilterActivity extends Activity implements
     88         ExpandableListView.OnChildClickListener,
     89         LoaderCallbacks<CustomContactListFilterActivity.AccountSet> {
     90     private static final String TAG = "CustomContactListFilter";
     91 
     92     public static final String EXTRA_CURRENT_LIST_FILTER_TYPE = "currentListFilterType";
     93 
     94     private static final int ACCOUNT_SET_LOADER_ID = 1;
     95 
     96     private ExpandableListView mList;
     97     private DisplayAdapter mAdapter;
     98 
     99     @Override
    100     protected void onCreate(Bundle icicle) {
    101         super.onCreate(icicle);
    102         setContentView(R.layout.contact_list_filter_custom);
    103 
    104         mList = (ExpandableListView) findViewById(android.R.id.list);
    105         mList.setOnChildClickListener(this);
    106         mList.setHeaderDividersEnabled(true);
    107         mList.setChildDivider(new ColorDrawable(Color.TRANSPARENT));
    108 
    109         mList.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
    110             @Override
    111             public void onLayoutChange(final View v, final int left, final int top, final int right,
    112                     final int bottom, final int oldLeft, final int oldTop, final int oldRight,
    113                     final int oldBottom) {
    114                 mList.setIndicatorBounds(
    115                         mList.getWidth() - getResources().getDimensionPixelSize(
    116                                 R.dimen.contact_filter_indicator_padding_end),
    117                         mList.getWidth() - getResources().getDimensionPixelSize(
    118                                 R.dimen.contact_filter_indicator_padding_start));
    119             }
    120         });
    121 
    122         mAdapter = new DisplayAdapter(this);
    123 
    124         mList.setOnCreateContextMenuListener(this);
    125 
    126         mList.setAdapter(mAdapter);
    127 
    128         ActionBar actionBar = getActionBar();
    129         if (actionBar != null) {
    130             // android.R.id.home will be triggered in onOptionsItemSelected()
    131             actionBar.setDisplayHomeAsUpEnabled(true);
    132         }
    133     }
    134 
    135     public static class CustomFilterConfigurationLoader extends ListenableFutureLoader<AccountSet> {
    136 
    137         private AccountTypeManager mAccountTypeManager;
    138 
    139         public CustomFilterConfigurationLoader(Context context) {
    140             super(context, new IntentFilter(AccountTypeManager.BROADCAST_ACCOUNTS_CHANGED));
    141             mAccountTypeManager = AccountTypeManager.getInstance(context);
    142         }
    143 
    144         @Override
    145         public ListenableFuture<AccountSet> loadData() {
    146             return Futures.transform(mAccountTypeManager.getAccountsAsync(),
    147                     new Function<List<AccountInfo>, AccountSet>() {
    148                 @Nullable
    149                 @Override
    150                 public AccountSet apply(@Nullable List<AccountInfo> input) {
    151                     return createAccountSet(input);
    152                 }
    153             }, ContactsExecutors.getDefaultThreadPoolExecutor());
    154         }
    155 
    156         private AccountSet createAccountSet(List<AccountInfo> sourceAccounts) {
    157             final Context context = getContext();
    158             final ContentResolver resolver = context.getContentResolver();
    159 
    160             final AccountSet accounts = new AccountSet();
    161 
    162             // Don't include the null account because it doesn't support writing to
    163             // ContactsContract.Settings
    164             for (AccountInfo info : sourceAccounts) {
    165                 final AccountWithDataSet account = info.getAccount();
    166                 final AccountDisplay accountDisplay = new AccountDisplay(resolver, info);
    167 
    168                 final Uri.Builder groupsUri = Groups.CONTENT_URI.buildUpon()
    169                         .appendQueryParameter(Groups.ACCOUNT_NAME, account.name)
    170                         .appendQueryParameter(Groups.ACCOUNT_TYPE, account.type);
    171                 if (account.dataSet != null) {
    172                     groupsUri.appendQueryParameter(Groups.DATA_SET, account.dataSet).build();
    173                 }
    174                 final Cursor cursor = resolver.query(groupsUri.build(), null, null, null, null);
    175                 if (cursor == null) {
    176                     continue;
    177                 }
    178                 android.content.EntityIterator iterator =
    179                         ContactsContract.Groups.newEntityIterator(cursor);
    180                 try {
    181                     boolean hasGroups = false;
    182 
    183                     // Create entries for each known group
    184                     while (iterator.hasNext()) {
    185                         final ContentValues values = iterator.next().getEntityValues();
    186                         final GroupDelta group = GroupDelta.fromBefore(values);
    187                         accountDisplay.addGroup(group);
    188                         hasGroups = true;
    189                     }
    190                     // Create single entry handling ungrouped status
    191                     accountDisplay.mUngrouped =
    192                         GroupDelta.fromSettings(resolver, account.name, account.type,
    193                                 account.dataSet, hasGroups);
    194                     accountDisplay.addGroup(accountDisplay.mUngrouped);
    195                 } finally {
    196                     iterator.close();
    197                 }
    198 
    199                 accounts.add(accountDisplay);
    200             }
    201 
    202             return accounts;
    203         }
    204     }
    205 
    206     @Override
    207     protected void onStart() {
    208         getLoaderManager().initLoader(ACCOUNT_SET_LOADER_ID, null, this);
    209         super.onStart();
    210     }
    211 
    212     @Override
    213     public Loader<AccountSet> onCreateLoader(int id, Bundle args) {
    214         return new CustomFilterConfigurationLoader(this);
    215     }
    216 
    217     @Override
    218     public void onLoadFinished(Loader<AccountSet> loader, AccountSet data) {
    219         mAdapter.setAccounts(data);
    220     }
    221 
    222     @Override
    223     public void onLoaderReset(Loader<AccountSet> loader) {
    224         mAdapter.setAccounts(null);
    225     }
    226 
    227     private static final int DEFAULT_SHOULD_SYNC = 1;
    228     private static final int DEFAULT_VISIBLE = 0;
    229 
    230     /**
    231      * Entry holding any changes to {@link Groups} or {@link Settings} rows,
    232      * such as {@link Groups#SHOULD_SYNC} or {@link Groups#GROUP_VISIBLE}.
    233      */
    234     protected static class GroupDelta extends ValuesDelta {
    235         private boolean mUngrouped = false;
    236         private boolean mAccountHasGroups;
    237 
    238         private GroupDelta() {
    239             super();
    240         }
    241 
    242         /**
    243          * Build {@link GroupDelta} from the {@link Settings} row for the given
    244          * {@link Settings#ACCOUNT_NAME}, {@link Settings#ACCOUNT_TYPE}, and
    245          * {@link Settings#DATA_SET}.
    246          */
    247         public static GroupDelta fromSettings(ContentResolver resolver, String accountName,
    248                 String accountType, String dataSet, boolean accountHasGroups) {
    249             final Uri.Builder settingsUri = Settings.CONTENT_URI.buildUpon()
    250                     .appendQueryParameter(Settings.ACCOUNT_NAME, accountName)
    251                     .appendQueryParameter(Settings.ACCOUNT_TYPE, accountType);
    252             if (dataSet != null) {
    253                 settingsUri.appendQueryParameter(Settings.DATA_SET, dataSet);
    254             }
    255             final Cursor cursor = resolver.query(settingsUri.build(), new String[] {
    256                     Settings.SHOULD_SYNC, Settings.UNGROUPED_VISIBLE
    257             }, null, null, null);
    258 
    259             try {
    260                 final ContentValues values = new ContentValues();
    261                 values.put(Settings.ACCOUNT_NAME, accountName);
    262                 values.put(Settings.ACCOUNT_TYPE, accountType);
    263                 values.put(Settings.DATA_SET, dataSet);
    264 
    265                 if (cursor != null && cursor.moveToFirst()) {
    266                     // Read existing values when present
    267                     values.put(Settings.SHOULD_SYNC, cursor.getInt(0));
    268                     values.put(Settings.UNGROUPED_VISIBLE, cursor.getInt(1));
    269                     return fromBefore(values).setUngrouped(accountHasGroups);
    270                 } else {
    271                     // Nothing found, so treat as create
    272                     values.put(Settings.SHOULD_SYNC, DEFAULT_SHOULD_SYNC);
    273                     values.put(Settings.UNGROUPED_VISIBLE, DEFAULT_VISIBLE);
    274                     return fromAfter(values).setUngrouped(accountHasGroups);
    275                 }
    276             } finally {
    277                 if (cursor != null) cursor.close();
    278             }
    279         }
    280 
    281         public static GroupDelta fromBefore(ContentValues before) {
    282             final GroupDelta entry = new GroupDelta();
    283             entry.mBefore = before;
    284             entry.mAfter = new ContentValues();
    285             return entry;
    286         }
    287 
    288         public static GroupDelta fromAfter(ContentValues after) {
    289             final GroupDelta entry = new GroupDelta();
    290             entry.mBefore = null;
    291             entry.mAfter = after;
    292             return entry;
    293         }
    294 
    295         protected GroupDelta setUngrouped(boolean accountHasGroups) {
    296             mUngrouped = true;
    297             mAccountHasGroups = accountHasGroups;
    298             return this;
    299         }
    300 
    301         @Override
    302         public boolean beforeExists() {
    303             return mBefore != null;
    304         }
    305 
    306         public boolean getShouldSync() {
    307             return getAsInteger(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC,
    308                     DEFAULT_SHOULD_SYNC) != 0;
    309         }
    310 
    311         public boolean getVisible() {
    312             return getAsInteger(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE,
    313                     DEFAULT_VISIBLE) != 0;
    314         }
    315 
    316         public void putShouldSync(boolean shouldSync) {
    317             put(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC, shouldSync ? 1 : 0);
    318         }
    319 
    320         public void putVisible(boolean visible) {
    321             put(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE, visible ? 1 : 0);
    322         }
    323 
    324         private String getAccountType() {
    325             return (mBefore == null ? mAfter : mBefore).getAsString(Settings.ACCOUNT_TYPE);
    326         }
    327 
    328         public CharSequence getTitle(Context context) {
    329             if (mUngrouped) {
    330                 final String customAllContactsName =
    331                         LocalizedNameResolver.getAllContactsName(context, getAccountType());
    332                 if (customAllContactsName != null) {
    333                     return customAllContactsName;
    334                 }
    335                 if (mAccountHasGroups) {
    336                     return context.getText(R.string.display_ungrouped);
    337                 } else {
    338                     return context.getText(R.string.display_all_contacts);
    339                 }
    340             } else {
    341                 final Integer titleRes = getAsInteger(Groups.TITLE_RES);
    342                 if (titleRes != null && titleRes != 0) {
    343                     final String packageName = getAsString(Groups.RES_PACKAGE);
    344                     if (packageName != null) {
    345                         return context.getPackageManager().getText(packageName, titleRes, null);
    346                     }
    347                 }
    348                 return getAsString(Groups.TITLE);
    349             }
    350         }
    351 
    352         /**
    353          * Build a possible {@link ContentProviderOperation} to persist any
    354          * changes to the {@link Groups} or {@link Settings} row described by
    355          * this {@link GroupDelta}.
    356          */
    357         public ContentProviderOperation buildDiff() {
    358             if (isInsert()) {
    359                 // Only allow inserts for Settings
    360                 if (mUngrouped) {
    361                     mAfter.remove(mIdColumn);
    362                     return ContentProviderOperation.newInsert(Settings.CONTENT_URI)
    363                             .withValues(mAfter)
    364                             .build();
    365                 }
    366                 else {
    367                     throw new IllegalStateException("Unexpected diff");
    368                 }
    369             } else if (isUpdate()) {
    370                 if (mUngrouped) {
    371                     String accountName = this.getAsString(Settings.ACCOUNT_NAME);
    372                     String accountType = this.getAsString(Settings.ACCOUNT_TYPE);
    373                     String dataSet = this.getAsString(Settings.DATA_SET);
    374                     StringBuilder selection = new StringBuilder(Settings.ACCOUNT_NAME + "=? AND "
    375                             + Settings.ACCOUNT_TYPE + "=?");
    376                     String[] selectionArgs;
    377                     if (dataSet == null) {
    378                         selection.append(" AND " + Settings.DATA_SET + " IS NULL");
    379                         selectionArgs = new String[] {accountName, accountType};
    380                     } else {
    381                         selection.append(" AND " + Settings.DATA_SET + "=?");
    382                         selectionArgs = new String[] {accountName, accountType, dataSet};
    383                     }
    384                     return ContentProviderOperation.newUpdate(Settings.CONTENT_URI)
    385                             .withSelection(selection.toString(), selectionArgs)
    386                             .withValues(mAfter)
    387                             .build();
    388                 } else {
    389                     return ContentProviderOperation.newUpdate(
    390                                     addCallerIsSyncAdapterParameter(Groups.CONTENT_URI))
    391                             .withSelection(Groups._ID + "=" + this.getId(), null)
    392                             .withValues(mAfter)
    393                             .build();
    394                 }
    395             } else {
    396                 return null;
    397             }
    398         }
    399     }
    400 
    401     private static Uri addCallerIsSyncAdapterParameter(Uri uri) {
    402         return uri.buildUpon()
    403             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
    404             .build();
    405     }
    406 
    407     /**
    408      * {@link Comparator} to sort by {@link Groups#_ID}.
    409      */
    410     private static Comparator<GroupDelta> sIdComparator = new Comparator<GroupDelta>() {
    411         public int compare(GroupDelta object1, GroupDelta object2) {
    412             final Long id1 = object1.getId();
    413             final Long id2 = object2.getId();
    414             if (id1 == null && id2 == null) {
    415                 return 0;
    416             } else if (id1 == null) {
    417                 return -1;
    418             } else if (id2 == null) {
    419                 return 1;
    420             } else if (id1 < id2) {
    421                 return -1;
    422             } else if (id1 > id2) {
    423                 return 1;
    424             } else {
    425                 return 0;
    426             }
    427         }
    428     };
    429 
    430     /**
    431      * Set of all {@link AccountDisplay} entries, one for each source.
    432      */
    433     protected static class AccountSet extends ArrayList<AccountDisplay> {
    434         public ArrayList<ContentProviderOperation> buildDiff() {
    435             final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
    436             for (AccountDisplay account : this) {
    437                 account.buildDiff(diff);
    438             }
    439             return diff;
    440         }
    441     }
    442 
    443     /**
    444      * {@link GroupDelta} details for a single {@link AccountWithDataSet}, usually shown as
    445      * children under a single expandable group.
    446      */
    447     protected static class AccountDisplay {
    448         public final String mName;
    449         public final String mType;
    450         public final String mDataSet;
    451         public final AccountInfo mAccountInfo;
    452 
    453         public GroupDelta mUngrouped;
    454         public ArrayList<GroupDelta> mSyncedGroups = Lists.newArrayList();
    455         public ArrayList<GroupDelta> mUnsyncedGroups = Lists.newArrayList();
    456 
    457         public GroupDelta getGroup(int position) {
    458             if (position < mSyncedGroups.size()) {
    459                 return mSyncedGroups.get(position);
    460             }
    461             position -= mSyncedGroups.size();
    462             return mUnsyncedGroups.get(position);
    463         }
    464 
    465         /**
    466          * Build an {@link AccountDisplay} covering all {@link Groups} under the
    467          * given {@link AccountWithDataSet}.
    468          */
    469         public AccountDisplay(ContentResolver resolver, AccountInfo accountInfo) {
    470             mName = accountInfo.getAccount().name;
    471             mType = accountInfo.getAccount().type;
    472             mDataSet = accountInfo.getAccount().dataSet;
    473             mAccountInfo = accountInfo;
    474         }
    475 
    476         /**
    477          * Add the given {@link GroupDelta} internally, filing based on its
    478          * {@link GroupDelta#getShouldSync()} status.
    479          */
    480         private void addGroup(GroupDelta group) {
    481             if (group.getShouldSync()) {
    482                 mSyncedGroups.add(group);
    483             } else {
    484                 mUnsyncedGroups.add(group);
    485             }
    486         }
    487 
    488         /**
    489          * Set the {@link GroupDelta#putShouldSync(boolean)} value for all
    490          * children {@link GroupDelta} rows.
    491          */
    492         public void setShouldSync(boolean shouldSync) {
    493             final Iterator<GroupDelta> oppositeChildren = shouldSync ?
    494                     mUnsyncedGroups.iterator() : mSyncedGroups.iterator();
    495             while (oppositeChildren.hasNext()) {
    496                 final GroupDelta child = oppositeChildren.next();
    497                 setShouldSync(child, shouldSync, false);
    498                 oppositeChildren.remove();
    499             }
    500         }
    501 
    502         public void setShouldSync(GroupDelta child, boolean shouldSync) {
    503             setShouldSync(child, shouldSync, true);
    504         }
    505 
    506         /**
    507          * Set {@link GroupDelta#putShouldSync(boolean)}, and file internally
    508          * based on updated state.
    509          */
    510         public void setShouldSync(GroupDelta child, boolean shouldSync, boolean attemptRemove) {
    511             child.putShouldSync(shouldSync);
    512             if (shouldSync) {
    513                 if (attemptRemove) {
    514                     mUnsyncedGroups.remove(child);
    515                 }
    516                 mSyncedGroups.add(child);
    517                 Collections.sort(mSyncedGroups, sIdComparator);
    518             } else {
    519                 if (attemptRemove) {
    520                     mSyncedGroups.remove(child);
    521                 }
    522                 mUnsyncedGroups.add(child);
    523             }
    524         }
    525 
    526         /**
    527          * Build set of {@link ContentProviderOperation} to persist any user
    528          * changes to {@link GroupDelta} rows under this {@link AccountWithDataSet}.
    529          */
    530         public void buildDiff(ArrayList<ContentProviderOperation> diff) {
    531             for (GroupDelta group : mSyncedGroups) {
    532                 final ContentProviderOperation oper = group.buildDiff();
    533                 if (oper != null) diff.add(oper);
    534             }
    535             for (GroupDelta group : mUnsyncedGroups) {
    536                 final ContentProviderOperation oper = group.buildDiff();
    537                 if (oper != null) diff.add(oper);
    538             }
    539         }
    540     }
    541 
    542     /**
    543      * {@link ExpandableListAdapter} that shows {@link GroupDelta} settings,
    544      * grouped by {@link AccountWithDataSet} type. Shows footer row when any groups are
    545      * unsynced, as determined through {@link AccountDisplay#mUnsyncedGroups}.
    546      */
    547     protected static class DisplayAdapter extends BaseExpandableListAdapter {
    548         private Context mContext;
    549         private LayoutInflater mInflater;
    550         private AccountTypeManager mAccountTypes;
    551         private AccountSet mAccounts;
    552 
    553         private boolean mChildWithPhones = false;
    554 
    555         public DisplayAdapter(Context context) {
    556             mContext = context;
    557             mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    558             mAccountTypes = AccountTypeManager.getInstance(context);
    559         }
    560 
    561         public void setAccounts(AccountSet accounts) {
    562             mAccounts = accounts;
    563             notifyDataSetChanged();
    564         }
    565 
    566         /**
    567          * In group descriptions, show the number of contacts with phone
    568          * numbers, in addition to the total contacts.
    569          */
    570         public void setChildDescripWithPhones(boolean withPhones) {
    571             mChildWithPhones = withPhones;
    572         }
    573 
    574         @Override
    575         public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
    576                 ViewGroup parent) {
    577             if (convertView == null) {
    578                 convertView = mInflater.inflate(
    579                         R.layout.custom_contact_list_filter_account, parent, false);
    580             }
    581 
    582             final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
    583             final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
    584 
    585             final AccountDisplay account = (AccountDisplay)this.getGroup(groupPosition);
    586 
    587             text1.setText(account.mAccountInfo.getNameLabel());
    588             text1.setVisibility(!account.mAccountInfo.isDeviceAccount()
    589                     || account.mAccountInfo.hasDistinctName()
    590                     ? View.VISIBLE : View.GONE);
    591             text2.setText(account.mAccountInfo.getTypeLabel());
    592 
    593             final int textColor = mContext.getResources().getColor(isExpanded
    594                     ? R.color.dialtacts_theme_color
    595                     : R.color.account_filter_text_color);
    596             text1.setTextColor(textColor);
    597             text2.setTextColor(textColor);
    598 
    599             return convertView;
    600         }
    601 
    602         @Override
    603         public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
    604                 View convertView, ViewGroup parent) {
    605             if (convertView == null) {
    606                 convertView = mInflater.inflate(
    607                         R.layout.custom_contact_list_filter_group, parent, false);
    608             }
    609 
    610             final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
    611             final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
    612             final CheckBox checkbox = (CheckBox)convertView.findViewById(android.R.id.checkbox);
    613 
    614             final AccountDisplay account = mAccounts.get(groupPosition);
    615             final GroupDelta child = (GroupDelta)this.getChild(groupPosition, childPosition);
    616             if (child != null) {
    617                 // Handle normal group, with title and checkbox
    618                 final boolean groupVisible = child.getVisible();
    619                 checkbox.setVisibility(View.VISIBLE);
    620                 checkbox.setChecked(groupVisible);
    621 
    622                 final CharSequence groupTitle = child.getTitle(mContext);
    623                 text1.setText(groupTitle);
    624                 text2.setVisibility(View.GONE);
    625             } else {
    626                 // When unknown child, this is "more" footer view
    627                 checkbox.setVisibility(View.GONE);
    628                 text1.setText(R.string.display_more_groups);
    629                 text2.setVisibility(View.GONE);
    630             }
    631 
    632             // Show divider at bottom only for the last child.
    633             final View dividerBottom = convertView.findViewById(R.id.adapter_divider_bottom);
    634             dividerBottom.setVisibility(isLastChild ? View.VISIBLE : View.GONE);
    635 
    636             return convertView;
    637         }
    638 
    639         @Override
    640         public Object getChild(int groupPosition, int childPosition) {
    641             final AccountDisplay account = mAccounts.get(groupPosition);
    642             final boolean validChild = childPosition >= 0
    643                     && childPosition < account.mSyncedGroups.size()
    644                     + account.mUnsyncedGroups.size();
    645             if (validChild) {
    646                 return account.getGroup(childPosition);
    647             } else {
    648                 return null;
    649             }
    650         }
    651 
    652         @Override
    653         public long getChildId(int groupPosition, int childPosition) {
    654             final GroupDelta child = (GroupDelta)getChild(groupPosition, childPosition);
    655             if (child != null) {
    656                 final Long childId = child.getId();
    657                 return childId != null ? childId : Long.MIN_VALUE;
    658             } else {
    659                 return Long.MIN_VALUE;
    660             }
    661         }
    662 
    663         @Override
    664         public int getChildrenCount(int groupPosition) {
    665             // Count is any synced groups, plus possible footer
    666             final AccountDisplay account = mAccounts.get(groupPosition);
    667             return account.mSyncedGroups.size() + account.mUnsyncedGroups.size();
    668         }
    669 
    670         @Override
    671         public Object getGroup(int groupPosition) {
    672             return mAccounts.get(groupPosition);
    673         }
    674 
    675         @Override
    676         public int getGroupCount() {
    677             if (mAccounts == null) {
    678                 return 0;
    679             }
    680             return mAccounts.size();
    681         }
    682 
    683         @Override
    684         public long getGroupId(int groupPosition) {
    685             return groupPosition;
    686         }
    687 
    688         @Override
    689         public boolean hasStableIds() {
    690             return true;
    691         }
    692 
    693         @Override
    694         public boolean isChildSelectable(int groupPosition, int childPosition) {
    695             return true;
    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     private boolean hasUnsavedChanges() {
    830         if (mAdapter == null || mAdapter.mAccounts == null) {
    831             return false;
    832         }
    833         if (getCurrentListFilterType() != ContactListFilter.FILTER_TYPE_CUSTOM) {
    834             return true;
    835         }
    836         final ArrayList<ContentProviderOperation> diff = mAdapter.mAccounts.buildDiff();
    837         if (diff.isEmpty()) {
    838             return false;
    839         }
    840         return true;
    841     }
    842 
    843     @SuppressWarnings("unchecked")
    844     private void doSaveAction() {
    845         if (mAdapter == null || mAdapter.mAccounts == null) {
    846             finish();
    847             return;
    848         }
    849 
    850         setResult(RESULT_OK);
    851 
    852         final ArrayList<ContentProviderOperation> diff = mAdapter.mAccounts.buildDiff();
    853         if (diff.isEmpty()) {
    854             finish();
    855             return;
    856         }
    857 
    858         new UpdateTask(this).execute(diff);
    859     }
    860 
    861     /**
    862      * Background task that persists changes to {@link Groups#GROUP_VISIBLE},
    863      * showing spinner dialog to user while updating.
    864      */
    865     public static class UpdateTask extends
    866             WeakAsyncTask<ArrayList<ContentProviderOperation>, Void, Void, Activity> {
    867         private ProgressDialog mProgress;
    868 
    869         public UpdateTask(Activity target) {
    870             super(target);
    871         }
    872 
    873         /** {@inheritDoc} */
    874         @Override
    875         protected void onPreExecute(Activity target) {
    876             final Context context = target;
    877 
    878             mProgress = ProgressDialog.show(
    879                     context, null, context.getText(R.string.savingDisplayGroups));
    880 
    881             // Before starting this task, start an empty service to protect our
    882             // process from being reclaimed by the system.
    883             context.startService(new Intent(context, EmptyService.class));
    884         }
    885 
    886         /** {@inheritDoc} */
    887         @Override
    888         protected Void doInBackground(
    889                 Activity target, ArrayList<ContentProviderOperation>... params) {
    890             final Context context = target;
    891             final ContentValues values = new ContentValues();
    892             final ContentResolver resolver = context.getContentResolver();
    893 
    894             try {
    895                 final ArrayList<ContentProviderOperation> diff = params[0];
    896                 resolver.applyBatch(ContactsContract.AUTHORITY, diff);
    897             } catch (RemoteException e) {
    898                 Log.e(TAG, "Problem saving display groups", e);
    899             } catch (OperationApplicationException e) {
    900                 Log.e(TAG, "Problem saving display groups", e);
    901             }
    902 
    903             return null;
    904         }
    905 
    906         /** {@inheritDoc} */
    907         @Override
    908         protected void onPostExecute(Activity target, Void result) {
    909             final Context context = target;
    910 
    911             try {
    912                 mProgress.dismiss();
    913             } catch (Exception e) {
    914                 Log.e(TAG, "Error dismissing progress dialog", e);
    915             }
    916 
    917             target.finish();
    918 
    919             // Stop the service that was protecting us
    920             context.stopService(new Intent(context, EmptyService.class));
    921         }
    922     }
    923 
    924     @Override
    925     public boolean onCreateOptionsMenu(Menu menu) {
    926         super.onCreateOptionsMenu(menu);
    927 
    928         final MenuItem menuItem = menu.add(Menu.NONE, R.id.menu_save, Menu.NONE,
    929                 R.string.menu_custom_filter_save);
    930         menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
    931 
    932         return true;
    933     }
    934 
    935     @Override
    936     public boolean onOptionsItemSelected(MenuItem item) {
    937         final int id = item.getItemId();
    938         if (id == android.R.id.home) {
    939             confirmFinish();
    940             return true;
    941         } else if (id == R.id.menu_save) {
    942             this.doSaveAction();
    943             return true;
    944         } else {
    945         }
    946         return super.onOptionsItemSelected(item);
    947     }
    948 
    949     @Override
    950     public void onBackPressed() {
    951         confirmFinish();
    952     }
    953 
    954     private void confirmFinish() {
    955         // Prompt the user whether they want to discard there customizations unless
    956         // nothing will be changed.
    957         if (hasUnsavedChanges()) {
    958             new ConfirmNavigationDialogFragment().show(getFragmentManager(),
    959                     "ConfirmNavigationDialog");
    960         } else {
    961             setResult(RESULT_CANCELED);
    962             finish();
    963         }
    964     }
    965 
    966     private int getCurrentListFilterType() {
    967         return getIntent().getIntExtra(EXTRA_CURRENT_LIST_FILTER_TYPE,
    968                 ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS);
    969     }
    970 
    971     public static class ConfirmNavigationDialogFragment
    972             extends DialogFragment implements DialogInterface.OnClickListener {
    973 
    974         @Override
    975         public Dialog onCreateDialog(Bundle savedInstanceState) {
    976             return new AlertDialog.Builder(getActivity(), getTheme())
    977                     .setMessage(R.string.leave_customize_confirmation_dialog_message)
    978                     .setNegativeButton(android.R.string.no, null)
    979                     .setPositiveButton(android.R.string.yes, this)
    980                     .create();
    981         }
    982 
    983         @Override
    984         public void onClick(DialogInterface dialogInterface, int i) {
    985             if (i == DialogInterface.BUTTON_POSITIVE) {
    986                 getActivity().setResult(RESULT_CANCELED);
    987                 getActivity().finish();
    988             }
    989         }
    990     }
    991 }
    992