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