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