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 android.app.ActionBar; 20 import android.app.Activity; 21 import android.app.AlertDialog; 22 import android.app.Dialog; 23 import android.app.DialogFragment; 24 import android.app.LoaderManager.LoaderCallbacks; 25 import android.app.ProgressDialog; 26 import android.content.ContentProviderOperation; 27 import android.content.ContentResolver; 28 import android.content.ContentValues; 29 import android.content.Context; 30 import android.content.DialogInterface; 31 import android.content.Intent; 32 import android.content.IntentFilter; 33 import android.content.Loader; 34 import android.content.OperationApplicationException; 35 import android.database.Cursor; 36 import android.graphics.Color; 37 import android.graphics.drawable.ColorDrawable; 38 import android.net.Uri; 39 import android.os.Bundle; 40 import android.os.RemoteException; 41 import android.provider.ContactsContract; 42 import android.provider.ContactsContract.Groups; 43 import android.provider.ContactsContract.Settings; 44 import android.util.Log; 45 import android.view.ContextMenu; 46 import android.view.LayoutInflater; 47 import android.view.Menu; 48 import android.view.MenuItem; 49 import android.view.MenuItem.OnMenuItemClickListener; 50 import android.view.View; 51 import android.view.ViewGroup; 52 import android.widget.BaseExpandableListAdapter; 53 import android.widget.CheckBox; 54 import android.widget.ExpandableListAdapter; 55 import android.widget.ExpandableListView; 56 import android.widget.ExpandableListView.ExpandableListContextMenuInfo; 57 import android.widget.TextView; 58 59 import com.android.contacts.R; 60 import com.android.contacts.model.AccountTypeManager; 61 import com.android.contacts.model.ValuesDelta; 62 import com.android.contacts.model.account.AccountInfo; 63 import com.android.contacts.model.account.AccountWithDataSet; 64 import com.android.contacts.model.account.GoogleAccountType; 65 import com.android.contacts.util.EmptyService; 66 import com.android.contacts.util.LocalizedNameResolver; 67 import com.android.contacts.util.WeakAsyncTask; 68 import com.android.contacts.util.concurrent.ContactsExecutors; 69 import com.android.contacts.util.concurrent.ListenableFutureLoader; 70 import com.google.common.base.Function; 71 import com.google.common.collect.Lists; 72 import com.google.common.util.concurrent.Futures; 73 import com.google.common.util.concurrent.ListenableFuture; 74 75 import java.util.ArrayList; 76 import java.util.Collections; 77 import java.util.Comparator; 78 import java.util.Iterator; 79 import java.util.List; 80 81 import 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 */ 87 public 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 onCreate(Bundle icicle)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 CustomFilterConfigurationLoader(Context context)139 public CustomFilterConfigurationLoader(Context context) { 140 super(context, new IntentFilter(AccountTypeManager.BROADCAST_ACCOUNTS_CHANGED)); 141 mAccountTypeManager = AccountTypeManager.getInstance(context); 142 } 143 144 @Override loadData()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 createAccountSet(List<AccountInfo> sourceAccounts)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 // Don't include the null account because it doesn't support writing to 167 // ContactsContract.Settings 168 if (account.isNullAccount()) { 169 continue; 170 } 171 172 final AccountDisplay accountDisplay = new AccountDisplay(resolver, info); 173 174 final Uri.Builder groupsUri = Groups.CONTENT_URI.buildUpon() 175 .appendQueryParameter(Groups.ACCOUNT_NAME, account.name) 176 .appendQueryParameter(Groups.ACCOUNT_TYPE, account.type); 177 if (account.dataSet != null) { 178 groupsUri.appendQueryParameter(Groups.DATA_SET, account.dataSet).build(); 179 } 180 final Cursor cursor = resolver.query(groupsUri.build(), null, 181 Groups.DELETED + "=0", null, null); 182 if (cursor == null) { 183 continue; 184 } 185 android.content.EntityIterator iterator = 186 ContactsContract.Groups.newEntityIterator(cursor); 187 try { 188 boolean hasGroups = false; 189 190 // Create entries for each known group 191 while (iterator.hasNext()) { 192 final ContentValues values = iterator.next().getEntityValues(); 193 final GroupDelta group = GroupDelta.fromBefore(values); 194 accountDisplay.addGroup(group); 195 hasGroups = true; 196 } 197 // Create single entry handling ungrouped status 198 accountDisplay.mUngrouped = 199 GroupDelta.fromSettings(resolver, account.name, account.type, 200 account.dataSet, hasGroups); 201 accountDisplay.addGroup(accountDisplay.mUngrouped); 202 } finally { 203 iterator.close(); 204 } 205 206 accounts.add(accountDisplay); 207 } 208 209 return accounts; 210 } 211 } 212 213 @Override 214 protected void onStart() { 215 getLoaderManager().initLoader(ACCOUNT_SET_LOADER_ID, null, this); 216 super.onStart(); 217 } 218 219 @Override 220 public Loader<AccountSet> onCreateLoader(int id, Bundle args) { 221 return new CustomFilterConfigurationLoader(this); 222 } 223 224 @Override 225 public void onLoadFinished(Loader<AccountSet> loader, AccountSet data) { 226 mAdapter.setAccounts(data); 227 } 228 229 @Override 230 public void onLoaderReset(Loader<AccountSet> loader) { 231 mAdapter.setAccounts(null); 232 } 233 234 private static final int DEFAULT_SHOULD_SYNC = 1; 235 private static final int DEFAULT_VISIBLE = 0; 236 237 /** 238 * Entry holding any changes to {@link Groups} or {@link Settings} rows, 239 * such as {@link Groups#SHOULD_SYNC} or {@link Groups#GROUP_VISIBLE}. 240 */ 241 protected static class GroupDelta extends ValuesDelta { 242 private boolean mUngrouped = false; 243 private boolean mAccountHasGroups; 244 245 private GroupDelta() { 246 super(); 247 } 248 249 /** 250 * Build {@link GroupDelta} from the {@link Settings} row for the given 251 * {@link Settings#ACCOUNT_NAME}, {@link Settings#ACCOUNT_TYPE}, and 252 * {@link Settings#DATA_SET}. 253 */ 254 public static GroupDelta fromSettings(ContentResolver resolver, String accountName, 255 String accountType, String dataSet, boolean accountHasGroups) { 256 final Uri.Builder settingsUri = Settings.CONTENT_URI.buildUpon() 257 .appendQueryParameter(Settings.ACCOUNT_NAME, accountName) 258 .appendQueryParameter(Settings.ACCOUNT_TYPE, accountType); 259 if (dataSet != null) { 260 settingsUri.appendQueryParameter(Settings.DATA_SET, dataSet); 261 } 262 final Cursor cursor = resolver.query(settingsUri.build(), new String[] { 263 Settings.SHOULD_SYNC, Settings.UNGROUPED_VISIBLE 264 }, null, null, null); 265 266 try { 267 final ContentValues values = new ContentValues(); 268 values.put(Settings.ACCOUNT_NAME, accountName); 269 values.put(Settings.ACCOUNT_TYPE, accountType); 270 values.put(Settings.DATA_SET, dataSet); 271 272 if (cursor != null && cursor.moveToFirst()) { 273 // Read existing values when present 274 values.put(Settings.SHOULD_SYNC, cursor.getInt(0)); 275 values.put(Settings.UNGROUPED_VISIBLE, cursor.getInt(1)); 276 return fromBefore(values).setUngrouped(accountHasGroups); 277 } else { 278 // Nothing found, so treat as create 279 values.put(Settings.SHOULD_SYNC, DEFAULT_SHOULD_SYNC); 280 values.put(Settings.UNGROUPED_VISIBLE, DEFAULT_VISIBLE); 281 return fromAfter(values).setUngrouped(accountHasGroups); 282 } 283 } finally { 284 if (cursor != null) cursor.close(); 285 } 286 } 287 288 public static GroupDelta fromBefore(ContentValues before) { 289 final GroupDelta entry = new GroupDelta(); 290 entry.mBefore = before; 291 entry.mAfter = new ContentValues(); 292 return entry; 293 } 294 295 public static GroupDelta fromAfter(ContentValues after) { 296 final GroupDelta entry = new GroupDelta(); 297 entry.mBefore = null; 298 entry.mAfter = after; 299 return entry; 300 } 301 302 protected GroupDelta setUngrouped(boolean accountHasGroups) { 303 mUngrouped = true; 304 mAccountHasGroups = accountHasGroups; 305 return this; 306 } 307 308 @Override 309 public boolean beforeExists() { 310 return mBefore != null; 311 } 312 313 public boolean getShouldSync() { 314 return getAsInteger(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC, 315 DEFAULT_SHOULD_SYNC) != 0; 316 } 317 318 public boolean getVisible() { 319 return getAsInteger(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE, 320 DEFAULT_VISIBLE) != 0; 321 } 322 323 public void putShouldSync(boolean shouldSync) { 324 put(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC, shouldSync ? 1 : 0); 325 } 326 327 public void putVisible(boolean visible) { 328 put(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE, visible ? 1 : 0); 329 } 330 331 private String getAccountType() { 332 return (mBefore == null ? mAfter : mBefore).getAsString(Settings.ACCOUNT_TYPE); 333 } 334 335 public CharSequence getTitle(Context context) { 336 if (mUngrouped) { 337 final String customAllContactsName = 338 LocalizedNameResolver.getAllContactsName(context, getAccountType()); 339 if (customAllContactsName != null) { 340 return customAllContactsName; 341 } 342 if (mAccountHasGroups) { 343 return context.getText(R.string.display_ungrouped); 344 } else { 345 return context.getText(R.string.display_all_contacts); 346 } 347 } else { 348 final Integer titleRes = getAsInteger(Groups.TITLE_RES); 349 if (titleRes != null && titleRes != 0) { 350 final String packageName = getAsString(Groups.RES_PACKAGE); 351 if (packageName != null) { 352 return context.getPackageManager().getText(packageName, titleRes, null); 353 } 354 } 355 return getAsString(Groups.TITLE); 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 public final AccountInfo mAccountInfo; 459 460 public GroupDelta mUngrouped; 461 public ArrayList<GroupDelta> mSyncedGroups = Lists.newArrayList(); 462 public ArrayList<GroupDelta> mUnsyncedGroups = Lists.newArrayList(); 463 464 public GroupDelta getGroup(int position) { 465 if (position < mSyncedGroups.size()) { 466 return mSyncedGroups.get(position); 467 } 468 position -= mSyncedGroups.size(); 469 return mUnsyncedGroups.get(position); 470 } 471 472 /** 473 * Build an {@link AccountDisplay} covering all {@link Groups} under the 474 * given {@link AccountWithDataSet}. 475 */ 476 public AccountDisplay(ContentResolver resolver, AccountInfo accountInfo) { 477 mName = accountInfo.getAccount().name; 478 mType = accountInfo.getAccount().type; 479 mDataSet = accountInfo.getAccount().dataSet; 480 mAccountInfo = accountInfo; 481 } 482 483 /** 484 * Add the given {@link GroupDelta} internally, filing based on its 485 * {@link GroupDelta#getShouldSync()} status. 486 */ 487 private void addGroup(GroupDelta group) { 488 if (group.getShouldSync()) { 489 mSyncedGroups.add(group); 490 } else { 491 mUnsyncedGroups.add(group); 492 } 493 } 494 495 /** 496 * Set the {@link GroupDelta#putShouldSync(boolean)} value for all 497 * children {@link GroupDelta} rows. 498 */ 499 public void setShouldSync(boolean shouldSync) { 500 final Iterator<GroupDelta> oppositeChildren = shouldSync ? 501 mUnsyncedGroups.iterator() : mSyncedGroups.iterator(); 502 while (oppositeChildren.hasNext()) { 503 final GroupDelta child = oppositeChildren.next(); 504 setShouldSync(child, shouldSync, false); 505 oppositeChildren.remove(); 506 } 507 } 508 509 public void setShouldSync(GroupDelta child, boolean shouldSync) { 510 setShouldSync(child, shouldSync, true); 511 } 512 513 /** 514 * Set {@link GroupDelta#putShouldSync(boolean)}, and file internally 515 * based on updated state. 516 */ 517 public void setShouldSync(GroupDelta child, boolean shouldSync, boolean attemptRemove) { 518 child.putShouldSync(shouldSync); 519 if (shouldSync) { 520 if (attemptRemove) { 521 mUnsyncedGroups.remove(child); 522 } 523 mSyncedGroups.add(child); 524 Collections.sort(mSyncedGroups, sIdComparator); 525 } else { 526 if (attemptRemove) { 527 mSyncedGroups.remove(child); 528 } 529 mUnsyncedGroups.add(child); 530 } 531 } 532 533 /** 534 * Build set of {@link ContentProviderOperation} to persist any user 535 * changes to {@link GroupDelta} rows under this {@link AccountWithDataSet}. 536 */ 537 public void buildDiff(ArrayList<ContentProviderOperation> diff) { 538 for (GroupDelta group : mSyncedGroups) { 539 final ContentProviderOperation oper = group.buildDiff(); 540 if (oper != null) diff.add(oper); 541 } 542 for (GroupDelta group : mUnsyncedGroups) { 543 final ContentProviderOperation oper = group.buildDiff(); 544 if (oper != null) diff.add(oper); 545 } 546 } 547 } 548 549 /** 550 * {@link ExpandableListAdapter} that shows {@link GroupDelta} settings, 551 * grouped by {@link AccountWithDataSet} type. Shows footer row when any groups are 552 * unsynced, as determined through {@link AccountDisplay#mUnsyncedGroups}. 553 */ 554 protected static class DisplayAdapter extends BaseExpandableListAdapter { 555 private Context mContext; 556 private LayoutInflater mInflater; 557 private AccountTypeManager mAccountTypes; 558 private AccountSet mAccounts; 559 560 private boolean mChildWithPhones = false; 561 562 public DisplayAdapter(Context context) { 563 mContext = context; 564 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 565 mAccountTypes = AccountTypeManager.getInstance(context); 566 } 567 568 public void setAccounts(AccountSet accounts) { 569 mAccounts = accounts; 570 notifyDataSetChanged(); 571 } 572 573 /** 574 * In group descriptions, show the number of contacts with phone 575 * numbers, in addition to the total contacts. 576 */ 577 public void setChildDescripWithPhones(boolean withPhones) { 578 mChildWithPhones = withPhones; 579 } 580 581 @Override 582 public View getGroupView(int groupPosition, boolean isExpanded, View convertView, 583 ViewGroup parent) { 584 if (convertView == null) { 585 convertView = mInflater.inflate( 586 R.layout.custom_contact_list_filter_account, parent, false); 587 } 588 589 final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1); 590 final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2); 591 592 final AccountDisplay account = (AccountDisplay)this.getGroup(groupPosition); 593 594 text1.setText(account.mAccountInfo.getNameLabel()); 595 text1.setVisibility(!account.mAccountInfo.isDeviceAccount() 596 || account.mAccountInfo.hasDistinctName() 597 ? View.VISIBLE : View.GONE); 598 text2.setText(account.mAccountInfo.getTypeLabel()); 599 600 final int textColor = mContext.getResources().getColor(isExpanded 601 ? R.color.dialtacts_theme_color 602 : R.color.account_filter_text_color); 603 text1.setTextColor(textColor); 604 text2.setTextColor(textColor); 605 606 return convertView; 607 } 608 609 @Override 610 public View getChildView(int groupPosition, int childPosition, boolean isLastChild, 611 View convertView, ViewGroup parent) { 612 if (convertView == null) { 613 convertView = mInflater.inflate( 614 R.layout.custom_contact_list_filter_group, parent, false); 615 } 616 617 final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1); 618 final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2); 619 final CheckBox checkbox = (CheckBox)convertView.findViewById(android.R.id.checkbox); 620 621 final AccountDisplay account = mAccounts.get(groupPosition); 622 final GroupDelta child = (GroupDelta)this.getChild(groupPosition, childPosition); 623 if (child != null) { 624 // Handle normal group, with title and checkbox 625 final boolean groupVisible = child.getVisible(); 626 checkbox.setVisibility(View.VISIBLE); 627 checkbox.setChecked(groupVisible); 628 629 final CharSequence groupTitle = child.getTitle(mContext); 630 text1.setText(groupTitle); 631 text2.setVisibility(View.GONE); 632 } else { 633 // When unknown child, this is "more" footer view 634 checkbox.setVisibility(View.GONE); 635 text1.setText(R.string.display_more_groups); 636 text2.setVisibility(View.GONE); 637 } 638 639 // Show divider at bottom only for the last child. 640 final View dividerBottom = convertView.findViewById(R.id.adapter_divider_bottom); 641 dividerBottom.setVisibility(isLastChild ? View.VISIBLE : View.GONE); 642 643 return convertView; 644 } 645 646 @Override 647 public Object getChild(int groupPosition, int childPosition) { 648 final AccountDisplay account = mAccounts.get(groupPosition); 649 final boolean validChild = childPosition >= 0 650 && childPosition < account.mSyncedGroups.size() 651 + account.mUnsyncedGroups.size(); 652 if (validChild) { 653 return account.getGroup(childPosition); 654 } else { 655 return null; 656 } 657 } 658 659 @Override 660 public long getChildId(int groupPosition, int childPosition) { 661 final GroupDelta child = (GroupDelta)getChild(groupPosition, childPosition); 662 if (child != null) { 663 final Long childId = child.getId(); 664 return childId != null ? childId : Long.MIN_VALUE; 665 } else { 666 return Long.MIN_VALUE; 667 } 668 } 669 670 @Override 671 public int getChildrenCount(int groupPosition) { 672 // Count is any synced groups, plus possible footer 673 final AccountDisplay account = mAccounts.get(groupPosition); 674 return account.mSyncedGroups.size() + account.mUnsyncedGroups.size(); 675 } 676 677 @Override 678 public Object getGroup(int groupPosition) { 679 return mAccounts.get(groupPosition); 680 } 681 682 @Override 683 public int getGroupCount() { 684 if (mAccounts == null) { 685 return 0; 686 } 687 return mAccounts.size(); 688 } 689 690 @Override 691 public long getGroupId(int groupPosition) { 692 return groupPosition; 693 } 694 695 @Override 696 public boolean hasStableIds() { 697 return true; 698 } 699 700 @Override 701 public boolean isChildSelectable(int groupPosition, int childPosition) { 702 return true; 703 } 704 } 705 706 /** 707 * Handle any clicks on {@link ExpandableListAdapter} children, which 708 * usually mean toggling its visible state. 709 */ 710 @Override 711 public boolean onChildClick(ExpandableListView parent, View view, int groupPosition, 712 int childPosition, long id) { 713 final CheckBox checkbox = (CheckBox)view.findViewById(android.R.id.checkbox); 714 715 final AccountDisplay account = (AccountDisplay)mAdapter.getGroup(groupPosition); 716 final GroupDelta child = (GroupDelta)mAdapter.getChild(groupPosition, childPosition); 717 if (child != null) { 718 checkbox.toggle(); 719 child.putVisible(checkbox.isChecked()); 720 } else { 721 // Open context menu for bringing back unsynced 722 this.openContextMenu(view); 723 } 724 return true; 725 } 726 727 // TODO: move these definitions to framework constants when we begin 728 // defining this mode through <sync-adapter> tags 729 private static final int SYNC_MODE_UNSUPPORTED = 0; 730 private static final int SYNC_MODE_UNGROUPED = 1; 731 private static final int SYNC_MODE_EVERYTHING = 2; 732 733 protected int getSyncMode(AccountDisplay account) { 734 // TODO: read sync mode through <sync-adapter> definition 735 if (GoogleAccountType.ACCOUNT_TYPE.equals(account.mType) && account.mDataSet == null) { 736 return SYNC_MODE_EVERYTHING; 737 } else { 738 return SYNC_MODE_UNSUPPORTED; 739 } 740 } 741 742 @Override 743 public void onCreateContextMenu(ContextMenu menu, View view, 744 ContextMenu.ContextMenuInfo menuInfo) { 745 super.onCreateContextMenu(menu, view, menuInfo); 746 747 // Bail if not working with expandable long-press, or if not child 748 if (!(menuInfo instanceof ExpandableListContextMenuInfo)) return; 749 750 final ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuInfo; 751 final int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition); 752 final int childPosition = ExpandableListView.getPackedPositionChild(info.packedPosition); 753 754 // Skip long-press on expandable parents 755 if (childPosition == -1) return; 756 757 final AccountDisplay account = (AccountDisplay)mAdapter.getGroup(groupPosition); 758 final GroupDelta child = (GroupDelta)mAdapter.getChild(groupPosition, childPosition); 759 760 // Ignore when selective syncing unsupported 761 final int syncMode = getSyncMode(account); 762 if (syncMode == SYNC_MODE_UNSUPPORTED) return; 763 764 if (child != null) { 765 showRemoveSync(menu, account, child, syncMode); 766 } else { 767 showAddSync(menu, account, syncMode); 768 } 769 } 770 771 protected void showRemoveSync(ContextMenu menu, final AccountDisplay account, 772 final GroupDelta child, final int syncMode) { 773 final CharSequence title = child.getTitle(this); 774 775 menu.setHeaderTitle(title); 776 menu.add(R.string.menu_sync_remove).setOnMenuItemClickListener( 777 new OnMenuItemClickListener() { 778 public boolean onMenuItemClick(MenuItem item) { 779 handleRemoveSync(account, child, syncMode, title); 780 return true; 781 } 782 }); 783 } 784 785 protected void handleRemoveSync(final AccountDisplay account, final GroupDelta child, 786 final int syncMode, CharSequence title) { 787 final boolean shouldSyncUngrouped = account.mUngrouped.getShouldSync(); 788 if (syncMode == SYNC_MODE_EVERYTHING && shouldSyncUngrouped 789 && !child.equals(account.mUngrouped)) { 790 // Warn before removing this group when it would cause ungrouped to stop syncing 791 final AlertDialog.Builder builder = new AlertDialog.Builder(this); 792 final CharSequence removeMessage = this.getString( 793 R.string.display_warn_remove_ungrouped, title); 794 builder.setTitle(R.string.menu_sync_remove); 795 builder.setMessage(removeMessage); 796 builder.setNegativeButton(android.R.string.cancel, null); 797 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 798 public void onClick(DialogInterface dialog, int which) { 799 // Mark both this group and ungrouped to stop syncing 800 account.setShouldSync(account.mUngrouped, false); 801 account.setShouldSync(child, false); 802 mAdapter.notifyDataSetChanged(); 803 } 804 }); 805 builder.show(); 806 } else { 807 // Mark this group to not sync 808 account.setShouldSync(child, false); 809 mAdapter.notifyDataSetChanged(); 810 } 811 } 812 813 protected void showAddSync(ContextMenu menu, final AccountDisplay account, final int syncMode) { 814 menu.setHeaderTitle(R.string.dialog_sync_add); 815 816 // Create item for each available, unsynced group 817 for (final GroupDelta child : account.mUnsyncedGroups) { 818 if (!child.getShouldSync()) { 819 final CharSequence title = child.getTitle(this); 820 menu.add(title).setOnMenuItemClickListener(new OnMenuItemClickListener() { 821 public boolean onMenuItemClick(MenuItem item) { 822 // Adding specific group for syncing 823 if (child.mUngrouped && syncMode == SYNC_MODE_EVERYTHING) { 824 account.setShouldSync(true); 825 } else { 826 account.setShouldSync(child, true); 827 } 828 mAdapter.notifyDataSetChanged(); 829 return true; 830 } 831 }); 832 } 833 } 834 } 835 836 private boolean hasUnsavedChanges() { 837 if (mAdapter == null || mAdapter.mAccounts == null) { 838 return false; 839 } 840 if (getCurrentListFilterType() != ContactListFilter.FILTER_TYPE_CUSTOM) { 841 return true; 842 } 843 final ArrayList<ContentProviderOperation> diff = mAdapter.mAccounts.buildDiff(); 844 if (diff.isEmpty()) { 845 return false; 846 } 847 return true; 848 } 849 850 @SuppressWarnings("unchecked") 851 private void doSaveAction() { 852 if (mAdapter == null || mAdapter.mAccounts == null) { 853 finish(); 854 return; 855 } 856 857 setResult(RESULT_OK); 858 859 final ArrayList<ContentProviderOperation> diff = mAdapter.mAccounts.buildDiff(); 860 if (diff.isEmpty()) { 861 finish(); 862 return; 863 } 864 865 new UpdateTask(this).execute(diff); 866 } 867 868 /** 869 * Background task that persists changes to {@link Groups#GROUP_VISIBLE}, 870 * showing spinner dialog to user while updating. 871 */ 872 public static class UpdateTask extends 873 WeakAsyncTask<ArrayList<ContentProviderOperation>, Void, Void, Activity> { 874 private ProgressDialog mProgress; 875 876 public UpdateTask(Activity target) { 877 super(target); 878 } 879 880 /** {@inheritDoc} */ 881 @Override 882 protected void onPreExecute(Activity target) { 883 final Context context = target; 884 885 mProgress = ProgressDialog.show( 886 context, null, context.getText(R.string.savingDisplayGroups)); 887 888 // Before starting this task, start an empty service to protect our 889 // process from being reclaimed by the system. 890 context.startService(new Intent(context, EmptyService.class)); 891 } 892 893 /** {@inheritDoc} */ 894 @Override 895 protected Void doInBackground( 896 Activity target, ArrayList<ContentProviderOperation>... params) { 897 final Context context = target; 898 final ContentValues values = new ContentValues(); 899 final ContentResolver resolver = context.getContentResolver(); 900 901 try { 902 final ArrayList<ContentProviderOperation> diff = params[0]; 903 resolver.applyBatch(ContactsContract.AUTHORITY, diff); 904 } catch (RemoteException e) { 905 Log.e(TAG, "Problem saving display groups", e); 906 } catch (OperationApplicationException e) { 907 Log.e(TAG, "Problem saving display groups", e); 908 } 909 910 return null; 911 } 912 913 /** {@inheritDoc} */ 914 @Override 915 protected void onPostExecute(Activity target, Void result) { 916 final Context context = target; 917 918 try { 919 mProgress.dismiss(); 920 } catch (Exception e) { 921 Log.e(TAG, "Error dismissing progress dialog", e); 922 } 923 924 target.finish(); 925 926 // Stop the service that was protecting us 927 context.stopService(new Intent(context, EmptyService.class)); 928 } 929 } 930 931 @Override 932 public boolean onCreateOptionsMenu(Menu menu) { 933 super.onCreateOptionsMenu(menu); 934 935 final MenuItem menuItem = menu.add(Menu.NONE, R.id.menu_save, Menu.NONE, 936 R.string.menu_custom_filter_save); 937 menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 938 939 return true; 940 } 941 942 @Override 943 public boolean onOptionsItemSelected(MenuItem item) { 944 final int id = item.getItemId(); 945 if (id == android.R.id.home) { 946 confirmFinish(); 947 return true; 948 } else if (id == R.id.menu_save) { 949 this.doSaveAction(); 950 return true; 951 } else { 952 } 953 return super.onOptionsItemSelected(item); 954 } 955 956 @Override 957 public void onBackPressed() { 958 confirmFinish(); 959 } 960 961 private void confirmFinish() { 962 // Prompt the user whether they want to discard there customizations unless 963 // nothing will be changed. 964 if (hasUnsavedChanges()) { 965 new ConfirmNavigationDialogFragment().show(getFragmentManager(), 966 "ConfirmNavigationDialog"); 967 } else { 968 setResult(RESULT_CANCELED); 969 finish(); 970 } 971 } 972 973 private int getCurrentListFilterType() { 974 return getIntent().getIntExtra(EXTRA_CURRENT_LIST_FILTER_TYPE, 975 ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS); 976 } 977 978 public static class ConfirmNavigationDialogFragment 979 extends DialogFragment implements DialogInterface.OnClickListener { 980 981 @Override 982 public Dialog onCreateDialog(Bundle savedInstanceState) { 983 return new AlertDialog.Builder(getActivity(), getTheme()) 984 .setMessage(R.string.leave_customize_confirmation_dialog_message) 985 .setNegativeButton(android.R.string.no, null) 986 .setPositiveButton(android.R.string.yes, this) 987 .create(); 988 } 989 990 @Override 991 public void onClick(DialogInterface dialogInterface, int i) { 992 if (i == DialogInterface.BUTTON_POSITIVE) { 993 getActivity().setResult(RESULT_CANCELED); 994 getActivity().finish(); 995 } 996 } 997 } 998 } 999