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