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