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