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