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