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.Dialog;
23import android.app.DialogFragment;
24import android.app.LoaderManager.LoaderCallbacks;
25import android.app.ProgressDialog;
26import android.content.ContentProviderOperation;
27import android.content.ContentResolver;
28import android.content.ContentValues;
29import android.content.Context;
30import android.content.DialogInterface;
31import android.content.Intent;
32import android.content.IntentFilter;
33import android.content.Loader;
34import android.content.OperationApplicationException;
35import android.database.Cursor;
36import android.graphics.Color;
37import android.graphics.drawable.ColorDrawable;
38import android.net.Uri;
39import android.os.Bundle;
40import android.os.RemoteException;
41import android.provider.ContactsContract;
42import android.provider.ContactsContract.Groups;
43import android.provider.ContactsContract.Settings;
44import android.util.Log;
45import android.view.ContextMenu;
46import android.view.LayoutInflater;
47import android.view.Menu;
48import android.view.MenuItem;
49import android.view.MenuItem.OnMenuItemClickListener;
50import android.view.View;
51import android.view.ViewGroup;
52import android.widget.BaseExpandableListAdapter;
53import android.widget.CheckBox;
54import android.widget.ExpandableListAdapter;
55import android.widget.ExpandableListView;
56import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
57import android.widget.TextView;
58
59import com.android.contacts.R;
60import com.android.contacts.model.AccountTypeManager;
61import com.android.contacts.model.ValuesDelta;
62import com.android.contacts.model.account.AccountInfo;
63import com.android.contacts.model.account.AccountWithDataSet;
64import com.android.contacts.model.account.GoogleAccountType;
65import com.android.contacts.util.EmptyService;
66import com.android.contacts.util.LocalizedNameResolver;
67import com.android.contacts.util.WeakAsyncTask;
68import com.android.contacts.util.concurrent.ContactsExecutors;
69import com.android.contacts.util.concurrent.ListenableFutureLoader;
70import com.google.common.base.Function;
71import com.google.common.collect.Lists;
72import com.google.common.util.concurrent.Futures;
73import com.google.common.util.concurrent.ListenableFuture;
74
75import java.util.ArrayList;
76import java.util.Collections;
77import java.util.Comparator;
78import java.util.Iterator;
79import java.util.List;
80
81import 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 */
87public 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