From e6a78405ed85e923458f68d420212c150b1e6a2e Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Tue, 10 May 2022 20:37:52 +0200 Subject: [PATCH] Migrate ContactsController to kotlin + viewbinding Signed-off-by: Andy Scherzinger --- .../talk/controllers/ContactsController.java | 1005 ----------------- .../talk/controllers/ContactsController.kt | 958 ++++++++++++++++ .../talk/controllers/ProfileController.kt | 1 + .../json/converters/EnumActorTypeConverter.kt | 2 +- .../res/layout/controller_contacts_rv.xml | 3 + 5 files changed, 963 insertions(+), 1006 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java create mode 100644 app/src/main/java/com/nextcloud/talk/controllers/ContactsController.kt diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java deleted file mode 100644 index 1f5dee4af..000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java +++ /dev/null @@ -1,1005 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * @author Marcel Hibbe - * Copyright (C) 2017 Mario Danic - * Copyright (C) 2022 Marcel Hibbe - * - * 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 . - */ - -package com.nextcloud.talk.controllers; - -import static com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum.OPS_CODE_INVITE_USERS; -import static com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum.OPS_CODE_GET_AND_JOIN_ROOM; - -import android.app.SearchManager; -import android.content.Context; -import android.graphics.PorterDuff; -import android.os.Build; -import android.os.Bundle; -import android.text.InputType; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; - -import com.bluelinelabs.logansquare.LoganSquare; -import com.nextcloud.talk.R; -import com.nextcloud.talk.adapters.items.GenericTextHeaderItem; -import com.nextcloud.talk.adapters.items.ContactItem; -import com.nextcloud.talk.api.NcApi; -import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.controllers.base.BaseController; -import com.nextcloud.talk.events.OpenConversationEvent; -import com.nextcloud.talk.jobs.AddParticipantsToConversation; -import com.nextcloud.talk.models.RetrofitBucket; -import com.nextcloud.talk.models.database.CapabilitiesUtil; -import com.nextcloud.talk.models.database.UserEntity; -import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall; -import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser; -import com.nextcloud.talk.models.json.conversations.Conversation; -import com.nextcloud.talk.models.json.conversations.RoomOverall; -import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter; -import com.nextcloud.talk.models.json.participants.Participant; -import com.nextcloud.talk.ui.dialog.ContactsBottomDialog; -import com.nextcloud.talk.utils.ApiUtils; -import com.nextcloud.talk.utils.ConductorRemapping; -import com.nextcloud.talk.utils.bundle.BundleKeys; -import com.nextcloud.talk.utils.database.user.UserUtils; -import com.nextcloud.talk.utils.preferences.AppPreferences; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.jetbrains.annotations.NotNull; -import org.parceler.Parcels; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.SearchView; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.content.res.ResourcesCompat; -import androidx.core.view.MenuItemCompat; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import androidx.work.Data; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; -import autodagger.AutoInjector; -import butterknife.BindView; -import butterknife.OnClick; -import butterknife.Optional; -import eu.davidea.flexibleadapter.FlexibleAdapter; -import eu.davidea.flexibleadapter.SelectableAdapter; -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager; -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; -import io.reactivex.Observer; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import okhttp3.ResponseBody; - -@AutoInjector(NextcloudTalkApplication.class) -public class ContactsController extends BaseController implements SearchView.OnQueryTextListener, - FlexibleAdapter.OnItemClickListener { - - public static final String TAG = "ContactsController"; - - @Nullable - @BindView(R.id.initial_relative_layout) - RelativeLayout initialRelativeLayout; - - @Nullable - @BindView(R.id.secondary_relative_layout) - RelativeLayout secondaryRelativeLayout; - - @BindView(R.id.loading_content) - LinearLayout loadingContent; - - @BindView(R.id.recycler_view) - RecyclerView recyclerView; - - @BindView(R.id.swipe_refresh_layout) - SwipeRefreshLayout swipeRefreshLayout; - - @BindView(R.id.call_header_layout) - RelativeLayout conversationPrivacyToogleLayout; - - @BindView(R.id.joinConversationViaLinkRelativeLayout) - RelativeLayout joinConversationViaLinkLayout; - - @BindView(R.id.joinConversationViaLinkImageView) - ImageView joinConversationViaLinkImageView; - - @BindView(R.id.public_call_link) - ImageView publicCallLinkImageView; - - @BindView(R.id.generic_rv_layout) - CoordinatorLayout genericRvLayout; - - @Inject - UserUtils userUtils; - - @Inject - EventBus eventBus; - - @Inject - AppPreferences appPreferences; - - @Inject - NcApi ncApi; - - private String credentials; - private UserEntity currentUser; - private Disposable contactsQueryDisposable; - private Disposable cacheQueryDisposable; - private FlexibleAdapter adapter; - private List contactItems; - - private SmoothScrollLinearLayoutManager layoutManager; - - private MenuItem searchItem; - private SearchView searchView; - - private boolean isNewConversationView; - private boolean isPublicCall; - - private HashMap userHeaderItems = new HashMap<>(); - - private boolean alreadyFetching = false; - - private MenuItem doneMenuItem; - - private Set selectedUserIds; - private Set selectedGroupIds; - private Set selectedCircleIds; - private Set selectedEmails; - private List existingParticipants; - private boolean isAddingParticipantsView; - private String conversationToken; - - private ContactsBottomDialog contactsBottomDialog; - - public ContactsController() { - super(); - setHasOptionsMenu(true); - } - - public ContactsController(Bundle args) { - super(args); - setHasOptionsMenu(true); - if (args.containsKey(BundleKeys.INSTANCE.getKEY_NEW_CONVERSATION())) { - isNewConversationView = true; - existingParticipants = new ArrayList<>(); - } else if (args.containsKey(BundleKeys.INSTANCE.getKEY_ADD_PARTICIPANTS())) { - isAddingParticipantsView = true; - conversationToken = args.getString(BundleKeys.INSTANCE.getKEY_TOKEN()); - - existingParticipants = new ArrayList<>(); - - if (args.containsKey(BundleKeys.INSTANCE.getKEY_EXISTING_PARTICIPANTS())) { - existingParticipants = args.getStringArrayList(BundleKeys.INSTANCE.getKEY_EXISTING_PARTICIPANTS()); - } - } - - selectedUserIds = new HashSet<>(); - selectedGroupIds = new HashSet<>(); - selectedEmails = new HashSet<>(); - selectedCircleIds = new HashSet<>(); - } - - @Override - protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { - return inflater.inflate(R.layout.controller_contacts_rv, container, false); - } - - @Override - protected void onAttach(@NonNull View view) { - super.onAttach(view); - eventBus.register(this); - - if (isNewConversationView) { - toggleNewCallHeaderVisibility(!isPublicCall); - } - - if (isAddingParticipantsView) { - joinConversationViaLinkLayout.setVisibility(View.GONE); - conversationPrivacyToogleLayout.setVisibility(View.GONE); - } - } - - @Override - protected void onViewBound(@NonNull View view) { - super.onViewBound(view); - NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); - - currentUser = userUtils.getCurrentUser(); - - if (currentUser != null) { - credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()); - } - - if (adapter == null) { - contactItems = new ArrayList<>(); - adapter = new FlexibleAdapter<>(contactItems, getActivity(), false); - - if (currentUser != null) { - fetchData(); - } - } - - setupAdapter(); - prepareViews(); - } - - private void setupAdapter() { - adapter.setNotifyChangeOfUnfilteredItems(true) - .setMode(SelectableAdapter.Mode.MULTI); - - adapter.setStickyHeaderElevation(5) - .setUnlinkAllItemsOnRemoveHeaders(true) - .setDisplayHeadersAtStartUp(true) - .setStickyHeaders(true); - - adapter.addListener(this); - } - - private void selectionDone() { - if (!isAddingParticipantsView) { - if (!isPublicCall && (selectedCircleIds.size() + selectedGroupIds.size() + selectedUserIds.size() == 1)) { - String userId; - String sourceType = null; - String roomType = "1"; - - if (selectedGroupIds.size() == 1) { - roomType = "2"; - userId = selectedGroupIds.iterator().next(); - } else if (selectedCircleIds.size() == 1) { - roomType = "2"; - sourceType = "circles"; - userId = selectedCircleIds.iterator().next(); - } else { - userId = selectedUserIds.iterator().next(); - } - - int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[]{ApiUtils.APIv4, 1}); - RetrofitBucket retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(apiVersion, - currentUser.getBaseUrl(), - roomType, - sourceType, - userId, - null); - ncApi.createRoom(credentials, - retrofitBucket.getUrl(), retrofitBucket.getQueryMap()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(RoomOverall roomOverall) { - Bundle bundle = new Bundle(); - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY(), currentUser); - bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), roomOverall.getOcs().getData().getToken()); - bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_ID(), roomOverall.getOcs().getData().getRoomId()); - - // FIXME once APIv2 or later is used only, the createRoom already returns all the data - ncApi.getRoom(credentials, - ApiUtils.getUrlForRoom(apiVersion, currentUser.getBaseUrl(), - roomOverall.getOcs().getData().getToken())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(RoomOverall roomOverall) { - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_ACTIVE_CONVERSATION(), - Parcels.wrap(roomOverall.getOcs().getData())); - - ConductorRemapping.INSTANCE.remapChatController(getRouter(), currentUser.getId(), - roomOverall.getOcs().getData().getToken(), bundle, true); - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - - } - }); - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - } - }); - } else { - - Bundle bundle = new Bundle(); - Conversation.ConversationType roomType; - if (isPublicCall) { - roomType = Conversation.ConversationType.ROOM_PUBLIC_CALL; - } else { - roomType = Conversation.ConversationType.ROOM_GROUP_CALL; - } - - ArrayList userIdsArray = new ArrayList<>(selectedUserIds); - ArrayList groupIdsArray = new ArrayList<>(selectedGroupIds); - ArrayList emailsArray = new ArrayList<>(selectedEmails); - ArrayList circleIdsArray = new ArrayList<>(selectedCircleIds); - - - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_CONVERSATION_TYPE(), Parcels.wrap(roomType)); - bundle.putStringArrayList(BundleKeys.INSTANCE.getKEY_INVITED_PARTICIPANTS(), userIdsArray); - bundle.putStringArrayList(BundleKeys.INSTANCE.getKEY_INVITED_GROUP(), groupIdsArray); - bundle.putStringArrayList(BundleKeys.INSTANCE.getKEY_INVITED_EMAIL(), emailsArray); - bundle.putStringArrayList(BundleKeys.INSTANCE.getKEY_INVITED_CIRCLE(), circleIdsArray); - bundle.putSerializable(BundleKeys.INSTANCE.getKEY_OPERATION_CODE(), OPS_CODE_INVITE_USERS); - prepareAndShowBottomSheetWithBundle(bundle); - } - } else { - String[] userIdsArray = selectedUserIds.toArray(new String[selectedUserIds.size()]); - String[] groupIdsArray = selectedGroupIds.toArray(new String[selectedGroupIds.size()]); - String[] emailsArray = selectedEmails.toArray(new String[selectedEmails.size()]); - String[] circleIdsArray = selectedCircleIds.toArray(new String[selectedCircleIds.size()]); - - Data.Builder data = new Data.Builder(); - data.putLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), currentUser.getId()); - data.putString(BundleKeys.INSTANCE.getKEY_TOKEN(), conversationToken); - data.putStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_USERS(), userIdsArray); - data.putStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_GROUPS(), groupIdsArray); - data.putStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_EMAILS(), emailsArray); - data.putStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_CIRCLES(), circleIdsArray); - - OneTimeWorkRequest addParticipantsToConversationWorker = - new OneTimeWorkRequest.Builder(AddParticipantsToConversation.class).setInputData(data.build()).build(); - WorkManager.getInstance().enqueue(addParticipantsToConversationWorker); - - getRouter().popCurrentController(); - } - } - - private void initSearchView() { - if (getActivity() != null) { - SearchManager searchManager = (SearchManager) getActivity().getSystemService(Context.SEARCH_SERVICE); - if (searchItem != null) { - searchView = (SearchView) MenuItemCompat.getActionView(searchItem); - searchView.setMaxWidth(Integer.MAX_VALUE); - searchView.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER); - int imeOptions = EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_FULLSCREEN; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences.getIsKeyboardIncognito()) { - imeOptions |= EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING; - } - searchView.setImeOptions(imeOptions); - searchView.setQueryHint(getResources().getString(R.string.nc_search)); - if (searchManager != null) { - searchView.setSearchableInfo(searchManager.getSearchableInfo(getActivity().getComponentName())); - } - searchView.setOnQueryTextListener(this); - } - } - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - int itemId = item.getItemId(); - if (itemId == android.R.id.home) { - return getRouter().popCurrentController(); - } else if (itemId == R.id.contacts_selection_done) { - selectionDone(); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.menu_contacts, menu); - searchItem = menu.findItem(R.id.action_search); - doneMenuItem = menu.findItem(R.id.contacts_selection_done); - - initSearchView(); - } - - @Override - public void onPrepareOptionsMenu(@NonNull Menu menu) { - super.onPrepareOptionsMenu(menu); - checkAndHandleDoneMenuItem(); - if (adapter.hasFilter()) { - searchItem.expandActionView(); - searchView.setQuery((CharSequence) adapter.getFilter(String.class), false); - } - } - - private void fetchData() { - dispose(null); - - alreadyFetching = true; - Set autocompleteUsersHashSet = new HashSet<>(); - - userHeaderItems = new HashMap<>(); - - String query = (String) adapter.getFilter(String.class); - - RetrofitBucket retrofitBucket = ApiUtils.getRetrofitBucketForContactsSearchFor14(currentUser.getBaseUrl(), query); - Map modifiedQueryMap = new HashMap(retrofitBucket.getQueryMap()); - modifiedQueryMap.put("limit", 50); - - if (isAddingParticipantsView) { - modifiedQueryMap.put("itemId", conversationToken); - } - - List shareTypesList; - - shareTypesList = new ArrayList<>(); - // users - shareTypesList.add("0"); - if (!isAddingParticipantsView) { - // groups - shareTypesList.add("1"); - } else if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails")) { - // groups - shareTypesList.add("1"); - // emails - shareTypesList.add("4"); - } - if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "circles-support")) { - // circles - shareTypesList.add("7"); - } - - modifiedQueryMap.put("shareTypes[]", shareTypesList); - - ncApi.getContactsWithSearchParam( - credentials, - retrofitBucket.getUrl(), shareTypesList, modifiedQueryMap) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .retry(3) - .subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable d) { - contactsQueryDisposable = d; - } - - @Override - public void onNext(@NotNull ResponseBody responseBody) { - if (responseBody != null) { - Participant participant; - - List newUserItemList = new ArrayList<>(); - EnumActorTypeConverter actorTypeConverter = new EnumActorTypeConverter(); - - try { - AutocompleteOverall autocompleteOverall = LoganSquare.parse( - responseBody.string(), - AutocompleteOverall.class); - autocompleteUsersHashSet.addAll(autocompleteOverall.getOcs().getData()); - - for (AutocompleteUser autocompleteUser : autocompleteUsersHashSet) { - if (!autocompleteUser.getId().equals(currentUser.getUserId()) - && !existingParticipants.contains(autocompleteUser.getId())) { - participant = new Participant(); - participant.setActorId(autocompleteUser.getId()); - participant.setActorType(actorTypeConverter.getFromString(autocompleteUser.getSource())); - participant.setDisplayName(autocompleteUser.getLabel()); - participant.setSource(autocompleteUser.getSource()); - - String headerTitle; - - if (participant.getActorType() == Participant.ActorType.GROUPS) { - headerTitle = getResources().getString(R.string.nc_groups); - } else if (participant.getActorType() == Participant.ActorType.CIRCLES) { - headerTitle = getResources().getString(R.string.nc_circles); - } else { - headerTitle = - participant.getDisplayName().substring(0, 1).toUpperCase(Locale.getDefault()); - } - - GenericTextHeaderItem genericTextHeaderItem; - if (!userHeaderItems.containsKey(headerTitle)) { - genericTextHeaderItem = new GenericTextHeaderItem(headerTitle); - userHeaderItems.put(headerTitle, genericTextHeaderItem); - } - - ContactItem newContactItem = new ContactItem( - participant, - currentUser, - userHeaderItems.get(headerTitle) - ); - - if (!contactItems.contains(newContactItem)) { - newUserItemList.add(newContactItem); - } - } - } - } catch (IOException ioe) { - Log.e(TAG, "Parsing response body failed while getting contacts", ioe); - } - - userHeaderItems = new HashMap<>(); - contactItems.addAll(newUserItemList); - - Collections.sort(newUserItemList, (o1, o2) -> { - String firstName; - String secondName; - - if (o1 instanceof ContactItem) { - firstName = ((ContactItem) o1).getModel().getDisplayName(); - } else { - firstName = ((GenericTextHeaderItem) o1).getModel(); - } - - if (o2 instanceof ContactItem) { - secondName = ((ContactItem) o2).getModel().getDisplayName(); - } else { - secondName = ((GenericTextHeaderItem) o2).getModel(); - } - - if (o1 instanceof ContactItem && o2 instanceof ContactItem) { - String firstSource = ((ContactItem) o1).getModel().getSource(); - String secondSource = ((ContactItem) o2).getModel().getSource(); - if (firstSource.equals(secondSource)) { - return firstName.compareToIgnoreCase(secondName); - } - - // First users - if ("users".equals(firstSource)) { - return -1; - } else if ("users".equals(secondSource)) { - return 1; - } - - // Then groups - if ("groups".equals(firstSource)) { - return -1; - } else if ("groups".equals(secondSource)) { - return 1; - } - - // Then circles - if ("circles".equals(firstSource)) { - return -1; - } else if ("circles".equals(secondSource)) { - return 1; - } - - // Otherwise fall back to name sorting - return firstName.compareToIgnoreCase(secondName); - } - - return firstName.compareToIgnoreCase(secondName); - }); - - Collections.sort(contactItems, (o1, o2) -> { - String firstName; - String secondName; - - if (o1 instanceof ContactItem) { - firstName = ((ContactItem) o1).getModel().getDisplayName(); - } else { - firstName = ((GenericTextHeaderItem) o1).getModel(); - } - - if (o2 instanceof ContactItem) { - secondName = ((ContactItem) o2).getModel().getDisplayName(); - } else { - secondName = ((GenericTextHeaderItem) o2).getModel(); - } - - if (o1 instanceof ContactItem && o2 instanceof ContactItem) { - if ("groups".equals(((ContactItem) o1).getModel().getSource()) && - "groups".equals(((ContactItem) o2).getModel().getSource())) { - return firstName.compareToIgnoreCase(secondName); - } else if ("groups".equals(((ContactItem) o1).getModel().getSource())) { - return -1; - } else if ("groups".equals(((ContactItem) o2).getModel().getSource())) { - return 1; - } - } - - return firstName.compareToIgnoreCase(secondName); - }); - - if (newUserItemList.size() > 0) { - adapter.updateDataSet(newUserItemList); - } else { - adapter.filterItems(); - } - - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - } - } - - @Override - public void onError(@NotNull Throwable e) { - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - dispose(contactsQueryDisposable); - } - - @Override - public void onComplete() { - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - dispose(contactsQueryDisposable); - alreadyFetching = false; - - disengageProgressBar(); - } - }); - } - - private void prepareViews() { - layoutManager = new SmoothScrollLinearLayoutManager(getActivity()); - recyclerView.setLayoutManager(layoutManager); - recyclerView.setHasFixedSize(true); - recyclerView.setAdapter(adapter); - - swipeRefreshLayout.setOnRefreshListener(this::fetchData); - swipeRefreshLayout.setColorSchemeResources(R.color.colorPrimary); - swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background); - - joinConversationViaLinkImageView - .getBackground() - .setColorFilter(ResourcesCompat.getColor(getResources(), R.color.colorBackgroundDarker, null), - PorterDuff.Mode.SRC_IN); - - publicCallLinkImageView - .getBackground() - .setColorFilter(ResourcesCompat.getColor(getResources(), R.color.colorPrimary, null), - PorterDuff.Mode.SRC_IN); - - disengageProgressBar(); - } - - private void disengageProgressBar() { - if (!alreadyFetching) { - loadingContent.setVisibility(View.GONE); - genericRvLayout.setVisibility(View.VISIBLE); - - if (isNewConversationView) { - conversationPrivacyToogleLayout.setVisibility(View.VISIBLE); - joinConversationViaLinkLayout.setVisibility(View.VISIBLE); - } - } - } - - private void dispose(@Nullable Disposable disposable) { - if (disposable != null && !disposable.isDisposed()) { - disposable.dispose(); - } else if (disposable == null) { - - if (contactsQueryDisposable != null && !contactsQueryDisposable.isDisposed()) { - contactsQueryDisposable.dispose(); - contactsQueryDisposable = null; - } - - if (cacheQueryDisposable != null && !cacheQueryDisposable.isDisposed()) { - cacheQueryDisposable.dispose(); - cacheQueryDisposable = null; - } - } - } - - - @Override - public void onSaveViewState(@NonNull View view, @NonNull Bundle outState) { - adapter.onSaveInstanceState(outState); - super.onSaveViewState(view, outState); - } - - @Override - public void onRestoreViewState(@NonNull View view, @NonNull Bundle savedViewState) { - super.onRestoreViewState(view, savedViewState); - if (adapter != null) { - adapter.onRestoreInstanceState(savedViewState); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - dispose(null); - } - - @Override - public boolean onQueryTextChange(String newText) { - if (!newText.equals("") && adapter.hasNewFilter(newText)) { - adapter.setFilter(newText); - fetchData(); - } else if (newText.equals("")) { - adapter.setFilter(""); - adapter.updateDataSet(contactItems); - } - - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setEnabled(!adapter.hasFilter()); - } - - return true; - } - - @Override - public boolean onQueryTextSubmit(String query) { - return onQueryTextChange(query); - } - - private void checkAndHandleDoneMenuItem() { - if (adapter != null && doneMenuItem != null) { - if ((selectedCircleIds.size() + selectedEmails.size() + selectedGroupIds.size() + selectedUserIds.size() > 0) || isPublicCall) { - doneMenuItem.setVisible(true); - } else { - doneMenuItem.setVisible(false); - } - } else if (doneMenuItem != null) { - doneMenuItem.setVisible(false); - } - } - - @Override - protected String getTitle() { - if (isAddingParticipantsView) { - return getResources().getString(R.string.nc_add_participants); - } else if (isNewConversationView) { - return getResources().getString(R.string.nc_select_participants); - } else { - return getResources().getString(R.string.nc_app_product_name); - } - } - - private void prepareAndShowBottomSheetWithBundle(Bundle bundle) { - // 11: create conversation-enter name for new conversation - // 10: get&join room when enter link - contactsBottomDialog = new ContactsBottomDialog(getActivity(), bundle); - contactsBottomDialog.show(); - } - - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onMessageEvent(OpenConversationEvent openConversationEvent) { - ConductorRemapping.INSTANCE.remapChatController(getRouter(), currentUser.getId(), - openConversationEvent.getConversation().getToken(), - openConversationEvent.getBundle(), true); - if (contactsBottomDialog != null) { - contactsBottomDialog.dismiss(); - } - } - - @Override - protected void onDetach(@NonNull View view) { - super.onDetach(view); - eventBus.unregister(this); - } - - @Override - public boolean onItemClick(View view, int position) { - if (adapter.getItem(position) instanceof ContactItem) { - if (!isNewConversationView && !isAddingParticipantsView) { - ContactItem contactItem = (ContactItem) adapter.getItem(position); - String roomType = "1"; - - if ("groups".equals(contactItem.getModel().getSource())) { - roomType = "2"; - } - - int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[]{ApiUtils.APIv4, 1}); - - RetrofitBucket retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(apiVersion, - currentUser.getBaseUrl(), - roomType, - null, - contactItem.getModel().getActorId(), - null); - - ncApi.createRoom(credentials, - retrofitBucket.getUrl(), retrofitBucket.getQueryMap()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(RoomOverall roomOverall) { - if (getActivity() != null) { - Bundle bundle = new Bundle(); - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY(), currentUser); - bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), roomOverall.getOcs().getData().getToken()); - bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_ID(), roomOverall.getOcs().getData().getRoomId()); - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_ACTIVE_CONVERSATION(), - Parcels.wrap(roomOverall.getOcs().getData())); - - ConductorRemapping.INSTANCE.remapChatController(getRouter(), currentUser.getId(), - roomOverall.getOcs().getData().getToken(), bundle, true); - } - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - - } - }); - } else { - Participant participant = ((ContactItem) adapter.getItem(position)).getModel(); - participant.setSelected(!participant.isSelected()); - - if ("groups".equals(participant.getSource())) { - if (participant.isSelected()) { - selectedGroupIds.add(participant.getActorId()); - } else { - selectedGroupIds.remove(participant.getActorId()); - } - } else if ("emails".equals(participant.getSource())) { - if (participant.isSelected()) { - selectedEmails.add(participant.getActorId()); - } else { - selectedEmails.remove(participant.getActorId()); - } - } else if ("circles".equals(participant.getSource())) { - if (participant.isSelected()) { - selectedCircleIds.add(participant.getActorId()); - } else { - selectedCircleIds.remove(participant.getActorId()); - } - } else { - if (participant.isSelected()) { - selectedUserIds.add(participant.getActorId()); - } else { - selectedUserIds.remove(participant.getActorId()); - } - } - - if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "last-room-activity") - && !CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails") && - "groups".equals(((ContactItem) adapter.getItem(position)).getModel().getSource()) && - participant.isSelected() && - adapter.getSelectedItemCount() > 1) { - List currentItems = adapter.getCurrentItems(); - Participant internalParticipant; - for (int i = 0; i < currentItems.size(); i++) { - internalParticipant = currentItems.get(i).getModel(); - if (internalParticipant.getActorId().equals(participant.getActorId()) && - internalParticipant.getActorType() == Participant.ActorType.GROUPS && - internalParticipant.isSelected()) { - internalParticipant.setSelected(false); - selectedGroupIds.remove(internalParticipant.getActorId()); - } - } - - } - - adapter.notifyDataSetChanged(); - checkAndHandleDoneMenuItem(); - } - } - return true; - } - - @Optional - @OnClick(R.id.joinConversationViaLinkRelativeLayout) - void joinConversationViaLink() { - Bundle bundle = new Bundle(); - bundle.putSerializable(BundleKeys.INSTANCE.getKEY_OPERATION_CODE(), OPS_CODE_GET_AND_JOIN_ROOM); - - prepareAndShowBottomSheetWithBundle(bundle); - } - - @Optional - @OnClick(R.id.call_header_layout) - void toggleCallHeader() { - toggleNewCallHeaderVisibility(isPublicCall); - isPublicCall = !isPublicCall; - - if (isPublicCall) { - joinConversationViaLinkLayout.setVisibility(View.GONE); - } else { - joinConversationViaLinkLayout.setVisibility(View.VISIBLE); - } - - if (isPublicCall) { - List currentItems = adapter.getCurrentItems(); - Participant internalParticipant; - for (int i = 0; i < currentItems.size(); i++) { - if (currentItems.get(i) instanceof ContactItem) { - internalParticipant = ((ContactItem) currentItems.get(i)).getModel(); - if (internalParticipant.getActorType() == Participant.ActorType.GROUPS && - internalParticipant.isSelected()) { - internalParticipant.setSelected(false); - selectedGroupIds.remove(internalParticipant.getActorId()); - } - } - } - } - - for (int i = 0; i < adapter.getItemCount(); i++) { - if (adapter.getItem(i) instanceof ContactItem) { - ContactItem contactItem = (ContactItem) adapter.getItem(i); - if ("groups".equals(contactItem.getModel().getSource())) { - contactItem.setEnabled(!isPublicCall); - } - } - } - - checkAndHandleDoneMenuItem(); - adapter.notifyDataSetChanged(); - } - - private void toggleNewCallHeaderVisibility(boolean showInitialLayout) { - if (showInitialLayout) { - if (initialRelativeLayout != null) { - initialRelativeLayout.setVisibility(View.VISIBLE); - } - if (secondaryRelativeLayout != null) { - secondaryRelativeLayout.setVisibility(View.GONE); - } - } else { - if (initialRelativeLayout != null) { - initialRelativeLayout.setVisibility(View.GONE); - } - if (secondaryRelativeLayout != null) { - secondaryRelativeLayout.setVisibility(View.VISIBLE); - } - } - } -} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.kt new file mode 100644 index 000000000..d2ca359ef --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.kt @@ -0,0 +1,958 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * @author Marcel Hibbe + * @author Andy Scherzinger + * Copyright (C) 2017 Mario Danic + * Copyright (C) 2022 Marcel Hibbe + * Copyright (C) 2022 Andy Scherzinger + * + * 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 . + */ +package com.nextcloud.talk.controllers + +import android.app.SearchManager +import android.content.Context +import android.graphics.PorterDuff +import android.os.Build +import android.os.Bundle +import android.text.InputType +import android.util.Log +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.appcompat.widget.SearchView +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.MenuItemCompat +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import autodagger.AutoInjector +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.items.ContactItem +import com.nextcloud.talk.adapters.items.GenericTextHeaderItem +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.controllers.base.NewBaseController +import com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum +import com.nextcloud.talk.controllers.util.viewBinding +import com.nextcloud.talk.databinding.ControllerContactsRvBinding +import com.nextcloud.talk.events.OpenConversationEvent +import com.nextcloud.talk.jobs.AddParticipantsToConversation +import com.nextcloud.talk.models.RetrofitBucket +import com.nextcloud.talk.models.database.CapabilitiesUtil +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.ui.dialog.ContactsBottomDialog +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ConductorRemapping +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.database.user.UserUtils +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.SelectableAdapter +import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import okhttp3.ResponseBody +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.parceler.Parcels +import java.io.IOException +import java.util.ArrayList +import java.util.Collections +import java.util.HashMap +import java.util.HashSet +import java.util.Locale +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ContactsController(args: Bundle) : + NewBaseController(R.layout.controller_contacts_rv), + SearchView.OnQueryTextListener, + FlexibleAdapter.OnItemClickListener { + private val binding: ControllerContactsRvBinding by viewBinding(ControllerContactsRvBinding::bind) + + @Inject + lateinit var userUtils: UserUtils + + @Inject + lateinit var eventBus: EventBus + + @Inject + lateinit var ncApi: NcApi + + private var credentials: String? = null + private var currentUser: UserEntity? = null + private var contactsQueryDisposable: Disposable? = null + private var cacheQueryDisposable: Disposable? = null + private var adapter: FlexibleAdapter<*>? = null + private var contactItems: MutableList>? = null + private var layoutManager: SmoothScrollLinearLayoutManager? = null + private var searchItem: MenuItem? = null + private var searchView: SearchView? = null + private var isNewConversationView = false + private var isPublicCall = false + private var userHeaderItems: HashMap = HashMap() + private var alreadyFetching = false + private var doneMenuItem: MenuItem? = null + private var selectedUserIds: MutableSet = HashSet() + private var selectedGroupIds: MutableSet = HashSet() + private var selectedCircleIds: MutableSet = HashSet() + private var selectedEmails: MutableSet = HashSet() + private var existingParticipants: List? = null + private var isAddingParticipantsView = false + private var conversationToken: String? = null + private var contactsBottomDialog: ContactsBottomDialog? = null + + init { + setHasOptionsMenu(true) + sharedApplication!!.componentApplication.inject(this) + + if (args.containsKey(BundleKeys.KEY_NEW_CONVERSATION)) { + isNewConversationView = true + existingParticipants = ArrayList() + } else if (args.containsKey(BundleKeys.KEY_ADD_PARTICIPANTS)) { + isAddingParticipantsView = true + conversationToken = args.getString(BundleKeys.KEY_TOKEN) + existingParticipants = ArrayList() + if (args.containsKey(BundleKeys.KEY_EXISTING_PARTICIPANTS)) { + existingParticipants = args.getStringArrayList(BundleKeys.KEY_EXISTING_PARTICIPANTS) + } + } + selectedUserIds = HashSet() + selectedGroupIds = HashSet() + selectedEmails = HashSet() + selectedCircleIds = HashSet() + } + + override fun onAttach(view: View) { + super.onAttach(view) + eventBus.register(this) + if (isNewConversationView) { + toggleNewCallHeaderVisibility(!isPublicCall) + } + if (isAddingParticipantsView) { + binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.visibility = View.GONE + binding.conversationPrivacyToggle.callHeaderLayout.visibility = View.GONE + } else { + binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.setOnClickListener { + joinConversationViaLink() + } + binding.conversationPrivacyToggle.callHeaderLayout.setOnClickListener { + toggleCallHeader() + } + } + } + + override fun onViewBound(view: View) { + super.onViewBound(view) + currentUser = userUtils.currentUser + if (currentUser != null) { + credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token) + } + if (adapter == null) { + contactItems = ArrayList>() + adapter = FlexibleAdapter(contactItems, activity, false) + if (currentUser != null) { + fetchData() + } + } + setupAdapter() + prepareViews() + } + + private fun setupAdapter() { + adapter?.setNotifyChangeOfUnfilteredItems(true)?.mode = SelectableAdapter.Mode.MULTI + adapter?.setStickyHeaderElevation(HEADER_ELEVATION) + ?.setUnlinkAllItemsOnRemoveHeaders(true) + ?.setDisplayHeadersAtStartUp(true) + ?.setStickyHeaders(true) + adapter?.addListener(this) + } + + private fun selectionDone() { + if (!isAddingParticipantsView) { + if (!isPublicCall && selectedCircleIds.size + selectedGroupIds.size + selectedUserIds.size == 1) { + val userId: String + var sourceType: String? = null + var roomType = "1" + when { + selectedGroupIds.size == 1 -> { + roomType = "2" + userId = selectedGroupIds.iterator().next() + } + selectedCircleIds.size == 1 -> { + roomType = "2" + sourceType = "circles" + userId = selectedCircleIds.iterator().next() + } + else -> { + userId = selectedUserIds.iterator().next() + } + } + createRoom(roomType, sourceType, userId) + } else { + val bundle = Bundle() + val roomType: Conversation.ConversationType = if (isPublicCall) { + Conversation.ConversationType.ROOM_PUBLIC_CALL + } else { + Conversation.ConversationType.ROOM_GROUP_CALL + } + val userIdsArray = ArrayList(selectedUserIds) + val groupIdsArray = ArrayList(selectedGroupIds) + val emailsArray = ArrayList(selectedEmails) + val circleIdsArray = ArrayList(selectedCircleIds) + bundle.putParcelable(BundleKeys.KEY_CONVERSATION_TYPE, Parcels.wrap(roomType)) + bundle.putStringArrayList(BundleKeys.KEY_INVITED_PARTICIPANTS, userIdsArray) + bundle.putStringArrayList(BundleKeys.KEY_INVITED_GROUP, groupIdsArray) + bundle.putStringArrayList(BundleKeys.KEY_INVITED_EMAIL, emailsArray) + bundle.putStringArrayList(BundleKeys.KEY_INVITED_CIRCLE, circleIdsArray) + bundle.putSerializable(BundleKeys.KEY_OPERATION_CODE, ConversationOperationEnum.OPS_CODE_INVITE_USERS) + prepareAndShowBottomSheetWithBundle(bundle) + } + } else { + addParticipantsToConversation() + } + } + + private fun createRoom(roomType: String, sourceType: String?, userId: String) { + val apiVersion: Int = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, 1)) + val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + apiVersion, + currentUser!!.baseUrl, + roomType, + sourceType, + userId, + null + ) + ncApi.createRoom( + credentials, + retrofitBucket.getUrl(), retrofitBucket.getQueryMap() + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + override fun onNext(roomOverall: RoomOverall) { + val bundle = Bundle() + bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, currentUser) + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.getOcs().getData().getToken()) + bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.getOcs().getData().getRoomId()) + + // FIXME once APIv2 or later is used only, the createRoom already returns all the data + ncApi.getRoom( + credentials, + ApiUtils.getUrlForRoom( + apiVersion, currentUser!!.baseUrl, + roomOverall.getOcs().getData().getToken() + ) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + override fun onNext(roomOverall: RoomOverall) { + bundle.putParcelable( + BundleKeys.KEY_ACTIVE_CONVERSATION, + Parcels.wrap(roomOverall.getOcs().getData()) + ) + ConductorRemapping.remapChatController( + router, currentUser!!.id, + roomOverall.getOcs().getData().getToken(), bundle, true + ) + } + + override fun onError(e: Throwable) { + // unused atm + } + + override fun onComplete() { + // unused atm + } + }) + } + + override fun onError(e: Throwable) { + // unused atm + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun addParticipantsToConversation() { + val userIdsArray: Array = selectedUserIds.toTypedArray() + val groupIdsArray: Array = selectedGroupIds.toTypedArray() + val emailsArray: Array = selectedEmails.toTypedArray() + val circleIdsArray: Array = selectedCircleIds.toTypedArray() + val data = Data.Builder() + data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, currentUser!!.id) + data.putString(BundleKeys.KEY_TOKEN, conversationToken) + data.putStringArray(BundleKeys.KEY_SELECTED_USERS, userIdsArray) + data.putStringArray(BundleKeys.KEY_SELECTED_GROUPS, groupIdsArray) + data.putStringArray(BundleKeys.KEY_SELECTED_EMAILS, emailsArray) + data.putStringArray(BundleKeys.KEY_SELECTED_CIRCLES, circleIdsArray) + val addParticipantsToConversationWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder( + AddParticipantsToConversation::class.java + ).setInputData(data.build()).build() + WorkManager.getInstance().enqueue(addParticipantsToConversationWorker) + router.popCurrentController() + } + + private fun initSearchView() { + if (activity != null) { + val searchManager: SearchManager? = activity?.getSystemService(Context.SEARCH_SERVICE) as SearchManager? + if (searchItem != null) { + searchView = MenuItemCompat.getActionView(searchItem) as SearchView + searchView!!.maxWidth = Int.MAX_VALUE + searchView!!.inputType = InputType.TYPE_TEXT_VARIATION_FILTER + var imeOptions: Int = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + appPreferences?.isKeyboardIncognito == true + ) { + imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + } + searchView!!.imeOptions = imeOptions + searchView!!.queryHint = resources!!.getString(R.string.nc_search) + if (searchManager != null) { + searchView!!.setSearchableInfo(searchManager.getSearchableInfo(activity?.componentName)) + } + searchView!!.setOnQueryTextListener(this) + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val itemId = item.itemId + if (itemId == R.id.home) { + return router.popCurrentController() + } else if (itemId == R.id.contacts_selection_done) { + selectionDone() + return true + } + return super.onOptionsItemSelected(item) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.menu_contacts, menu) + searchItem = menu.findItem(R.id.action_search) + doneMenuItem = menu.findItem(R.id.contacts_selection_done) + initSearchView() + } + + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + checkAndHandleDoneMenuItem() + if (adapter?.hasFilter() == true) { + searchItem!!.expandActionView() + searchView!!.setQuery(adapter!!.getFilter(String::class.java) as CharSequence, false) + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun fetchData() { + dispose(null) + alreadyFetching = true + userHeaderItems = HashMap() + val query = adapter!!.getFilter(String::class.java) as String? + val retrofitBucket: RetrofitBucket = + ApiUtils.getRetrofitBucketForContactsSearchFor14(currentUser!!.baseUrl, query) + val modifiedQueryMap: HashMap = HashMap(retrofitBucket.getQueryMap()) + modifiedQueryMap.put("limit", CONTACTS_BATCH_SIZE) + if (isAddingParticipantsView) { + modifiedQueryMap.put("itemId", conversationToken) + } + val shareTypesList: ArrayList = ArrayList() + // users + shareTypesList.add("0") + if (!isAddingParticipantsView) { + // groups + shareTypesList.add("1") + } else if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails")) { + // groups + shareTypesList.add("1") + // emails + shareTypesList.add("4") + } + if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "circles-support")) { + // circles + shareTypesList.add("7") + } + modifiedQueryMap.put("shareTypes[]", shareTypesList) + ncApi.getContactsWithSearchParam( + credentials, + retrofitBucket.getUrl(), shareTypesList, modifiedQueryMap + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .retry(RETRIES) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + contactsQueryDisposable = d + } + + override fun onNext(responseBody: ResponseBody) { + val newUserItemList = processAutocompleteUserList(responseBody) + + userHeaderItems = HashMap() + contactItems!!.addAll(newUserItemList) + + sortUserItems(newUserItemList) + + if (newUserItemList.size > 0) { + adapter?.updateDataSet(newUserItemList as List?) + } else { + adapter?.filterItems() + } + + try { + binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false + } catch (npe: NullPointerException) { + // view binding can be null + // since this is called asynchronously and UI might have been destroyed in the meantime + Log.i(TAG, "UI destroyed - view binding already gone") + } + } + + override fun onError(e: Throwable) { + try { + binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false + } catch (npe: NullPointerException) { + // view binding can be null + // since this is called asynchronously and UI might have been destroyed in the meantime + Log.i(TAG, "UI destroyed - view binding already gone") + } + dispose(contactsQueryDisposable) + } + + override fun onComplete() { + try { + binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false + } catch (npe: NullPointerException) { + // view binding can be null + // since this is called asynchronously and UI might have been destroyed in the meantime + Log.i(TAG, "UI destroyed - view binding already gone") + } + dispose(contactsQueryDisposable) + alreadyFetching = false + disengageProgressBar() + } + }) + } + + private fun processAutocompleteUserList(responseBody: ResponseBody) : MutableList> { + try { + val autocompleteOverall: AutocompleteOverall = LoganSquare.parse( + responseBody.string(), + AutocompleteOverall::class.java + ) + val autocompleteUsersList: ArrayList = ArrayList() + autocompleteUsersList.addAll(autocompleteOverall.ocs!!.data!!) + return processAutocompleteUserList(autocompleteUsersList) + } catch (ioe: IOException) { + Log.e(TAG, "Parsing response body failed while getting contacts", ioe) + } + + return ArrayList>() + } + + private fun processAutocompleteUserList( + autocompleteUsersList: ArrayList + ): MutableList> { + var participant: Participant + val actorTypeConverter = EnumActorTypeConverter() + val newUserItemList: MutableList> = ArrayList>() + for (autocompleteUser in autocompleteUsersList) { + if (autocompleteUser.id != currentUser!!.userId && + !existingParticipants!!.contains(autocompleteUser.id!!) + ) { + participant = createParticipant(autocompleteUser, actorTypeConverter) + val headerTitle = getHeaderTitle(participant) + var genericTextHeaderItem: GenericTextHeaderItem + if (!userHeaderItems.containsKey(headerTitle)) { + genericTextHeaderItem = GenericTextHeaderItem(headerTitle) + userHeaderItems.put(headerTitle, genericTextHeaderItem) + } + val newContactItem = ContactItem( + participant, + currentUser, + userHeaderItems[headerTitle] + ) + if (!contactItems!!.contains(newContactItem)) { + newUserItemList.add(newContactItem) + } + } + } + return newUserItemList + } + + private fun getHeaderTitle(participant: Participant): String { + return when { + participant.getActorType() == Participant.ActorType.GROUPS -> { + resources!!.getString(R.string.nc_groups) + } + participant.getActorType() == Participant.ActorType.CIRCLES -> { + resources!!.getString(R.string.nc_circles) + } + else -> { + participant.getDisplayName().substring(0, 1).toUpperCase(Locale.getDefault()) + } + } + } + + private fun createParticipant( + autocompleteUser: AutocompleteUser, + actorTypeConverter: EnumActorTypeConverter + ): Participant { + val participant = Participant() + participant.setActorId(autocompleteUser.id) + participant.setActorType(actorTypeConverter.getFromString(autocompleteUser.source)) + participant.setDisplayName(autocompleteUser.label) + participant.setSource(autocompleteUser.source) + + return participant + } + + private fun sortUserItems(newUserItemList: MutableList>) { + Collections.sort( + newUserItemList, + { o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> -> + val firstName: String = if (o1 is ContactItem) { + (o1 as ContactItem).model.getDisplayName() + } else { + (o1 as GenericTextHeaderItem).model + } + val secondName: String = if (o2 is ContactItem) { + (o2 as ContactItem).model.getDisplayName() + } else { + (o2 as GenericTextHeaderItem).model + } + if (o1 is ContactItem && o2 is ContactItem) { + val firstSource: String = (o1 as ContactItem).model.getSource() + val secondSource: String = (o2 as ContactItem).model.getSource() + if (firstSource == secondSource) { + return@sort firstName.compareTo(secondName, ignoreCase = true) + } + + // First users + if ("users" == firstSource) { + return@sort -1 + } else if ("users" == secondSource) { + return@sort 1 + } + + // Then groups + if ("groups" == firstSource) { + return@sort -1 + } else if ("groups" == secondSource) { + return@sort 1 + } + + // Then circles + if ("circles" == firstSource) { + return@sort -1 + } else if ("circles" == secondSource) { + return@sort 1 + } + + // Otherwise fall back to name sorting + return@sort firstName.compareTo(secondName, ignoreCase = true) + } + firstName.compareTo(secondName, ignoreCase = true) + } + ) + + Collections.sort( + contactItems + ) { o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> -> + val firstName: String = if (o1 is ContactItem) { + (o1 as ContactItem).model.getDisplayName() + } else { + (o1 as GenericTextHeaderItem).model + } + val secondName: String = if (o2 is ContactItem) { + (o2 as ContactItem).model.getDisplayName() + } else { + (o2 as GenericTextHeaderItem).model + } + if (o1 is ContactItem && o2 is ContactItem) { + if ("groups" == (o1 as ContactItem).model.getSource() && + "groups" == (o2 as ContactItem).model.getSource() + ) { + return@sort firstName.compareTo(secondName, ignoreCase = true) + } else if ("groups" == (o1 as ContactItem).model.getSource()) { + return@sort -1 + } else if ("groups" == (o2 as ContactItem).model.getSource()) { + return@sort 1 + } + } + firstName.compareTo(secondName, ignoreCase = true) + } + } + + private fun prepareViews() { + layoutManager = SmoothScrollLinearLayoutManager(activity) + binding.controllerGenericRv.recyclerView.layoutManager = layoutManager + binding.controllerGenericRv.recyclerView.setHasFixedSize(true) + binding.controllerGenericRv.recyclerView.adapter = adapter + binding.controllerGenericRv.swipeRefreshLayout.setOnRefreshListener { fetchData() } + binding.controllerGenericRv.swipeRefreshLayout.setColorSchemeResources(R.color.colorPrimary) + binding.controllerGenericRv.swipeRefreshLayout + .setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background) + binding.joinConversationViaLink.joinConversationViaLinkImageView + .background + .setColorFilter( + ResourcesCompat.getColor(resources!!, R.color.colorBackgroundDarker, null), + PorterDuff.Mode.SRC_IN + ) + binding.conversationPrivacyToggle.publicCallLink + .background + .setColorFilter( + ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null), + PorterDuff.Mode.SRC_IN + ) + disengageProgressBar() + } + + private fun disengageProgressBar() { + if (!alreadyFetching) { + binding.loadingContent.visibility = View.GONE + binding.controllerGenericRv.root.visibility = View.VISIBLE + if (isNewConversationView) { + binding.conversationPrivacyToggle.callHeaderLayout.visibility = View.VISIBLE + binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.visibility = View.VISIBLE + } + } + } + + private fun dispose(disposable: Disposable?) { + if (disposable != null && !disposable.isDisposed) { + disposable.dispose() + } else if (disposable == null) { + if (contactsQueryDisposable != null && !contactsQueryDisposable!!.isDisposed) { + contactsQueryDisposable!!.dispose() + contactsQueryDisposable = null + } + if (cacheQueryDisposable != null && !cacheQueryDisposable!!.isDisposed) { + cacheQueryDisposable!!.dispose() + cacheQueryDisposable = null + } + } + } + + override fun onSaveViewState(view: View, outState: Bundle) { + adapter?.onSaveInstanceState(outState) + super.onSaveViewState(view, outState) + } + + override fun onRestoreViewState(view: View, savedViewState: Bundle) { + super.onRestoreViewState(view, savedViewState) + if (adapter != null) { + adapter?.onRestoreInstanceState(savedViewState) + } + } + + override fun onDestroy() { + super.onDestroy() + dispose(null) + } + + override fun onQueryTextChange(newText: String): Boolean { + if (newText != "" && adapter?.hasNewFilter(newText) == true) { + adapter?.setFilter(newText) + fetchData() + } else if (newText == "") { + adapter?.setFilter("") + adapter?.updateDataSet(contactItems as List?) + } + + try { + binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = !adapter!!.hasFilter() + } catch (npe: NullPointerException) { + // view binding can be null + // since this is called asynchronously and UI might have been destroyed in the meantime + Log.i(TAG, "UI destroyed - view binding already gone") + } + + return true + } + + override fun onQueryTextSubmit(query: String): Boolean { + return onQueryTextChange(query) + } + + private fun checkAndHandleDoneMenuItem() { + if (adapter != null && doneMenuItem != null) { + doneMenuItem!!.isVisible = + selectedCircleIds.size + selectedEmails.size + selectedGroupIds.size + selectedUserIds.size > 0 || + isPublicCall + } else if (doneMenuItem != null) { + doneMenuItem!!.isVisible = false + } + } + + override val title: String + get() = when { + isAddingParticipantsView -> { + resources!!.getString(R.string.nc_add_participants) + } + isNewConversationView -> { + resources!!.getString(R.string.nc_select_participants) + } + else -> { + resources!!.getString(R.string.nc_app_product_name) + } + } + + private fun prepareAndShowBottomSheetWithBundle(bundle: Bundle) { + // 11: create conversation-enter name for new conversation + // 10: get&join room when enter link + contactsBottomDialog = ContactsBottomDialog(activity!!, bundle) + contactsBottomDialog?.show() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(openConversationEvent: OpenConversationEvent) { + ConductorRemapping.remapChatController( + router, currentUser!!.id, + openConversationEvent.conversation!!.getToken(), + openConversationEvent.bundle!!, true + ) + contactsBottomDialog?.dismiss() + } + + override fun onDetach(view: View) { + super.onDetach(view) + eventBus.unregister(this) + } + + override fun onItemClick(view: View, position: Int): Boolean { + if (adapter?.getItem(position) is ContactItem) { + if (!isNewConversationView && !isAddingParticipantsView) { + createRoom(adapter?.getItem(position) as ContactItem) + } else { + val participant: Participant = (adapter?.getItem(position) as ContactItem).model + updateSelection((adapter?.getItem(position) as ContactItem)) + } + } + return true + } + + private fun updateSelection(contactItem: ContactItem) { + contactItem.model.isSelected = !contactItem.model.isSelected + updateSelectionLists(contactItem.model) + if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "last-room-activity") && + !CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails") && + isValidGroupSelection(contactItem, contactItem.model, adapter) + ) { + val currentItems: List = adapter?.currentItems as List + var internalParticipant: Participant + for (i in currentItems.indices) { + internalParticipant = currentItems[i].model + if (internalParticipant.getActorId() == contactItem.model.getActorId() && + internalParticipant.getActorType() == Participant.ActorType.GROUPS && + internalParticipant.isSelected + ) { + internalParticipant.isSelected = false + selectedGroupIds.remove(internalParticipant.getActorId()) + } + } + } + adapter?.notifyDataSetChanged() + checkAndHandleDoneMenuItem() + } + + private fun createRoom(contactItem: ContactItem) { + var roomType = "1" + if ("groups" == contactItem.model.getSource()) { + roomType = "2" + } + val apiVersion: Int = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, 1)) + val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + apiVersion, + currentUser!!.baseUrl, + roomType, + null, + contactItem.model.getActorId(), + null + ) + ncApi.createRoom( + credentials, + retrofitBucket.getUrl(), retrofitBucket.getQueryMap() + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + override fun onNext(roomOverall: RoomOverall) { + if (activity != null) { + val bundle = Bundle() + bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, currentUser) + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.getOcs().getData().getToken()) + bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.getOcs().getData().getRoomId()) + bundle.putParcelable( + BundleKeys.KEY_ACTIVE_CONVERSATION, + Parcels.wrap(roomOverall.getOcs().getData()) + ) + ConductorRemapping.remapChatController( + router, + currentUser!!.id, + roomOverall.getOcs().getData().getToken(), + bundle, + true + ) + } + } + + override fun onError(e: Throwable) { + // unused atm + } + override fun onComplete() { + // unused atm + } + }) + } + + private fun updateSelectionLists(participant: Participant) { + if ("groups" == participant.getSource()) { + if (participant.isSelected) { + selectedGroupIds.add(participant.getActorId()) + } else { + selectedGroupIds.remove(participant.getActorId()) + } + } else if ("emails" == participant.getSource()) { + if (participant.isSelected) { + selectedEmails.add(participant.getActorId()) + } else { + selectedEmails.remove(participant.getActorId()) + } + } else if ("circles" == participant.getSource()) { + if (participant.isSelected) { + selectedCircleIds.add(participant.getActorId()) + } else { + selectedCircleIds.remove(participant.getActorId()) + } + } else { + if (participant.isSelected) { + selectedUserIds.add(participant.getActorId()) + } else { + selectedUserIds.remove(participant.getActorId()) + } + } + } + + private fun isValidGroupSelection( + contactItem: ContactItem, + participant: Participant, + adapter: FlexibleAdapter<*>? + ): Boolean { + return "groups" == contactItem.model.getSource() && participant.isSelected && adapter?.selectedItemCount!! > 1 + } + + private fun joinConversationViaLink() { + val bundle = Bundle() + bundle.putSerializable(BundleKeys.KEY_OPERATION_CODE, ConversationOperationEnum.OPS_CODE_GET_AND_JOIN_ROOM) + prepareAndShowBottomSheetWithBundle(bundle) + } + + private fun toggleCallHeader() { + toggleNewCallHeaderVisibility(isPublicCall) + isPublicCall = !isPublicCall + + if (isPublicCall) { + binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.visibility = View.GONE + updateGroupParticipantSelection() + } else { + binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.visibility = View.VISIBLE + } + + enableContactForNonPublicCall() + checkAndHandleDoneMenuItem() + adapter?.notifyDataSetChanged() + } + + private fun updateGroupParticipantSelection() { + val currentItems: List> = adapter?.currentItems as + List> + var internalParticipant: Participant + for (i in currentItems.indices) { + if (currentItems[i] is ContactItem) { + internalParticipant = (currentItems[i] as ContactItem).model + if (internalParticipant.getActorType() == Participant.ActorType.GROUPS && + internalParticipant.isSelected + ) { + internalParticipant.isSelected = false + selectedGroupIds.remove(internalParticipant.getActorId()) + } + } + } + } + + private fun enableContactForNonPublicCall() { + for (i in 0 until adapter!!.itemCount) { + if (adapter?.getItem(i) is ContactItem) { + val contactItem: ContactItem = adapter?.getItem(i) as ContactItem + if ("groups" == contactItem.model.getSource()) { + contactItem.isEnabled = !isPublicCall + } + } + } + } + + private fun toggleNewCallHeaderVisibility(showInitialLayout: Boolean) { + try { + if (showInitialLayout) { + binding.conversationPrivacyToggle.initialRelativeLayout.visibility = View.VISIBLE + binding.conversationPrivacyToggle.secondaryRelativeLayout.visibility = View.GONE + } else { + binding.conversationPrivacyToggle.initialRelativeLayout.visibility = View.GONE + binding.conversationPrivacyToggle.secondaryRelativeLayout.visibility = View.VISIBLE + } + } catch (npe: NullPointerException) { + // view binding can be null + // since this is called asynchronously and UI might have been destroyed in the meantime + Log.i(TAG, "UI destroyed - view binding already gone") + } + } + + companion object { + const val TAG = "ContactsController" + const val RETRIES: Long = 3 + const val CONTACTS_BATCH_SIZE: Int = 50 + const val HEADER_ELEVATION: Int = 5 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt index 8a753bb28..19aa363e0 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt @@ -357,6 +357,7 @@ class ProfileController : NewBaseController(R.layout.controller_profile) { } } + @Suppress("Detekt.LongMethod") private fun createUserInfoDetails(userInfo: UserProfileData?): List { val result: MutableList = LinkedList() diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumActorTypeConverter.kt b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumActorTypeConverter.kt index 0a7ff90b7..45547fae6 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumActorTypeConverter.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumActorTypeConverter.kt @@ -32,7 +32,7 @@ import com.nextcloud.talk.models.json.participants.Participant.ActorType.GUESTS import com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS class EnumActorTypeConverter : StringBasedTypeConverter() { - override fun getFromString(string: String): Participant.ActorType { + override fun getFromString(string: String?): Participant.ActorType { return when (string) { "emails" -> EMAILS "groups" -> GROUPS diff --git a/app/src/main/res/layout/controller_contacts_rv.xml b/app/src/main/res/layout/controller_contacts_rv.xml index 1a86acb33..5975135ff 100644 --- a/app/src/main/res/layout/controller_contacts_rv.xml +++ b/app/src/main/res/layout/controller_contacts_rv.xml @@ -62,14 +62,17 @@