/** * Wire * Copyright (C) 2016 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.waz.zclient.pages.main.pickuser; import android.support.annotation.IntDef; import android.support.v7.widget.RecyclerView; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.waz.api.Contact; import com.waz.api.ContactDetails; import com.waz.api.Contacts; import com.waz.api.IConversation; import com.waz.api.User; import com.waz.zclient.R; import com.waz.zclient.pages.main.pickuser.controller.IPickUserController; import com.waz.zclient.pages.main.pickuser.views.ContactRowView; import com.waz.zclient.pages.main.pickuser.views.viewholders.AddressBookContactViewHolder; import com.waz.zclient.pages.main.pickuser.views.viewholders.AddressBookSectionHeaderViewHolder; import com.waz.zclient.pages.main.pickuser.views.viewholders.ConversationViewHolder; import com.waz.zclient.pages.main.pickuser.views.viewholders.SectionExpanderViewHolder; import com.waz.zclient.pages.main.pickuser.views.viewholders.SectionHeaderViewHolder; import com.waz.zclient.pages.main.pickuser.views.viewholders.TopUsersViewHolder; import com.waz.zclient.pages.main.pickuser.views.viewholders.UserViewHolder; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; public class SearchResultAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { @IntDef({ITEM_TYPE_TOP_USER, ITEM_TYPE_INITIAL, ITEM_TYPE_CONTACT, ITEM_TYPE_CONNECTED_USER, ITEM_TYPE_OTHER_USER, ITEM_TYPE_CONVERSATION, ITEM_TYPE_SECTION_HEADER, ITEM_TYPE_EXPAND_BUTTON }) @interface ItemType { } public static final int ITEM_TYPE_TOP_USER = 0; public static final int ITEM_TYPE_INITIAL = 1; public static final int ITEM_TYPE_CONTACT = 2; public static final int ITEM_TYPE_CONNECTED_USER = 3; public static final int ITEM_TYPE_OTHER_USER = 4; public static final int ITEM_TYPE_CONVERSATION = 5; public static final int ITEM_TYPE_SECTION_HEADER = 6; public static final int ITEM_TYPE_EXPAND_BUTTON = 7; public static final int ROW_COUNT_SECTION_HEADER = 1; public static final int COLLAPSED_LIMIT = 4; private Callback callback; private User[] connectedUsers; private User[] otherUsers; private User[] topUsers; private Contacts contacts; private IConversation[] conversations; private boolean showSearch; private boolean darkTheme; private SearchResultOnItemTouchListener topUsersOnItemTouchListener; private int itemCount; private int accentColor; private SparseArray<int[]> positionsMap; private ContactRowView.Callback contactsCallback; private boolean contactsCollapsed = true; private boolean groupsCollapsed = true; private List<SearchContact> mergedContacts = new ArrayList<>(); private class SearchContact { @ItemType public int itemType; public int index; public String name; SearchContact(int itemType, int index, String name) { this.itemType = itemType; this.index = index; this.name = name; } } private void updateMergedContacts() { mergedContacts.clear(); if (contacts != null) { for (int i = 0; i < contacts.size(); i++) { ContactDetails details = contacts.get(i).getDetails(); if (details != null) { mergedContacts.add(new SearchContact(ITEM_TYPE_CONTACT, i, details.getDisplayName())); } } } if (connectedUsers != null) { for (int i = 0; i < connectedUsers.length; i++) { mergedContacts.add(new SearchContact(ITEM_TYPE_CONNECTED_USER, i, connectedUsers[i].getDisplayName())); } } Collections.sort(mergedContacts, new Comparator<SearchContact>() { @Override public int compare(SearchContact o1, SearchContact o2) { return o1.name.compareToIgnoreCase(o2.name); } }); } public SearchResultAdapter(final Callback callback) { positionsMap = new SparseArray<>(); if (callback == null) { return; } this.callback = callback; this.contactsCallback = new ContactRowView.Callback() { @Override public void onContactListUserClicked(User user) { callback.onContactListUserClicked(user); } @Override public void onContactListContactClicked(ContactDetails contactDetails) { callback.onContactListContactClicked(contactDetails); } @Override public int getDestination() { return callback.getDestination(); } @Override public boolean isUserSelected(User user) { if (callback.getSelectedUsers() == null) { return false; } return callback.getSelectedUsers().contains(user); } }; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, @ItemType int viewType) { View view; switch (viewType) { case ITEM_TYPE_TOP_USER: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.startui_top_users, parent, false); TopUserAdapter topUserAdapter = new TopUserAdapter(new TopUserAdapter.Callback() { @Override public Set<User> getSelectedUsers() { return callback.getSelectedUsers(); } }); return new TopUsersViewHolder(view, topUserAdapter, parent.getContext()); case ITEM_TYPE_OTHER_USER: case ITEM_TYPE_CONNECTED_USER: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.startui_user, parent, false); return new UserViewHolder(view, darkTheme, true); case ITEM_TYPE_CONVERSATION: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.startui_conversation, parent, false); return new ConversationViewHolder(view); case ITEM_TYPE_SECTION_HEADER: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.startui_section_header, parent, false); return new SectionHeaderViewHolder(view); case ITEM_TYPE_INITIAL: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.startui_section_header, parent, false); return new AddressBookSectionHeaderViewHolder(view, darkTheme); case ITEM_TYPE_CONTACT: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.contactlist_user, parent, false); return new AddressBookContactViewHolder(view, darkTheme); case ITEM_TYPE_EXPAND_BUTTON: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.startui_section_expander, parent, false); return new SectionExpanderViewHolder(view); } return null; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { @ItemType int itemType = getItemViewType(position); switch (itemType) { case ITEM_TYPE_TOP_USER: ((TopUsersViewHolder) holder).bind(topUsers); ((TopUsersViewHolder) holder).bindOnItemTouchListener(topUsersOnItemTouchListener); break; case ITEM_TYPE_CONVERSATION: IConversation conversation = conversations[getConversationInternalPosition(position) - ROW_COUNT_SECTION_HEADER]; ((ConversationViewHolder) holder).bind(conversation); break; case ITEM_TYPE_OTHER_USER: User otherUser = otherUsers[getOtherUserInternalPosition(position) - ROW_COUNT_SECTION_HEADER]; boolean otherIsSelected = callback.getSelectedUsers().contains(otherUser); ((UserViewHolder) holder).bind(otherUser, otherIsSelected); break; case ITEM_TYPE_CONNECTED_USER: int index = mergedContacts.get(position - ROW_COUNT_SECTION_HEADER).index; User connectedUser = connectedUsers[index]; boolean contactIsSelected = callback.getSelectedUsers().contains(connectedUser); ((UserViewHolder) holder).bind(connectedUser, contactIsSelected); break; case ITEM_TYPE_SECTION_HEADER: int type = getSectionItemType(position); ((SectionHeaderViewHolder) holder).bind(type); break; case ITEM_TYPE_INITIAL: if (contacts == null || contacts.getInitials() == null || contacts.getInitials().isEmpty()) { break; } position = showSearch ? position - ROW_COUNT_SECTION_HEADER : position; String initial = getContactInitial(position); ((AddressBookSectionHeaderViewHolder) holder).bind(initial); break; case ITEM_TYPE_CONTACT: if (contacts == null || contacts.getInitials() == null || contacts.getInitials().isEmpty()) { break; } if (showSearch) { position = mergedContacts.get(position - ROW_COUNT_SECTION_HEADER).index; Contact contact = contacts.get(position); ((AddressBookContactViewHolder) holder).bind(contact, contactsCallback, accentColor); } else { int[] contactMapping = getContactMapping(position); String contactInitial = getContactInitial(position); int contactInternalPosition = contactMapping[2]; Contact contact = contacts.getContactForInitial(contactInitial, contactInternalPosition); ((AddressBookContactViewHolder) holder).bind(contact, contactsCallback, accentColor); } break; case ITEM_TYPE_EXPAND_BUTTON: if (getSectionForPosition(position) == ITEM_TYPE_CONNECTED_USER) { ((SectionExpanderViewHolder) holder).bind(mergedContacts.size(), new View.OnClickListener() { @Override public void onClick(View v) { setContactsCollapsed(false); } }); } else { ((SectionExpanderViewHolder) holder).bind(conversations.length, new View.OnClickListener() { @Override public void onClick(View v) { setGroupsCollapsed(false); } }); } break; } } @Override public int getItemCount() { return itemCount; } @Override public @ItemType int getItemViewType(int position) { @ItemType int type = -1; if (position < 0) { return type; } if (showSearch) { if (hasConnectedUsers() && position < getContactsSectionLength()) { // Connected users if (position == 0) { type = ITEM_TYPE_SECTION_HEADER; } else if (position == getContactsSectionLength() - 1 && isContactsCollapsed()) { type = ITEM_TYPE_EXPAND_BUTTON; } else { type = mergedContacts.get(position - ROW_COUNT_SECTION_HEADER).itemType; } } else if (hasConversations() && getConversationInternalPosition(position) < getGroupsSectionLength()) { // Conversations int internalPosition = getConversationInternalPosition(position); if (internalPosition == 0) { type = ITEM_TYPE_SECTION_HEADER; } else if (internalPosition == getGroupsSectionLength() - 1 && isGroupsCollapsed()) { type = ITEM_TYPE_EXPAND_BUTTON; } else { type = ITEM_TYPE_CONVERSATION; } } else { // Other users type = getOtherUserInternalPosition(position) == 0 ? ITEM_TYPE_SECTION_HEADER : ITEM_TYPE_OTHER_USER; } } else { if (hasTopUsers() && position < 2) { // Top users type = position == 0 ? ITEM_TYPE_SECTION_HEADER : ITEM_TYPE_TOP_USER; } else { int start = hasTopUsers() ? 2 : 0; if (position == start) { type = ITEM_TYPE_SECTION_HEADER; } else { int contactsPos = getContactInternalPosition(position); type = getContactItemViewType(contactsPos); } } } return type; } public void setAccentColor(int color) { accentColor = color; } public void setTopUsersOnItemTouchListener(SearchResultOnItemTouchListener topUsersOnItemTouchListener) { this.topUsersOnItemTouchListener = topUsersOnItemTouchListener; } public void setDarkTheme(boolean darkTheme) { this.darkTheme = darkTheme; } public void setTopUsers(User[] users) { showSearch = false; this.topUsers = users; updateItemCount(); notifyDataSetChanged(); } public void setContacts(Contacts contacts) { this.contacts = contacts; updateContactsPositionMapping(); updateMergedContacts(); updateItemCount(); notifyDataSetChanged(); } public void setSearchResult(User[] connectedUsers, User[] otherUsers, IConversation[] conversations) { showSearch = true; this.connectedUsers = connectedUsers; this.otherUsers = otherUsers; this.conversations = conversations; updateMergedContacts(); updateItemCount(); notifyDataSetChanged(); } public void reset() { connectedUsers = null; conversations = null; otherUsers = null; contacts = null; } public boolean hasTopUsers() { if (topUsers == null) { return false; } return topUsers.length > 0; } public boolean hasContacts() { return positionsMap.size() > 0; } public boolean hasConnectedUsers() { return connectedUsers != null && getContactsListLength() > 0; } public boolean hasOtherUsers() { return otherUsers != null && otherUsers.length > 0; } public boolean hasConversations() { return conversations != null && getGroupsListLength() > 0; } public int getConversationInternalPosition(int position) { if (hasConnectedUsers()) { position = position - getContactsSectionLength(); } return position; } private int getContactInternalPosition(int position) { if (hasTopUsers()) { // 2 section headers + 1 row for top users return position - 3; } // 1 for section header return position - 1; } private int getSearchContactInternalPosition(int position) { if (hasConnectedUsers()) { position = position - getContactsSectionLength(); } if (hasConversations()) { position = position - getGroupsSectionLength(); } return position; } public int getOtherUserInternalPosition(int position) { if (hasConnectedUsers()) { position = position - getContactsSectionLength(); } if (hasConversations()) { position = position - getGroupsSectionLength(); } return position; } private int getSectionItemType(int position) { int type = -1; if (showSearch) { if (hasConnectedUsers() && position == 0) { type = ITEM_TYPE_CONNECTED_USER; } else if (hasConversations() && getConversationInternalPosition(position) == 0) { type = ITEM_TYPE_CONVERSATION; } else if (hasOtherUsers() && getOtherUserInternalPosition(position) == 0) { type = ITEM_TYPE_OTHER_USER; } } else { if (hasTopUsers() && position < 2) { type = ITEM_TYPE_TOP_USER; } else { type = ITEM_TYPE_CONTACT; } } return type; } private int getSectionForPosition(int position) { int type = -1; if (showSearch) { if (hasConnectedUsers() && position < getContactsSectionLength()) { type = ITEM_TYPE_CONNECTED_USER; } else if (hasConversations() && getConversationInternalPosition(position) < getGroupsSectionLength()) { type = ITEM_TYPE_CONVERSATION; } else if (hasOtherUsers() && getOtherUserInternalPosition(position) < otherUsers.length + ROW_COUNT_SECTION_HEADER) { type = ITEM_TYPE_OTHER_USER; } } else { if (hasTopUsers() && position < 2) { type = ITEM_TYPE_TOP_USER; } else { type = ITEM_TYPE_CONTACT; } } return type; } private void updateItemCount() { itemCount = 0; if (showSearch) { if (hasConnectedUsers()) { itemCount += getContactsSectionLength(); } if (hasConversations()) { itemCount += getGroupsSectionLength(); } if (hasOtherUsers()) { itemCount += otherUsers.length + ROW_COUNT_SECTION_HEADER; } } else { if (hasTopUsers()) { // If top users are visible, are extra row and section header = 2 itemCount += 2; } if (hasContacts()) { itemCount += ROW_COUNT_SECTION_HEADER + positionsMap.size(); } } } public @ItemType int getContactItemViewType(int position) { int[] mapping = positionsMap.get(position); if (mapping[0] == ITEM_TYPE_CONTACT) { return ITEM_TYPE_CONTACT; } return ITEM_TYPE_INITIAL; } private void updateContactsPositionMapping() { positionsMap.clear(); if (contacts == null) { return; } int pos = 0; int initialPos = 0; for (String initial : contacts.getInitials()) { positionsMap.put(pos, new int[] {ITEM_TYPE_INITIAL, initialPos, -1}); int numContactsForInitial = contacts.getNumberOfContactsForInitial(initial); for (int contactPos = 0; contactPos < numContactsForInitial; contactPos++) { pos++; positionsMap.put(pos, new int[] {ITEM_TYPE_CONTACT, initialPos, contactPos}); } pos++; initialPos++; } } private int[] getContactMapping(int position) { position = showSearch ? getSearchContactInternalPosition(position) : getContactInternalPosition(position); int[] mapping = positionsMap.get(position); return mapping; } private String getContactInitial(int position) { String[] initials = contacts.getInitials().toArray(new String[contacts.getInitials().size()]); int[] mapping = getContactMapping(position); return initials[mapping[1]]; } public boolean isContactsCollapsed() { return mergedContacts.size() > COLLAPSED_LIMIT && contactsCollapsed; } public void setContactsCollapsed(boolean collapsed) { contactsCollapsed = collapsed; updateItemCount(); notifyDataSetChanged(); } private int getContactsListLength() { return isContactsCollapsed() ? COLLAPSED_LIMIT : mergedContacts.size(); } private int getContactsSectionLength() { return getContactsListLength() + ROW_COUNT_SECTION_HEADER + (isContactsCollapsed() ? 1 : 0); } public boolean isGroupsCollapsed() { return conversations.length > COLLAPSED_LIMIT && groupsCollapsed; } public void setGroupsCollapsed(boolean collapsed) { groupsCollapsed = collapsed; updateItemCount(); notifyDataSetChanged(); } private int getGroupsListLength() { return isGroupsCollapsed() ? COLLAPSED_LIMIT : conversations.length; } private int getGroupsSectionLength() { return getGroupsListLength() + ROW_COUNT_SECTION_HEADER + (isGroupsCollapsed() ? 1 : 0); } public interface Callback { Set<User> getSelectedUsers(); void onContactListUserClicked(User user); void onContactListContactClicked(ContactDetails contactDetails); @IPickUserController.ContactListDestination int getDestination(); } }