1/* 2 * Copyright (C) 2016 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 */ 16package com.android.contacts.group; 17 18import android.app.Activity; 19import android.app.LoaderManager.LoaderCallbacks; 20import android.content.ContentResolver; 21import android.content.Context; 22import android.content.CursorLoader; 23import android.content.Intent; 24import android.content.Loader; 25import android.database.Cursor; 26import android.database.CursorWrapper; 27import android.graphics.PorterDuff; 28import android.graphics.drawable.Drawable; 29import android.net.Uri; 30import android.os.Bundle; 31import android.os.Handler; 32import android.os.Message; 33import android.provider.ContactsContract; 34import android.provider.ContactsContract.Contacts; 35import android.support.v4.content.ContextCompat; 36import android.text.TextUtils; 37import android.util.Log; 38import android.view.Gravity; 39import android.view.LayoutInflater; 40import android.view.Menu; 41import android.view.MenuInflater; 42import android.view.MenuItem; 43import android.view.View; 44import android.view.ViewGroup; 45import android.widget.Button; 46import android.widget.FrameLayout; 47import android.widget.ImageView; 48import android.widget.LinearLayout; 49import android.widget.Toast; 50 51import com.android.contacts.ContactSaveService; 52import com.android.contacts.ContactsUtils; 53import com.android.contacts.GroupMetaDataLoader; 54import com.android.contacts.R; 55import com.android.contacts.activities.ActionBarAdapter; 56import com.android.contacts.activities.PeopleActivity; 57import com.android.contacts.group.GroupMembersAdapter.GroupMembersQuery; 58import com.android.contacts.interactions.GroupDeletionDialogFragment; 59import com.android.contacts.list.ContactsRequest; 60import com.android.contacts.list.ContactsSectionIndexer; 61import com.android.contacts.list.MultiSelectContactsListFragment; 62import com.android.contacts.list.MultiSelectEntryContactListAdapter.DeleteContactListener; 63import com.android.contacts.list.UiIntentActions; 64import com.android.contacts.logging.ListEvent; 65import com.android.contacts.logging.ListEvent.ListType; 66import com.android.contacts.logging.Logger; 67import com.android.contacts.logging.ScreenEvent; 68import com.android.contacts.model.account.AccountWithDataSet; 69import com.android.contacts.util.ImplicitIntentsUtil; 70import com.android.contactsbind.FeedbackHelper; 71import com.google.common.primitives.Longs; 72 73import java.util.ArrayList; 74import java.util.HashMap; 75import java.util.HashSet; 76import java.util.List; 77import java.util.Map; 78import java.util.Set; 79 80/** Displays the members of a group. */ 81public class GroupMembersFragment extends MultiSelectContactsListFragment<GroupMembersAdapter> { 82 83 private static final String TAG = "GroupMembers"; 84 85 private static final String KEY_IS_EDIT_MODE = "editMode"; 86 private static final String KEY_GROUP_URI = "groupUri"; 87 private static final String KEY_GROUP_METADATA = "groupMetadata"; 88 89 public static final String TAG_GROUP_NAME_EDIT_DIALOG = "groupNameEditDialog"; 90 91 private static final String ARG_GROUP_URI = "groupUri"; 92 93 private static final int LOADER_GROUP_METADATA = 0; 94 private static final int MSG_FAIL_TO_LOAD = 1; 95 private static final int RESULT_GROUP_ADD_MEMBER = 100; 96 97 /** Filters out duplicate contacts. */ 98 private class FilterCursorWrapper extends CursorWrapper { 99 100 private int[] mIndex; 101 private int mCount = 0; 102 private int mPos = 0; 103 104 public FilterCursorWrapper(Cursor cursor) { 105 super(cursor); 106 107 mCount = super.getCount(); 108 mIndex = new int[mCount]; 109 110 final List<Integer> indicesToFilter = new ArrayList<>(); 111 112 if (Log.isLoggable(TAG, Log.VERBOSE)) { 113 Log.v(TAG, "Group members CursorWrapper start: " + mCount); 114 } 115 116 final Bundle bundle = cursor.getExtras(); 117 final String sections[] = bundle.getStringArray(Contacts 118 .EXTRA_ADDRESS_BOOK_INDEX_TITLES); 119 final int counts[] = bundle.getIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS); 120 final ContactsSectionIndexer indexer = (sections == null || counts == null) 121 ? null : new ContactsSectionIndexer(sections, counts); 122 123 mGroupMemberContactIds.clear(); 124 for (int i = 0; i < mCount; i++) { 125 super.moveToPosition(i); 126 final String contactId = getString(GroupMembersQuery.CONTACT_ID); 127 if (!mGroupMemberContactIds.contains(contactId)) { 128 mIndex[mPos++] = i; 129 mGroupMemberContactIds.add(contactId); 130 } else { 131 indicesToFilter.add(i); 132 } 133 } 134 135 if (indexer != null && GroupUtil.needTrimming(mCount, counts, indexer.getPositions())) { 136 GroupUtil.updateBundle(bundle, indexer, indicesToFilter, sections, counts); 137 } 138 139 mCount = mPos; 140 mPos = 0; 141 super.moveToFirst(); 142 143 if (Log.isLoggable(TAG, Log.VERBOSE)) { 144 Log.v(TAG, "Group members CursorWrapper end: " + mCount); 145 } 146 } 147 148 @Override 149 public boolean move(int offset) { 150 return moveToPosition(mPos + offset); 151 } 152 153 @Override 154 public boolean moveToNext() { 155 return moveToPosition(mPos + 1); 156 } 157 158 @Override 159 public boolean moveToPrevious() { 160 return moveToPosition(mPos - 1); 161 } 162 163 @Override 164 public boolean moveToFirst() { 165 return moveToPosition(0); 166 } 167 168 @Override 169 public boolean moveToLast() { 170 return moveToPosition(mCount - 1); 171 } 172 173 @Override 174 public boolean moveToPosition(int position) { 175 if (position >= mCount) { 176 mPos = mCount; 177 return false; 178 } else if (position < 0) { 179 mPos = -1; 180 return false; 181 } 182 mPos = mIndex[position]; 183 return super.moveToPosition(mPos); 184 } 185 186 @Override 187 public int getCount() { 188 return mCount; 189 } 190 191 @Override 192 public int getPosition() { 193 return mPos; 194 } 195 } 196 197 private final LoaderCallbacks<Cursor> mGroupMetaDataCallbacks = new LoaderCallbacks<Cursor>() { 198 199 @Override 200 public CursorLoader onCreateLoader(int id, Bundle args) { 201 return new GroupMetaDataLoader(mActivity, mGroupUri); 202 } 203 204 @Override 205 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 206 if (cursor == null || cursor.isClosed() || !cursor.moveToNext()) { 207 Log.e(TAG, "Failed to load group metadata for " + mGroupUri); 208 Toast.makeText(getContext(), R.string.groupLoadErrorToast, Toast.LENGTH_SHORT) 209 .show(); 210 mHandler.sendEmptyMessage(MSG_FAIL_TO_LOAD); 211 return; 212 } 213 mGroupMetaData = new GroupMetaData(getActivity(), cursor); 214 onGroupMetadataLoaded(); 215 } 216 217 @Override 218 public void onLoaderReset(Loader<Cursor> loader) {} 219 }; 220 221 private ActionBarAdapter mActionBarAdapter; 222 223 private PeopleActivity mActivity; 224 225 private Uri mGroupUri; 226 227 private boolean mIsEditMode; 228 229 private GroupMetaData mGroupMetaData; 230 231 private Set<String> mGroupMemberContactIds = new HashSet(); 232 233 private Handler mHandler = new Handler() { 234 @Override 235 public void handleMessage(Message msg) { 236 if(msg.what == MSG_FAIL_TO_LOAD) { 237 mActivity.onBackPressed(); 238 } 239 } 240 }; 241 242 public static GroupMembersFragment newInstance(Uri groupUri) { 243 final Bundle args = new Bundle(); 244 args.putParcelable(ARG_GROUP_URI, groupUri); 245 246 final GroupMembersFragment fragment = new GroupMembersFragment(); 247 fragment.setArguments(args); 248 return fragment; 249 } 250 251 public GroupMembersFragment() { 252 setPhotoLoaderEnabled(true); 253 setSectionHeaderDisplayEnabled(true); 254 setHasOptionsMenu(true); 255 setListType(ListType.GROUP); 256 } 257 258 @Override 259 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 260 if (mGroupMetaData == null) { 261 // Hide menu options until metadata is fully loaded 262 return; 263 } 264 inflater.inflate(R.menu.view_group, menu); 265 } 266 267 @Override 268 public void onPrepareOptionsMenu(Menu menu) { 269 final boolean isSelectionMode = mActionBarAdapter.isSelectionMode(); 270 final boolean isGroupEditable = mGroupMetaData != null && mGroupMetaData.editable; 271 final boolean isGroupReadOnly = mGroupMetaData != null && mGroupMetaData.readOnly; 272 273 setVisible(getContext(), menu, R.id.menu_multi_send_email, !mIsEditMode && !isGroupEmpty()); 274 setVisible(getContext(), menu, R.id.menu_multi_send_message, 275 !mIsEditMode && !isGroupEmpty()); 276 setVisible(getContext(), menu, R.id.menu_add, isGroupEditable && !isSelectionMode); 277 setVisible(getContext(), menu, R.id.menu_rename_group, 278 !isGroupReadOnly && !isSelectionMode); 279 setVisible(getContext(), menu, R.id.menu_delete_group, 280 !isGroupReadOnly && !isSelectionMode); 281 setVisible(getContext(), menu, R.id.menu_edit_group, 282 isGroupEditable && !mIsEditMode && !isSelectionMode && !isGroupEmpty()); 283 setVisible(getContext(), menu, R.id.menu_remove_from_group, 284 isGroupEditable && isSelectionMode && !mIsEditMode); 285 } 286 287 private boolean isGroupEmpty() { 288 return getAdapter() != null && getAdapter().isEmpty(); 289 } 290 291 private static void setVisible(Context context, Menu menu, int id, boolean visible) { 292 final MenuItem menuItem = menu.findItem(id); 293 if (menuItem != null) { 294 menuItem.setVisible(visible); 295 final Drawable icon = menuItem.getIcon(); 296 if (icon != null) { 297 icon.mutate().setColorFilter(ContextCompat.getColor(context, 298 R.color.actionbar_icon_color), PorterDuff.Mode.SRC_ATOP); 299 } 300 } 301 } 302 303 /** 304 * Helper class for cp2 query used to look up all contact's emails and phone numbers. 305 */ 306 public static abstract class Query { 307 public static final String EMAIL_SELECTION = 308 ContactsContract.Data.MIMETYPE + "='" 309 + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE + "'"; 310 311 public static final String PHONE_SELECTION = 312 ContactsContract.Data.MIMETYPE + "='" 313 + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'"; 314 315 public static final String[] EMAIL_PROJECTION = { 316 ContactsContract.Data.CONTACT_ID, 317 ContactsContract.CommonDataKinds.Email._ID, 318 ContactsContract.Data.IS_SUPER_PRIMARY, 319 ContactsContract.Data.TIMES_USED, 320 ContactsContract.Data.DATA1 321 }; 322 323 public static final String[] PHONE_PROJECTION = { 324 ContactsContract.Data.CONTACT_ID, 325 ContactsContract.CommonDataKinds.Phone._ID, 326 ContactsContract.Data.IS_SUPER_PRIMARY, 327 ContactsContract.Data.TIMES_USED, 328 ContactsContract.Data.DATA1 329 }; 330 331 public static final int CONTACT_ID = 0; 332 public static final int ITEM_ID = 1; 333 public static final int PRIMARY = 2; 334 public static final int TIMES_USED = 3; 335 public static final int DATA1 = 4; 336 } 337 338 /** 339 * Helper class for managing data related to contacts and emails/phone numbers. 340 */ 341 private class ContactDataHelperClass { 342 343 private List<String> items = new ArrayList<>(); 344 private String mostUsedItemId = null; 345 private int mostUsedTimes; 346 private String primaryItemId = null; 347 348 public void addItem(String item, int timesUsed, boolean primaryFlag) { 349 if (mostUsedItemId == null || timesUsed > mostUsedTimes) { 350 mostUsedItemId = item; 351 mostUsedTimes = timesUsed; 352 } 353 if (primaryFlag) { 354 primaryItemId = item; 355 } 356 items.add(item); 357 } 358 359 public boolean hasDefaultItem() { 360 return primaryItemId != null || items.size() == 1; 361 } 362 363 public String getDefaultSelectionItemId() { 364 return primaryItemId != null 365 ? primaryItemId 366 : mostUsedItemId; 367 } 368 } 369 370 private void sendToGroup(long[] ids, String sendScheme, String title) { 371 if (ids == null || ids.length == 0) return; 372 373 // Get emails or phone numbers 374 // contactMap <contact_id, contact_data> 375 final Map<String, ContactDataHelperClass> contactMap = new HashMap<>(); 376 // itemList <item_data> 377 final List<String> itemList = new ArrayList<>(); 378 final String sIds = GroupUtil.convertArrayToString(ids); 379 final String select = (ContactsUtils.SCHEME_MAILTO.equals(sendScheme) 380 ? Query.EMAIL_SELECTION 381 : Query.PHONE_SELECTION) 382 + " AND " + ContactsContract.Data.CONTACT_ID + " IN (" + sIds + ")"; 383 final ContentResolver contentResolver = getContext().getContentResolver(); 384 final Cursor cursor = contentResolver.query(ContactsContract.Data.CONTENT_URI, 385 ContactsUtils.SCHEME_MAILTO.equals(sendScheme) 386 ? Query.EMAIL_PROJECTION 387 : Query.PHONE_PROJECTION, 388 select, null, null); 389 390 if (cursor == null) { 391 return; 392 } 393 394 try { 395 cursor.moveToPosition(-1); 396 while (cursor.moveToNext()) { 397 final String contactId = cursor.getString(Query.CONTACT_ID); 398 final String itemId = cursor.getString(Query.ITEM_ID); 399 final boolean isPrimary = cursor.getInt(Query.PRIMARY) != 0; 400 final int timesUsed = cursor.getInt(Query.TIMES_USED); 401 final String data = cursor.getString(Query.DATA1); 402 403 if (!TextUtils.isEmpty(data)) { 404 final ContactDataHelperClass contact; 405 if (!contactMap.containsKey(contactId)) { 406 contact = new ContactDataHelperClass(); 407 contactMap.put(contactId, contact); 408 } else { 409 contact = contactMap.get(contactId); 410 } 411 contact.addItem(itemId, timesUsed, isPrimary); 412 itemList.add(data); 413 } 414 } 415 } finally { 416 cursor.close(); 417 } 418 419 // Start picker if a contact does not have a default 420 for (ContactDataHelperClass i : contactMap.values()) { 421 if (!i.hasDefaultItem()) { 422 // Build list of default selected item ids 423 final List<Long> defaultSelection = new ArrayList<>(); 424 for (ContactDataHelperClass j : contactMap.values()) { 425 final String selectionItemId = j.getDefaultSelectionItemId(); 426 if (selectionItemId != null) { 427 defaultSelection.add(Long.parseLong(selectionItemId)); 428 } 429 } 430 final long[] defaultSelectionArray = Longs.toArray(defaultSelection); 431 startSendToSelectionPickerActivity(ids, defaultSelectionArray, sendScheme, title); 432 return; 433 } 434 } 435 436 if (itemList.size() == 0 || contactMap.size() < ids.length) { 437 Toast.makeText(getContext(), ContactsUtils.SCHEME_MAILTO.equals(sendScheme) 438 ? getString(R.string.groupSomeContactsNoEmailsToast) 439 : getString(R.string.groupSomeContactsNoPhonesToast), 440 Toast.LENGTH_LONG).show(); 441 } 442 443 if (itemList.size() == 0) { 444 return; 445 } 446 447 final String itemsString = TextUtils.join(",", itemList); 448 GroupUtil.startSendToSelectionActivity(this, itemsString, sendScheme, title); 449 } 450 451 private void startSendToSelectionPickerActivity(long[] ids, long[] defaultSelection, 452 String sendScheme, String title) { 453 startActivity(GroupUtil.createSendToSelectionPickerIntent(getContext(), ids, 454 defaultSelection, sendScheme, title)); 455 } 456 457 private void startGroupAddMemberActivity() { 458 startActivityForResult(GroupUtil.createPickMemberIntent(getContext(), mGroupMetaData, 459 getMemberContactIds()), RESULT_GROUP_ADD_MEMBER); 460 } 461 462 @Override 463 public boolean onOptionsItemSelected(MenuItem item) { 464 final int id = item.getItemId(); 465 if (id == android.R.id.home) { 466 mActivity.onBackPressed(); 467 } else if (id == R.id.menu_add) { 468 startGroupAddMemberActivity(); 469 } else if (id == R.id.menu_multi_send_email) { 470 final long[] ids = mActionBarAdapter.isSelectionMode() 471 ? getAdapter().getSelectedContactIdsArray() 472 : GroupUtil.convertStringSetToLongArray(mGroupMemberContactIds); 473 sendToGroup(ids, ContactsUtils.SCHEME_MAILTO, 474 getString(R.string.menu_sendEmailOption)); 475 } else if (id == R.id.menu_multi_send_message) { 476 final long[] ids = mActionBarAdapter.isSelectionMode() 477 ? getAdapter().getSelectedContactIdsArray() 478 : GroupUtil.convertStringSetToLongArray(mGroupMemberContactIds); 479 sendToGroup(ids, ContactsUtils.SCHEME_SMSTO, 480 getString(R.string.menu_sendMessageOption)); 481 } else if (id == R.id.menu_rename_group) { 482 GroupNameEditDialogFragment.newInstanceForUpdate( 483 new AccountWithDataSet(mGroupMetaData.accountName, 484 mGroupMetaData.accountType, mGroupMetaData.dataSet), 485 GroupUtil.ACTION_UPDATE_GROUP, mGroupMetaData.groupId, 486 mGroupMetaData.groupName).show(getFragmentManager(), 487 TAG_GROUP_NAME_EDIT_DIALOG); 488 } else if (id == R.id.menu_delete_group) { 489 deleteGroup(); 490 } else if (id == R.id.menu_edit_group) { 491 mIsEditMode = true; 492 mActionBarAdapter.setSelectionMode(true); 493 displayDeleteButtons(true); 494 } else if (id == R.id.menu_remove_from_group) { 495 logListEvent(); 496 removeSelectedContacts(); 497 } else { 498 return super.onOptionsItemSelected(item); 499 } 500 return true; 501 } 502 503 private void removeSelectedContacts() { 504 final long[] contactIds = getAdapter().getSelectedContactIdsArray(); 505 new UpdateGroupMembersAsyncTask(UpdateGroupMembersAsyncTask.TYPE_REMOVE, 506 getContext(), contactIds, mGroupMetaData.groupId, mGroupMetaData.accountName, 507 mGroupMetaData.accountType, mGroupMetaData.dataSet).execute(); 508 509 mActionBarAdapter.setSelectionMode(false); 510 } 511 512 @Override 513 public void onActivityResult(int requestCode, int resultCode, Intent data) { 514 if (resultCode != Activity.RESULT_OK || data == null 515 || requestCode != RESULT_GROUP_ADD_MEMBER) { 516 return; 517 } 518 519 long[] contactIds = data.getLongArrayExtra( 520 UiIntentActions.TARGET_CONTACT_IDS_EXTRA_KEY); 521 if (contactIds == null) { 522 final long contactId = data.getLongExtra( 523 UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, -1); 524 if (contactId > -1) { 525 contactIds = new long[1]; 526 contactIds[0] = contactId; 527 } 528 } 529 new UpdateGroupMembersAsyncTask( 530 UpdateGroupMembersAsyncTask.TYPE_ADD, 531 getContext(), contactIds, mGroupMetaData.groupId, mGroupMetaData.accountName, 532 mGroupMetaData.accountType, mGroupMetaData.dataSet).execute(); 533 } 534 535 private final ActionBarAdapter.Listener mActionBarListener = new ActionBarAdapter.Listener() { 536 @Override 537 public void onAction(int action) { 538 switch (action) { 539 case ActionBarAdapter.Listener.Action.START_SELECTION_MODE: 540 if (mIsEditMode) { 541 displayDeleteButtons(true); 542 mActionBarAdapter.setActionBarTitle(getString(R.string.title_edit_group)); 543 } else { 544 displayCheckBoxes(true); 545 } 546 mActivity.invalidateOptionsMenu(); 547 break; 548 case ActionBarAdapter.Listener.Action.STOP_SEARCH_AND_SELECTION_MODE: 549 mActionBarAdapter.setSearchMode(false); 550 if (mIsEditMode) { 551 displayDeleteButtons(false); 552 } else { 553 displayCheckBoxes(false); 554 } 555 mActivity.invalidateOptionsMenu(); 556 break; 557 case ActionBarAdapter.Listener.Action.BEGIN_STOPPING_SEARCH_AND_SELECTION_MODE: 558 break; 559 } 560 } 561 562 @Override 563 public void onUpButtonPressed() { 564 mActivity.onBackPressed(); 565 } 566 }; 567 568 private final OnCheckBoxListActionListener mCheckBoxListener = 569 new OnCheckBoxListActionListener() { 570 @Override 571 public void onStartDisplayingCheckBoxes() { 572 mActionBarAdapter.setSelectionMode(true); 573 } 574 575 @Override 576 public void onSelectedContactIdsChanged() { 577 if (mActionBarAdapter == null) { 578 return; 579 } 580 if (mIsEditMode) { 581 mActionBarAdapter.setActionBarTitle(getString(R.string.title_edit_group)); 582 } else { 583 mActionBarAdapter.setSelectionCount(getSelectedContactIds().size()); 584 } 585 } 586 587 @Override 588 public void onStopDisplayingCheckBoxes() { 589 mActionBarAdapter.setSelectionMode(false); 590 } 591 }; 592 593 private void logListEvent() { 594 Logger.logListEvent( 595 ListEvent.ActionType.REMOVE_LABEL, 596 getListType(), 597 getAdapter().getCount(), 598 /* clickedIndex */ -1, 599 getAdapter().getSelectedContactIdsArray().length); 600 } 601 602 private void deleteGroup() { 603 if (getMemberCount() == 0) { 604 final Intent intent = ContactSaveService.createGroupDeletionIntent( 605 getContext(), mGroupMetaData.groupId); 606 getContext().startService(intent); 607 mActivity.switchToAllContacts(); 608 } else { 609 GroupDeletionDialogFragment.show(getFragmentManager(), mGroupMetaData.groupId, 610 mGroupMetaData.groupName); 611 } 612 } 613 614 @Override 615 public void onActivityCreated(Bundle savedInstanceState) { 616 super.onActivityCreated(savedInstanceState); 617 mActivity = (PeopleActivity) getActivity(); 618 mActionBarAdapter = new ActionBarAdapter(mActivity, mActionBarListener, 619 mActivity.getSupportActionBar(), mActivity.getToolbar(), 620 R.string.enter_contact_name); 621 mActionBarAdapter.setShowHomeIcon(true); 622 final ContactsRequest contactsRequest = new ContactsRequest(); 623 contactsRequest.setActionCode(ContactsRequest.ACTION_GROUP); 624 mActionBarAdapter.initialize(savedInstanceState, contactsRequest); 625 if (mGroupMetaData != null) { 626 mActivity.setTitle(mGroupMetaData.groupName); 627 if (mGroupMetaData.editable) { 628 setCheckBoxListListener(mCheckBoxListener); 629 } 630 } 631 } 632 633 @Override 634 public ActionBarAdapter getActionBarAdapter() { 635 return mActionBarAdapter; 636 } 637 638 public void displayDeleteButtons(boolean displayDeleteButtons) { 639 getAdapter().setDisplayDeleteButtons(displayDeleteButtons); 640 } 641 642 public ArrayList<String> getMemberContactIds() { 643 return new ArrayList<>(mGroupMemberContactIds); 644 } 645 646 public int getMemberCount() { 647 return mGroupMemberContactIds.size(); 648 } 649 650 public boolean isEditMode() { 651 return mIsEditMode; 652 } 653 654 @Override 655 public void onCreate(Bundle savedState) { 656 super.onCreate(savedState); 657 if (savedState == null) { 658 mGroupUri = getArguments().getParcelable(ARG_GROUP_URI); 659 } else { 660 mIsEditMode = savedState.getBoolean(KEY_IS_EDIT_MODE); 661 mGroupUri = savedState.getParcelable(KEY_GROUP_URI); 662 mGroupMetaData = savedState.getParcelable(KEY_GROUP_METADATA); 663 } 664 maybeAttachCheckBoxListener(); 665 } 666 667 @Override 668 public void onResume() { 669 super.onResume(); 670 // Re-register the listener, which may have been cleared when onSaveInstanceState was 671 // called. See also: onSaveInstanceState 672 mActionBarAdapter.setListener(mActionBarListener); 673 } 674 675 @Override 676 protected void startLoading() { 677 if (mGroupMetaData == null || !mGroupMetaData.isValid()) { 678 getLoaderManager().restartLoader(LOADER_GROUP_METADATA, null, mGroupMetaDataCallbacks); 679 } else { 680 onGroupMetadataLoaded(); 681 } 682 } 683 684 @Override 685 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 686 if (data != null) { 687 // Wait until contacts are loaded before showing the scrollbar 688 setVisibleScrollbarEnabled(true); 689 690 final FilterCursorWrapper cursorWrapper = new FilterCursorWrapper(data); 691 bindMembersCount(cursorWrapper.getCount()); 692 super.onLoadFinished(loader, cursorWrapper); 693 // Update state of menu items (e.g. "Remove contacts") based on number of group members. 694 mActivity.invalidateOptionsMenu(); 695 mActionBarAdapter.updateOverflowButtonColor(); 696 } 697 } 698 699 private void bindMembersCount(int memberCount) { 700 final View accountFilterContainer = getView().findViewById( 701 R.id.account_filter_header_container); 702 final View emptyGroupView = getView().findViewById(R.id.empty_group); 703 if (memberCount > 0) { 704 final AccountWithDataSet accountWithDataSet = new AccountWithDataSet( 705 mGroupMetaData.accountName, mGroupMetaData.accountType, mGroupMetaData.dataSet); 706 bindListHeader(getContext(), getListView(), accountFilterContainer, 707 accountWithDataSet, memberCount); 708 emptyGroupView.setVisibility(View.GONE); 709 } else { 710 hideHeaderAndAddPadding(getContext(), getListView(), accountFilterContainer); 711 emptyGroupView.setVisibility(View.VISIBLE); 712 } 713 } 714 715 @Override 716 public void onSaveInstanceState(Bundle outState) { 717 super.onSaveInstanceState(outState); 718 if (mActionBarAdapter != null) { 719 mActionBarAdapter.setListener(null); 720 mActionBarAdapter.onSaveInstanceState(outState); 721 } 722 outState.putBoolean(KEY_IS_EDIT_MODE, mIsEditMode); 723 outState.putParcelable(KEY_GROUP_URI, mGroupUri); 724 outState.putParcelable(KEY_GROUP_METADATA, mGroupMetaData); 725 } 726 727 private void onGroupMetadataLoaded() { 728 if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "Loaded " + mGroupMetaData); 729 730 maybeAttachCheckBoxListener(); 731 732 mActivity.setTitle(mGroupMetaData.groupName); 733 mActivity.invalidateOptionsMenu(); 734 mActivity.updateDrawerGroupMenu(mGroupMetaData.groupId); 735 736 // Start loading the group members 737 super.startLoading(); 738 } 739 740 private void maybeAttachCheckBoxListener() { 741 // Don't attach the multi select check box listener if we can't edit the group 742 if (mGroupMetaData != null && mGroupMetaData.editable) { 743 setCheckBoxListListener(mCheckBoxListener); 744 } 745 } 746 747 @Override 748 protected GroupMembersAdapter createListAdapter() { 749 final GroupMembersAdapter adapter = new GroupMembersAdapter(getContext()); 750 adapter.setSectionHeaderDisplayEnabled(true); 751 adapter.setDisplayPhotos(true); 752 adapter.setDeleteContactListener(new DeletionListener()); 753 return adapter; 754 } 755 756 @Override 757 protected void configureAdapter() { 758 super.configureAdapter(); 759 if (mGroupMetaData != null) { 760 getAdapter().setGroupId(mGroupMetaData.groupId); 761 } 762 } 763 764 @Override 765 protected View inflateView(LayoutInflater inflater, ViewGroup container) { 766 final View view = inflater.inflate(R.layout.contact_list_content, /* root */ null); 767 final View emptyGroupView = inflater.inflate(R.layout.empty_group_view, null); 768 769 final ImageView image = (ImageView) emptyGroupView.findViewById(R.id.empty_group_image); 770 final LinearLayout.LayoutParams params = 771 (LinearLayout.LayoutParams) image.getLayoutParams(); 772 final int screenHeight = getResources().getDisplayMetrics().heightPixels; 773 params.setMargins(0, screenHeight / 774 getResources().getInteger(R.integer.empty_group_view_image_margin_divisor), 0, 0); 775 params.gravity = Gravity.CENTER_HORIZONTAL; 776 image.setLayoutParams(params); 777 778 final FrameLayout contactListLayout = (FrameLayout) view.findViewById(R.id.contact_list); 779 contactListLayout.addView(emptyGroupView); 780 781 final Button addContactsButton = 782 (Button) emptyGroupView.findViewById(R.id.add_member_button); 783 addContactsButton.setOnClickListener(new View.OnClickListener() { 784 @Override 785 public void onClick(View v) { 786 startActivityForResult(GroupUtil.createPickMemberIntent(getContext(), 787 mGroupMetaData, getMemberContactIds()), RESULT_GROUP_ADD_MEMBER); 788 } 789 }); 790 return view; 791 } 792 793 @Override 794 protected void onItemClick(int position, long id) { 795 final Uri uri = getAdapter().getContactUri(position); 796 if (uri == null) { 797 return; 798 } 799 if (getAdapter().isDisplayingCheckBoxes()) { 800 super.onItemClick(position, id); 801 return; 802 } 803 final int count = getAdapter().getCount(); 804 Logger.logListEvent(ListEvent.ActionType.CLICK, ListEvent.ListType.GROUP, count, 805 /* clickedIndex */ position, /* numSelected */ 0); 806 ImplicitIntentsUtil.startQuickContact( 807 getActivity(), uri, ScreenEvent.ScreenType.LIST_GROUP); 808 } 809 810 @Override 811 protected boolean onItemLongClick(int position, long id) { 812 if (mActivity != null && mIsEditMode) { 813 return true; 814 } 815 return super.onItemLongClick(position, id); 816 } 817 818 private final class DeletionListener implements DeleteContactListener { 819 @Override 820 public void onContactDeleteClicked(int position) { 821 final long contactId = getAdapter().getContactId(position); 822 final long[] contactIds = new long[1]; 823 contactIds[0] = contactId; 824 new UpdateGroupMembersAsyncTask(UpdateGroupMembersAsyncTask.TYPE_REMOVE, 825 getContext(), contactIds, mGroupMetaData.groupId, mGroupMetaData.accountName, 826 mGroupMetaData.accountType, mGroupMetaData.dataSet).execute(); 827 } 828 } 829 830 public GroupMetaData getGroupMetaData() { 831 return mGroupMetaData; 832 } 833 834 public boolean isCurrentGroup(long groupId) { 835 return mGroupMetaData != null && mGroupMetaData.groupId == groupId; 836 } 837 838 /** 839 * Return true if the fragment is not yet added, being removed, or detached. 840 */ 841 public boolean isInactive() { 842 return !isAdded() || isRemoving() || isDetached(); 843 } 844 845 @Override 846 public void onDestroy() { 847 if (mActionBarAdapter != null) { 848 mActionBarAdapter.setListener(null); 849 } 850 super.onDestroy(); 851 } 852 853 public void updateExistingGroupFragment(Uri newGroupUri, String action) { 854 toastForSaveAction(action); 855 856 if (isEditMode() && getGroupCount() == 1) { 857 // If we're deleting the last group member, exit edit mode 858 exitEditMode(); 859 } else if (!GroupUtil.ACTION_REMOVE_FROM_GROUP.equals(action)) { 860 mGroupUri = newGroupUri; 861 mGroupMetaData = null; // Clear mGroupMetaData to trigger a new load. 862 reloadData(); 863 mActivity.invalidateOptionsMenu(); 864 } 865 } 866 867 public void toastForSaveAction(String action) { 868 int id = -1; 869 switch(action) { 870 case GroupUtil.ACTION_UPDATE_GROUP: 871 id = R.string.groupUpdatedToast; 872 break; 873 case GroupUtil.ACTION_REMOVE_FROM_GROUP: 874 id = R.string.groupMembersRemovedToast; 875 break; 876 case GroupUtil.ACTION_CREATE_GROUP: 877 id = R.string.groupCreatedToast; 878 break; 879 case GroupUtil.ACTION_ADD_TO_GROUP: 880 id = R.string.groupMembersAddedToast; 881 break; 882 case GroupUtil.ACTION_SWITCH_GROUP: 883 // No toast associated with this action. 884 break; 885 default: 886 FeedbackHelper.sendFeedback(getContext(), TAG, 887 "toastForSaveAction passed unknown action: " + action, 888 new IllegalArgumentException("Unhandled contact save action " + action)); 889 } 890 toast(id); 891 } 892 893 private void toast(int resId) { 894 if (resId >= 0) { 895 Toast.makeText(getContext(), resId, Toast.LENGTH_SHORT).show(); 896 } 897 } 898 899 private int getGroupCount() { 900 return getAdapter() != null ? getAdapter().getCount() : -1; 901 } 902 903 public void exitEditMode() { 904 mIsEditMode = false; 905 mActionBarAdapter.setSelectionMode(false); 906 displayDeleteButtons(false); 907 } 908} 909