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.list; 18 19 import com.android.contacts.ContactsActivity; 20 import com.android.contacts.R; 21 import com.android.contacts.model.AccountType; 22 import com.android.contacts.model.AccountTypeManager; 23 import com.android.contacts.model.AccountWithDataSet; 24 import com.android.contacts.model.EntityDelta.ValuesDelta; 25 import com.android.contacts.model.GoogleAccountType; 26 import com.android.contacts.util.EmptyService; 27 import com.android.contacts.util.LocalizedNameResolver; 28 import com.android.contacts.util.WeakAsyncTask; 29 import com.google.android.collect.Lists; 30 31 import android.app.ActionBar; 32 import android.app.Activity; 33 import android.app.AlertDialog; 34 import android.app.LoaderManager.LoaderCallbacks; 35 import android.app.ProgressDialog; 36 import android.content.AsyncTaskLoader; 37 import android.content.ContentProviderOperation; 38 import android.content.ContentResolver; 39 import android.content.ContentValues; 40 import android.content.Context; 41 import android.content.DialogInterface; 42 import android.content.EntityIterator; 43 import android.content.Intent; 44 import android.content.Loader; 45 import android.content.OperationApplicationException; 46 import android.content.SharedPreferences; 47 import android.database.Cursor; 48 import android.net.Uri; 49 import android.os.Bundle; 50 import android.os.RemoteException; 51 import android.preference.PreferenceManager; 52 import android.provider.ContactsContract; 53 import android.provider.ContactsContract.Groups; 54 import android.provider.ContactsContract.Settings; 55 import android.util.Log; 56 import android.view.ContextMenu; 57 import android.view.LayoutInflater; 58 import android.view.MenuItem; 59 import android.view.MenuItem.OnMenuItemClickListener; 60 import android.view.View; 61 import android.view.ViewGroup; 62 import android.widget.BaseExpandableListAdapter; 63 import android.widget.CheckBox; 64 import android.widget.ExpandableListAdapter; 65 import android.widget.ExpandableListView; 66 import android.widget.ExpandableListView.ExpandableListContextMenuInfo; 67 import android.widget.TextView; 68 69 import java.util.ArrayList; 70 import java.util.Collections; 71 import java.util.Comparator; 72 import java.util.Iterator; 73 74 /** 75 * Shows a list of all available {@link Groups} available, letting the user 76 * select which ones they want to be visible. 77 */ 78 public class CustomContactListFilterActivity extends ContactsActivity 79 implements View.OnClickListener, ExpandableListView.OnChildClickListener, 80 LoaderCallbacks<CustomContactListFilterActivity.AccountSet> 81 { 82 private static final String TAG = "CustomContactListFilterActivity"; 83 84 private static final int ACCOUNT_SET_LOADER_ID = 1; 85 86 private ExpandableListView mList; 87 private DisplayAdapter mAdapter; 88 89 private SharedPreferences mPrefs; 90 91 @Override 92 protected void onCreate(Bundle icicle) { 93 super.onCreate(icicle); 94 setContentView(R.layout.contact_list_filter_custom); 95 96 mList = (ExpandableListView) findViewById(com.android.internal.R.id.list); 97 mList.setOnChildClickListener(this); 98 mList.setHeaderDividersEnabled(true); 99 mPrefs = PreferenceManager.getDefaultSharedPreferences(this); 100 mAdapter = new DisplayAdapter(this); 101 102 final LayoutInflater inflater = getLayoutInflater(); 103 104 findViewById(R.id.btn_done).setOnClickListener(this); 105 findViewById(R.id.btn_discard).setOnClickListener(this); 106 107 mList.setOnCreateContextMenuListener(this); 108 109 mList.setAdapter(mAdapter); 110 111 ActionBar actionBar = getActionBar(); 112 if (actionBar != null) { 113 // android.R.id.home will be triggered in onOptionsItemSelected() 114 actionBar.setDisplayHomeAsUpEnabled(true); 115 } 116 } 117 118 public static class CustomFilterConfigurationLoader extends AsyncTaskLoader<AccountSet> { 119 120 private AccountSet mAccountSet; 121 122 public CustomFilterConfigurationLoader(Context context) { 123 super(context); 124 } 125 126 @Override 127 public AccountSet loadInBackground() { 128 Context context = getContext(); 129 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(context); 130 final ContentResolver resolver = context.getContentResolver(); 131 132 final AccountSet accounts = new AccountSet(); 133 for (AccountWithDataSet account : accountTypes.getAccounts(false)) { 134 final AccountType accountType = accountTypes.getAccountTypeForAccount(account); 135 if (accountType.isExtension() && !account.hasData(context)) { 136 // Extension with no data -- skip. 137 continue; 138 } 139 140 AccountDisplay accountDisplay = 141 new AccountDisplay(resolver, account.name, account.type, account.dataSet); 142 143 final Uri.Builder groupsUri = Groups.CONTENT_URI.buildUpon() 144 .appendQueryParameter(Groups.ACCOUNT_NAME, account.name) 145 .appendQueryParameter(Groups.ACCOUNT_TYPE, account.type); 146 if (account.dataSet != null) { 147 groupsUri.appendQueryParameter(Groups.DATA_SET, account.dataSet).build(); 148 } 149 EntityIterator iterator = 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