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.Dialog; 23import android.app.DialogFragment; 24import android.app.LoaderManager.LoaderCallbacks; 25import android.app.ProgressDialog; 26import android.content.ContentProviderOperation; 27import android.content.ContentResolver; 28import android.content.ContentValues; 29import android.content.Context; 30import android.content.DialogInterface; 31import android.content.Intent; 32import android.content.IntentFilter; 33import android.content.Loader; 34import android.content.OperationApplicationException; 35import android.database.Cursor; 36import android.graphics.Color; 37import android.graphics.drawable.ColorDrawable; 38import android.net.Uri; 39import android.os.Bundle; 40import android.os.RemoteException; 41import android.provider.ContactsContract; 42import android.provider.ContactsContract.Groups; 43import android.provider.ContactsContract.Settings; 44import android.util.Log; 45import android.view.ContextMenu; 46import android.view.LayoutInflater; 47import android.view.Menu; 48import android.view.MenuItem; 49import android.view.MenuItem.OnMenuItemClickListener; 50import android.view.View; 51import android.view.ViewGroup; 52import android.widget.BaseExpandableListAdapter; 53import android.widget.CheckBox; 54import android.widget.ExpandableListAdapter; 55import android.widget.ExpandableListView; 56import android.widget.ExpandableListView.ExpandableListContextMenuInfo; 57import android.widget.TextView; 58 59import com.android.contacts.R; 60import com.android.contacts.model.AccountTypeManager; 61import com.android.contacts.model.ValuesDelta; 62import com.android.contacts.model.account.AccountInfo; 63import com.android.contacts.model.account.AccountWithDataSet; 64import com.android.contacts.model.account.GoogleAccountType; 65import com.android.contacts.util.EmptyService; 66import com.android.contacts.util.LocalizedNameResolver; 67import com.android.contacts.util.WeakAsyncTask; 68import com.android.contacts.util.concurrent.ContactsExecutors; 69import com.android.contacts.util.concurrent.ListenableFutureLoader; 70import com.google.common.base.Function; 71import com.google.common.collect.Lists; 72import com.google.common.util.concurrent.Futures; 73import com.google.common.util.concurrent.ListenableFuture; 74 75import java.util.ArrayList; 76import java.util.Collections; 77import java.util.Comparator; 78import java.util.Iterator; 79import java.util.List; 80 81import javax.annotation.Nullable; 82 83/** 84 * Shows a list of all available {@link Groups} available, letting the user 85 * select which ones they want to be visible. 86 */ 87public class CustomContactListFilterActivity extends Activity implements 88 ExpandableListView.OnChildClickListener, 89 LoaderCallbacks<CustomContactListFilterActivity.AccountSet> { 90 private static final String TAG = "CustomContactListFilter"; 91 92 public static final String EXTRA_CURRENT_LIST_FILTER_TYPE = "currentListFilterType"; 93 94 private static final int ACCOUNT_SET_LOADER_ID = 1; 95 96 private ExpandableListView mList; 97 private DisplayAdapter mAdapter; 98 99 @Override 100 protected void onCreate(Bundle icicle) { 101 super.onCreate(icicle); 102 setContentView(R.layout.contact_list_filter_custom); 103 104 mList = (ExpandableListView) findViewById(android.R.id.list); 105 mList.setOnChildClickListener(this); 106 mList.setHeaderDividersEnabled(true); 107 mList.setChildDivider(new ColorDrawable(Color.TRANSPARENT)); 108 109 mList.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 110 @Override 111 public void onLayoutChange(final View v, final int left, final int top, final int right, 112 final int bottom, final int oldLeft, final int oldTop, final int oldRight, 113 final int oldBottom) { 114 mList.setIndicatorBounds( 115 mList.getWidth() - getResources().getDimensionPixelSize( 116 R.dimen.contact_filter_indicator_padding_end), 117 mList.getWidth() - getResources().getDimensionPixelSize( 118 R.dimen.contact_filter_indicator_padding_start)); 119 } 120 }); 121 122 mAdapter = new DisplayAdapter(this); 123 124 mList.setOnCreateContextMenuListener(this); 125 126 mList.setAdapter(mAdapter); 127 128 ActionBar actionBar = getActionBar(); 129 if (actionBar != null) { 130 // android.R.id.home will be triggered in onOptionsItemSelected() 131 actionBar.setDisplayHomeAsUpEnabled(true); 132 } 133 } 134 135 public static class CustomFilterConfigurationLoader extends ListenableFutureLoader<AccountSet> { 136 137 private AccountTypeManager mAccountTypeManager; 138 139 public CustomFilterConfigurationLoader(Context context) { 140 super(context, new IntentFilter(AccountTypeManager.BROADCAST_ACCOUNTS_CHANGED)); 141 mAccountTypeManager = AccountTypeManager.getInstance(context); 142 } 143 144 @Override 145 public ListenableFuture<AccountSet> loadData() { 146 return Futures.transform(mAccountTypeManager.getAccountsAsync(), 147 new Function<List<AccountInfo>, AccountSet>() { 148 @Nullable 149 @Override 150 public AccountSet apply(@Nullable List<AccountInfo> input) { 151 return createAccountSet(input); 152 } 153 }, ContactsExecutors.getDefaultThreadPoolExecutor()); 154 } 155 156 private AccountSet createAccountSet(List<AccountInfo> sourceAccounts) { 157 final Context context = getContext(); 158 final ContentResolver resolver = context.getContentResolver(); 159 160 final AccountSet accounts = new AccountSet(); 161 162 // Don't include the null account because it doesn't support writing to 163 // ContactsContract.Settings 164 for (AccountInfo info : sourceAccounts) { 165 final AccountWithDataSet account = info.getAccount(); 166 final AccountDisplay accountDisplay = new AccountDisplay(resolver, info); 167 168 final Uri.Builder groupsUri = Groups.CONTENT_URI.buildUpon() 169 .appendQueryParameter(Groups.ACCOUNT_NAME, account.name) 170 .appendQueryParameter(Groups.ACCOUNT_TYPE, account.type); 171 if (account.dataSet != null) { 172 groupsUri.appendQueryParameter(Groups.DATA_SET, account.dataSet).build(); 173 } 174 final Cursor cursor = resolver.query(groupsUri.build(), null, null, null, null); 175 if (cursor == null) { 176 continue; 177 } 178 android.content.EntityIterator iterator = 179 ContactsContract.Groups.newEntityIterator(cursor); 180 try { 181 boolean hasGroups = false; 182 183 // Create entries for each known group 184 while (iterator.hasNext()) { 185 final ContentValues values = iterator.next().getEntityValues(); 186 final GroupDelta group = GroupDelta.fromBefore(values); 187 accountDisplay.addGroup(group); 188 hasGroups = true; 189 } 190 // Create single entry handling ungrouped status 191 accountDisplay.mUngrouped = 192 GroupDelta.fromSettings(resolver, account.name, account.type, 193 account.dataSet, hasGroups); 194 accountDisplay.addGroup(accountDisplay.mUngrouped); 195 } finally { 196 iterator.close(); 197 } 198 199 accounts.add(accountDisplay); 200 } 201 202 return accounts; 203 } 204 } 205 206 @Override 207 protected void onStart() { 208 getLoaderManager().initLoader(ACCOUNT_SET_LOADER_ID, null, this); 209 super.onStart(); 210 } 211 212 @Override 213 public Loader<AccountSet> onCreateLoader(int id, Bundle args) { 214 return new CustomFilterConfigurationLoader(this); 215 } 216 217 @Override 218 public void onLoadFinished(Loader<AccountSet> loader, AccountSet data) { 219 mAdapter.setAccounts(data); 220 } 221 222 @Override 223 public void onLoaderReset(Loader<AccountSet> loader) { 224 mAdapter.setAccounts(null); 225 } 226 227 private static final int DEFAULT_SHOULD_SYNC = 1; 228 private static final int DEFAULT_VISIBLE = 0; 229 230 /** 231 * Entry holding any changes to {@link Groups} or {@link Settings} rows, 232 * such as {@link Groups#SHOULD_SYNC} or {@link Groups#GROUP_VISIBLE}. 233 */ 234 protected static class GroupDelta extends ValuesDelta { 235 private boolean mUngrouped = false; 236 private boolean mAccountHasGroups; 237 238 private GroupDelta() { 239 super(); 240 } 241 242 /** 243 * Build {@link GroupDelta} from the {@link Settings} row for the given 244 * {@link Settings#ACCOUNT_NAME}, {@link Settings#ACCOUNT_TYPE}, and 245 * {@link Settings#DATA_SET}. 246 */ 247 public static GroupDelta fromSettings(ContentResolver resolver, String accountName, 248 String accountType, String dataSet, boolean accountHasGroups) { 249 final Uri.Builder settingsUri = Settings.CONTENT_URI.buildUpon() 250 .appendQueryParameter(Settings.ACCOUNT_NAME, accountName) 251 .appendQueryParameter(Settings.ACCOUNT_TYPE, accountType); 252 if (dataSet != null) { 253 settingsUri.appendQueryParameter(Settings.DATA_SET, dataSet); 254 } 255 final Cursor cursor = resolver.query(settingsUri.build(), new String[] { 256 Settings.SHOULD_SYNC, Settings.UNGROUPED_VISIBLE 257 }, null, null, null); 258 259 try { 260 final ContentValues values = new ContentValues(); 261 values.put(Settings.ACCOUNT_NAME, accountName); 262 values.put(Settings.ACCOUNT_TYPE, accountType); 263 values.put(Settings.DATA_SET, dataSet); 264 265 if (cursor != null && cursor.moveToFirst()) { 266 // Read existing values when present 267 values.put(Settings.SHOULD_SYNC, cursor.getInt(0)); 268 values.put(Settings.UNGROUPED_VISIBLE, cursor.getInt(1)); 269 return fromBefore(values).setUngrouped(accountHasGroups); 270 } else { 271 // Nothing found, so treat as create 272 values.put(Settings.SHOULD_SYNC, DEFAULT_SHOULD_SYNC); 273 values.put(Settings.UNGROUPED_VISIBLE, DEFAULT_VISIBLE); 274 return fromAfter(values).setUngrouped(accountHasGroups); 275 } 276 } finally { 277 if (cursor != null) cursor.close(); 278 } 279 } 280 281 public static GroupDelta fromBefore(ContentValues before) { 282 final GroupDelta entry = new GroupDelta(); 283 entry.mBefore = before; 284 entry.mAfter = new ContentValues(); 285 return entry; 286 } 287 288 public static GroupDelta fromAfter(ContentValues after) { 289 final GroupDelta entry = new GroupDelta(); 290 entry.mBefore = null; 291 entry.mAfter = after; 292 return entry; 293 } 294 295 protected GroupDelta setUngrouped(boolean accountHasGroups) { 296 mUngrouped = true; 297 mAccountHasGroups = accountHasGroups; 298 return this; 299 } 300 301 @Override 302 public boolean beforeExists() { 303 return mBefore != null; 304 } 305 306 public boolean getShouldSync() { 307 return getAsInteger(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC, 308 DEFAULT_SHOULD_SYNC) != 0; 309 } 310 311 public boolean getVisible() { 312 return getAsInteger(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE, 313 DEFAULT_VISIBLE) != 0; 314 } 315 316 public void putShouldSync(boolean shouldSync) { 317 put(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC, shouldSync ? 1 : 0); 318 } 319 320 public void putVisible(boolean visible) { 321 put(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE, visible ? 1 : 0); 322 } 323 324 private String getAccountType() { 325 return (mBefore == null ? mAfter : mBefore).getAsString(Settings.ACCOUNT_TYPE); 326 } 327 328 public CharSequence getTitle(Context context) { 329 if (mUngrouped) { 330 final String customAllContactsName = 331 LocalizedNameResolver.getAllContactsName(context, getAccountType()); 332 if (customAllContactsName != null) { 333 return customAllContactsName; 334 } 335 if (mAccountHasGroups) { 336 return context.getText(R.string.display_ungrouped); 337 } else { 338 return context.getText(R.string.display_all_contacts); 339 } 340 } else { 341 final Integer titleRes = getAsInteger(Groups.TITLE_RES); 342 if (titleRes != null && titleRes != 0) { 343 final String packageName = getAsString(Groups.RES_PACKAGE); 344 if (packageName != null) { 345 return context.getPackageManager().getText(packageName, titleRes, null); 346 } 347 } 348 return getAsString(Groups.TITLE); 349 } 350 } 351 352 /** 353 * Build a possible {@link ContentProviderOperation} to persist any 354 * changes to the {@link Groups} or {@link Settings} row described by 355 * this {@link GroupDelta}. 356 */ 357 public ContentProviderOperation buildDiff() { 358 if (isInsert()) { 359 // Only allow inserts for Settings 360 if (mUngrouped) { 361 mAfter.remove(mIdColumn); 362 return ContentProviderOperation.newInsert(Settings.CONTENT_URI) 363 .withValues(mAfter) 364 .build(); 365 } 366 else { 367 throw new IllegalStateException("Unexpected diff"); 368 } 369 } else if (isUpdate()) { 370 if (mUngrouped) { 371 String accountName = this.getAsString(Settings.ACCOUNT_NAME); 372 String accountType = this.getAsString(Settings.ACCOUNT_TYPE); 373 String dataSet = this.getAsString(Settings.DATA_SET); 374 StringBuilder selection = new StringBuilder(Settings.ACCOUNT_NAME + "=? AND " 375 + Settings.ACCOUNT_TYPE + "=?"); 376 String[] selectionArgs; 377 if (dataSet == null) { 378 selection.append(" AND " + Settings.DATA_SET + " IS NULL"); 379 selectionArgs = new String[] {accountName, accountType}; 380 } else { 381 selection.append(" AND " + Settings.DATA_SET + "=?"); 382 selectionArgs = new String[] {accountName, accountType, dataSet}; 383 } 384 return ContentProviderOperation.newUpdate(Settings.CONTENT_URI) 385 .withSelection(selection.toString(), selectionArgs) 386 .withValues(mAfter) 387 .build(); 388 } else { 389 return ContentProviderOperation.newUpdate( 390 addCallerIsSyncAdapterParameter(Groups.CONTENT_URI)) 391 .withSelection(Groups._ID + "=" + this.getId(), null) 392 .withValues(mAfter) 393 .build(); 394 } 395 } else { 396 return null; 397 } 398 } 399 } 400 401 private static Uri addCallerIsSyncAdapterParameter(Uri uri) { 402 return uri.buildUpon() 403 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") 404 .build(); 405 } 406 407 /** 408 * {@link Comparator} to sort by {@link Groups#_ID}. 409 */ 410 private static Comparator<GroupDelta> sIdComparator = new Comparator<GroupDelta>() { 411 public int compare(GroupDelta object1, GroupDelta object2) { 412 final Long id1 = object1.getId(); 413 final Long id2 = object2.getId(); 414 if (id1 == null && id2 == null) { 415 return 0; 416 } else if (id1 == null) { 417 return -1; 418 } else if (id2 == null) { 419 return 1; 420 } else if (id1 < id2) { 421 return -1; 422 } else if (id1 > id2) { 423 return 1; 424 } else { 425 return 0; 426 } 427 } 428 }; 429 430 /** 431 * Set of all {@link AccountDisplay} entries, one for each source. 432 */ 433 protected static class AccountSet extends ArrayList<AccountDisplay> { 434 public ArrayList<ContentProviderOperation> buildDiff() { 435 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList(); 436 for (AccountDisplay account : this) { 437 account.buildDiff(diff); 438 } 439 return diff; 440 } 441 } 442 443 /** 444 * {@link GroupDelta} details for a single {@link AccountWithDataSet}, usually shown as 445 * children under a single expandable group. 446 */ 447 protected static class AccountDisplay { 448 public final String mName; 449 public final String mType; 450 public final String mDataSet; 451 public final AccountInfo mAccountInfo; 452 453 public GroupDelta mUngrouped; 454 public ArrayList<GroupDelta> mSyncedGroups = Lists.newArrayList(); 455 public ArrayList<GroupDelta> mUnsyncedGroups = Lists.newArrayList(); 456 457 public GroupDelta getGroup(int position) { 458 if (position < mSyncedGroups.size()) { 459 return mSyncedGroups.get(position); 460 } 461 position -= mSyncedGroups.size(); 462 return mUnsyncedGroups.get(position); 463 } 464 465 /** 466 * Build an {@link AccountDisplay} covering all {@link Groups} under the 467 * given {@link AccountWithDataSet}. 468 */ 469 public AccountDisplay(ContentResolver resolver, AccountInfo accountInfo) { 470 mName = accountInfo.getAccount().name; 471 mType = accountInfo.getAccount().type; 472 mDataSet = accountInfo.getAccount().dataSet; 473 mAccountInfo = accountInfo; 474 } 475 476 /** 477 * Add the given {@link GroupDelta} internally, filing based on its 478 * {@link GroupDelta#getShouldSync()} status. 479 */ 480 private void addGroup(GroupDelta group) { 481 if (group.getShouldSync()) { 482 mSyncedGroups.add(group); 483 } else { 484 mUnsyncedGroups.add(group); 485 } 486 } 487 488 /** 489 * Set the {@link GroupDelta#putShouldSync(boolean)} value for all 490 * children {@link GroupDelta} rows. 491 */ 492 public void setShouldSync(boolean shouldSync) { 493 final Iterator<GroupDelta> oppositeChildren = shouldSync ? 494 mUnsyncedGroups.iterator() : mSyncedGroups.iterator(); 495 while (oppositeChildren.hasNext()) { 496 final GroupDelta child = oppositeChildren.next(); 497 setShouldSync(child, shouldSync, false); 498 oppositeChildren.remove(); 499 } 500 } 501 502 public void setShouldSync(GroupDelta child, boolean shouldSync) { 503 setShouldSync(child, shouldSync, true); 504 } 505 506 /** 507 * Set {@link GroupDelta#putShouldSync(boolean)}, and file internally 508 * based on updated state. 509 */ 510 public void setShouldSync(GroupDelta child, boolean shouldSync, boolean attemptRemove) { 511 child.putShouldSync(shouldSync); 512 if (shouldSync) { 513 if (attemptRemove) { 514 mUnsyncedGroups.remove(child); 515 } 516 mSyncedGroups.add(child); 517 Collections.sort(mSyncedGroups, sIdComparator); 518 } else { 519 if (attemptRemove) { 520 mSyncedGroups.remove(child); 521 } 522 mUnsyncedGroups.add(child); 523 } 524 } 525 526 /** 527 * Build set of {@link ContentProviderOperation} to persist any user 528 * changes to {@link GroupDelta} rows under this {@link AccountWithDataSet}. 529 */ 530 public void buildDiff(ArrayList<ContentProviderOperation> diff) { 531 for (GroupDelta group : mSyncedGroups) { 532 final ContentProviderOperation oper = group.buildDiff(); 533 if (oper != null) diff.add(oper); 534 } 535 for (GroupDelta group : mUnsyncedGroups) { 536 final ContentProviderOperation oper = group.buildDiff(); 537 if (oper != null) diff.add(oper); 538 } 539 } 540 } 541 542 /** 543 * {@link ExpandableListAdapter} that shows {@link GroupDelta} settings, 544 * grouped by {@link AccountWithDataSet} type. Shows footer row when any groups are 545 * unsynced, as determined through {@link AccountDisplay#mUnsyncedGroups}. 546 */ 547 protected static class DisplayAdapter extends BaseExpandableListAdapter { 548 private Context mContext; 549 private LayoutInflater mInflater; 550 private AccountTypeManager mAccountTypes; 551 private AccountSet mAccounts; 552 553 private boolean mChildWithPhones = false; 554 555 public DisplayAdapter(Context context) { 556 mContext = context; 557 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 558 mAccountTypes = AccountTypeManager.getInstance(context); 559 } 560 561 public void setAccounts(AccountSet accounts) { 562 mAccounts = accounts; 563 notifyDataSetChanged(); 564 } 565 566 /** 567 * In group descriptions, show the number of contacts with phone 568 * numbers, in addition to the total contacts. 569 */ 570 public void setChildDescripWithPhones(boolean withPhones) { 571 mChildWithPhones = withPhones; 572 } 573 574 @Override 575 public View getGroupView(int groupPosition, boolean isExpanded, View convertView, 576 ViewGroup parent) { 577 if (convertView == null) { 578 convertView = mInflater.inflate( 579 R.layout.custom_contact_list_filter_account, parent, false); 580 } 581 582 final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1); 583 final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2); 584 585 final AccountDisplay account = (AccountDisplay)this.getGroup(groupPosition); 586 587 text1.setText(account.mAccountInfo.getNameLabel()); 588 text1.setVisibility(!account.mAccountInfo.isDeviceAccount() 589 || account.mAccountInfo.hasDistinctName() 590 ? View.VISIBLE : View.GONE); 591 text2.setText(account.mAccountInfo.getTypeLabel()); 592 593 final int textColor = mContext.getResources().getColor(isExpanded 594 ? R.color.dialtacts_theme_color 595 : R.color.account_filter_text_color); 596 text1.setTextColor(textColor); 597 text2.setTextColor(textColor); 598 599 return convertView; 600 } 601 602 @Override 603 public View getChildView(int groupPosition, int childPosition, boolean isLastChild, 604 View convertView, ViewGroup parent) { 605 if (convertView == null) { 606 convertView = mInflater.inflate( 607 R.layout.custom_contact_list_filter_group, parent, false); 608 } 609 610 final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1); 611 final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2); 612 final CheckBox checkbox = (CheckBox)convertView.findViewById(android.R.id.checkbox); 613 614 final AccountDisplay account = mAccounts.get(groupPosition); 615 final GroupDelta child = (GroupDelta)this.getChild(groupPosition, childPosition); 616 if (child != null) { 617 // Handle normal group, with title and checkbox 618 final boolean groupVisible = child.getVisible(); 619 checkbox.setVisibility(View.VISIBLE); 620 checkbox.setChecked(groupVisible); 621 622 final CharSequence groupTitle = child.getTitle(mContext); 623 text1.setText(groupTitle); 624 text2.setVisibility(View.GONE); 625 } else { 626 // When unknown child, this is "more" footer view 627 checkbox.setVisibility(View.GONE); 628 text1.setText(R.string.display_more_groups); 629 text2.setVisibility(View.GONE); 630 } 631 632 // Show divider at bottom only for the last child. 633 final View dividerBottom = convertView.findViewById(R.id.adapter_divider_bottom); 634 dividerBottom.setVisibility(isLastChild ? View.VISIBLE : View.GONE); 635 636 return convertView; 637 } 638 639 @Override 640 public Object getChild(int groupPosition, int childPosition) { 641 final AccountDisplay account = mAccounts.get(groupPosition); 642 final boolean validChild = childPosition >= 0 643 && childPosition < account.mSyncedGroups.size() 644 + account.mUnsyncedGroups.size(); 645 if (validChild) { 646 return account.getGroup(childPosition); 647 } else { 648 return null; 649 } 650 } 651 652 @Override 653 public long getChildId(int groupPosition, int childPosition) { 654 final GroupDelta child = (GroupDelta)getChild(groupPosition, childPosition); 655 if (child != null) { 656 final Long childId = child.getId(); 657 return childId != null ? childId : Long.MIN_VALUE; 658 } else { 659 return Long.MIN_VALUE; 660 } 661 } 662 663 @Override 664 public int getChildrenCount(int groupPosition) { 665 // Count is any synced groups, plus possible footer 666 final AccountDisplay account = mAccounts.get(groupPosition); 667 return account.mSyncedGroups.size() + account.mUnsyncedGroups.size(); 668 } 669 670 @Override 671 public Object getGroup(int groupPosition) { 672 return mAccounts.get(groupPosition); 673 } 674 675 @Override 676 public int getGroupCount() { 677 if (mAccounts == null) { 678 return 0; 679 } 680 return mAccounts.size(); 681 } 682 683 @Override 684 public long getGroupId(int groupPosition) { 685 return groupPosition; 686 } 687 688 @Override 689 public boolean hasStableIds() { 690 return true; 691 } 692 693 @Override 694 public boolean isChildSelectable(int groupPosition, int childPosition) { 695 return true; 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 private boolean hasUnsavedChanges() { 830 if (mAdapter == null || mAdapter.mAccounts == null) { 831 return false; 832 } 833 if (getCurrentListFilterType() != ContactListFilter.FILTER_TYPE_CUSTOM) { 834 return true; 835 } 836 final ArrayList<ContentProviderOperation> diff = mAdapter.mAccounts.buildDiff(); 837 if (diff.isEmpty()) { 838 return false; 839 } 840 return true; 841 } 842 843 @SuppressWarnings("unchecked") 844 private void doSaveAction() { 845 if (mAdapter == null || mAdapter.mAccounts == null) { 846 finish(); 847 return; 848 } 849 850 setResult(RESULT_OK); 851 852 final ArrayList<ContentProviderOperation> diff = mAdapter.mAccounts.buildDiff(); 853 if (diff.isEmpty()) { 854 finish(); 855 return; 856 } 857 858 new UpdateTask(this).execute(diff); 859 } 860 861 /** 862 * Background task that persists changes to {@link Groups#GROUP_VISIBLE}, 863 * showing spinner dialog to user while updating. 864 */ 865 public static class UpdateTask extends 866 WeakAsyncTask<ArrayList<ContentProviderOperation>, Void, Void, Activity> { 867 private ProgressDialog mProgress; 868 869 public UpdateTask(Activity target) { 870 super(target); 871 } 872 873 /** {@inheritDoc} */ 874 @Override 875 protected void onPreExecute(Activity target) { 876 final Context context = target; 877 878 mProgress = ProgressDialog.show( 879 context, null, context.getText(R.string.savingDisplayGroups)); 880 881 // Before starting this task, start an empty service to protect our 882 // process from being reclaimed by the system. 883 context.startService(new Intent(context, EmptyService.class)); 884 } 885 886 /** {@inheritDoc} */ 887 @Override 888 protected Void doInBackground( 889 Activity target, ArrayList<ContentProviderOperation>... params) { 890 final Context context = target; 891 final ContentValues values = new ContentValues(); 892 final ContentResolver resolver = context.getContentResolver(); 893 894 try { 895 final ArrayList<ContentProviderOperation> diff = params[0]; 896 resolver.applyBatch(ContactsContract.AUTHORITY, diff); 897 } catch (RemoteException e) { 898 Log.e(TAG, "Problem saving display groups", e); 899 } catch (OperationApplicationException e) { 900 Log.e(TAG, "Problem saving display groups", e); 901 } 902 903 return null; 904 } 905 906 /** {@inheritDoc} */ 907 @Override 908 protected void onPostExecute(Activity target, Void result) { 909 final Context context = target; 910 911 try { 912 mProgress.dismiss(); 913 } catch (Exception e) { 914 Log.e(TAG, "Error dismissing progress dialog", e); 915 } 916 917 target.finish(); 918 919 // Stop the service that was protecting us 920 context.stopService(new Intent(context, EmptyService.class)); 921 } 922 } 923 924 @Override 925 public boolean onCreateOptionsMenu(Menu menu) { 926 super.onCreateOptionsMenu(menu); 927 928 final MenuItem menuItem = menu.add(Menu.NONE, R.id.menu_save, Menu.NONE, 929 R.string.menu_custom_filter_save); 930 menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 931 932 return true; 933 } 934 935 @Override 936 public boolean onOptionsItemSelected(MenuItem item) { 937 final int id = item.getItemId(); 938 if (id == android.R.id.home) { 939 confirmFinish(); 940 return true; 941 } else if (id == R.id.menu_save) { 942 this.doSaveAction(); 943 return true; 944 } else { 945 } 946 return super.onOptionsItemSelected(item); 947 } 948 949 @Override 950 public void onBackPressed() { 951 confirmFinish(); 952 } 953 954 private void confirmFinish() { 955 // Prompt the user whether they want to discard there customizations unless 956 // nothing will be changed. 957 if (hasUnsavedChanges()) { 958 new ConfirmNavigationDialogFragment().show(getFragmentManager(), 959 "ConfirmNavigationDialog"); 960 } else { 961 setResult(RESULT_CANCELED); 962 finish(); 963 } 964 } 965 966 private int getCurrentListFilterType() { 967 return getIntent().getIntExtra(EXTRA_CURRENT_LIST_FILTER_TYPE, 968 ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS); 969 } 970 971 public static class ConfirmNavigationDialogFragment 972 extends DialogFragment implements DialogInterface.OnClickListener { 973 974 @Override 975 public Dialog onCreateDialog(Bundle savedInstanceState) { 976 return new AlertDialog.Builder(getActivity(), getTheme()) 977 .setMessage(R.string.leave_customize_confirmation_dialog_message) 978 .setNegativeButton(android.R.string.no, null) 979 .setPositiveButton(android.R.string.yes, this) 980 .create(); 981 } 982 983 @Override 984 public void onClick(DialogInterface dialogInterface, int i) { 985 if (i == DialogInterface.BUTTON_POSITIVE) { 986 getActivity().setResult(RESULT_CANCELED); 987 getActivity().finish(); 988 } 989 } 990 } 991} 992