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