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.common.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.common.R;
57import com.android.contacts.common.model.AccountTypeManager;
58import com.android.contacts.common.model.ValuesDelta;
59import com.android.contacts.common.model.account.AccountType;
60import com.android.contacts.common.model.account.AccountWithDataSet;
61import com.android.contacts.common.model.account.GoogleAccountType;
62import com.android.contacts.common.util.EmptyService;
63import com.android.contacts.common.util.LocalizedNameResolver;
64import com.android.contacts.common.util.WeakAsyncTask;
65import com.google.common.collect.Lists;
66
67import java.util.ArrayList;
68import java.util.Collections;
69import java.util.Comparator;
70import 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 */
76public 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