diff --git a/app/build.gradle b/app/build.gradle index 173ebf9d2..b6c0a4577 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -158,7 +158,6 @@ android { ext { androidxCameraVersion = "1.1.0" - butterknifeVersion = "10.2.3" coilKtVersion = "2.1.0" daggerVersion = "2.43.2" lifecycleVersion = '2.5.1' @@ -256,8 +255,6 @@ dependencies { implementation "org.parceler:parceler-api:$parcelerVersion" implementation 'net.orange-box.storebox:storebox-lib:1.4.0' - implementation "com.jakewharton:butterknife:${butterknifeVersion}" - kapt "com.jakewharton:butterknife-compiler:${butterknifeVersion}" implementation 'eu.davidea:flexible-adapter:5.1.0' implementation 'eu.davidea:flexible-adapter-ui:1.0.0' implementation fileTree(downloadWebRtc.libFile.path) diff --git a/app/lint.xml b/app/lint.xml index 48787f40d..9015fa6cb 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -32,7 +32,6 @@ - diff --git a/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.kt b/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.kt index eb3efc898..cc1fad687 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.kt @@ -38,7 +38,7 @@ import com.nextcloud.talk.R 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.base.BaseController import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ControllerAccountVerificationBinding @@ -73,7 +73,7 @@ import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class AccountVerificationController(args: Bundle? = null) : - NewBaseController( + BaseController( R.layout.controller_account_verification, args ) { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt index 12012ef3d..9c280953a 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt @@ -125,7 +125,7 @@ import com.nextcloud.talk.adapters.messages.VoiceMessageInterface import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.callbacks.MentionAutocompleteCallback -import com.nextcloud.talk.controllers.base.NewBaseController +import com.nextcloud.talk.controllers.base.BaseController import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ControllerChatBinding @@ -211,7 +211,7 @@ import kotlin.math.roundToInt @AutoInjector(NextcloudTalkApplication::class) class ChatController(args: Bundle) : - NewBaseController( + BaseController( R.layout.controller_chat, args ), diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.kt index 29799f224..ccac1d0b9 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.kt @@ -49,7 +49,7 @@ 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.base.BaseController import com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.data.user.model.User @@ -89,7 +89,7 @@ import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class ContactsController(args: Bundle) : - NewBaseController(R.layout.controller_contacts_rv), + BaseController(R.layout.controller_contacts_rv), SearchView.OnQueryTextListener, FlexibleAdapter.OnItemClickListener { private val binding: ControllerContactsRvBinding by viewBinding(ControllerContactsRvBinding::bind) diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt index 43602e232..5e44ff2a1 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt @@ -55,7 +55,7 @@ import com.nextcloud.talk.R import com.nextcloud.talk.adapters.items.ParticipantItem import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication -import com.nextcloud.talk.controllers.base.NewBaseController +import com.nextcloud.talk.controllers.base.BaseController import com.nextcloud.talk.controllers.bottomsheet.items.BasicListItemWithImage import com.nextcloud.talk.controllers.bottomsheet.items.listItemsWithImage import com.nextcloud.talk.controllers.util.viewBinding @@ -99,7 +99,7 @@ import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class ConversationInfoController(args: Bundle) : - NewBaseController( + BaseController( R.layout.controller_conversation_info, args ), diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java deleted file mode 100644 index 87bc446cb..000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java +++ /dev/null @@ -1,1386 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * @author Andy Scherzinger - * @author Marcel Hibbe - * Copyright (C) 2021 Andy Scherzinger (info@andy-scherzinger.de) - * Copyright (C) 2017-2020 Mario Danic (mario@lovelyhq.com) - * Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de) - * - * 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.animation.AnimatorInflater; -import android.annotation.SuppressLint; -import android.app.SearchManager; -import android.content.ClipData; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.text.InputType; -import android.text.TextUtils; -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.view.inputmethod.InputMethodManager; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.Toast; - -import com.bluelinelabs.conductor.RouterTransaction; -import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler; -import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler; -import com.facebook.common.executors.UiThreadImmediateExecutorService; -import com.facebook.common.references.CloseableReference; -import com.facebook.datasource.DataSource; -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.imagepipeline.core.ImagePipeline; -import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; -import com.facebook.imagepipeline.image.CloseableImage; -import com.facebook.imagepipeline.request.ImageRequest; -import com.google.android.material.button.MaterialButton; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.nextcloud.talk.R; -import com.nextcloud.talk.activities.MainActivity; -import com.nextcloud.talk.adapters.items.ConversationItem; -import com.nextcloud.talk.adapters.items.GenericTextHeaderItem; -import com.nextcloud.talk.adapters.items.LoadMoreResultsItem; -import com.nextcloud.talk.adapters.items.MessageResultItem; -import com.nextcloud.talk.adapters.items.MessagesTextHeaderItem; -import com.nextcloud.talk.api.NcApi; -import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.controllers.base.BaseController; -import com.nextcloud.talk.data.user.model.User; -import com.nextcloud.talk.events.ConversationsListFetchDataEvent; -import com.nextcloud.talk.events.EventStatus; -import com.nextcloud.talk.interfaces.ConversationMenuInterface; -import com.nextcloud.talk.jobs.AccountRemovalWorker; -import com.nextcloud.talk.jobs.ContactAddressBookWorker; -import com.nextcloud.talk.jobs.DeleteConversationWorker; -import com.nextcloud.talk.jobs.UploadAndShareFilesWorker; -import com.nextcloud.talk.messagesearch.MessageSearchHelper; -import com.nextcloud.talk.models.domain.SearchMessageEntry; -import com.nextcloud.talk.models.json.conversations.Conversation; -import com.nextcloud.talk.models.json.status.Status; -import com.nextcloud.talk.models.json.statuses.StatusesOverall; -import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository; -import com.nextcloud.talk.ui.dialog.ChooseAccountDialogFragment; -import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog; -import com.nextcloud.talk.ui.theme.ViewThemeUtils; -import com.nextcloud.talk.users.UserManager; -import com.nextcloud.talk.utils.ApiUtils; -import com.nextcloud.talk.utils.AttendeePermissionsUtil; -import com.nextcloud.talk.utils.ClosedInterfaceImpl; -import com.nextcloud.talk.utils.ConductorRemapping; -import com.nextcloud.talk.utils.DisplayUtils; -import com.nextcloud.talk.utils.UriUtils; -import com.nextcloud.talk.utils.bundle.BundleKeys; -import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew; -import com.nextcloud.talk.utils.preferences.AppPreferences; -import com.nextcloud.talk.utils.rx.SearchViewObservable; -import com.nextcloud.ui.popupbubble.PopupBubble; - -import org.apache.commons.lang3.builder.CompareToBuilder; -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.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.SearchView; -import androidx.core.content.res.ResourcesCompat; -import androidx.core.graphics.drawable.RoundedBitmapDrawable; -import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; -import androidx.core.view.MenuItemCompat; -import androidx.fragment.app.DialogFragment; -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 eu.davidea.flexibleadapter.FlexibleAdapter; -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager; -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; -import eu.davidea.flexibleadapter.items.IHeader; -import io.reactivex.Observable; -import io.reactivex.Observer; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import retrofit2.HttpException; - -import static com.nextcloud.talk.utils.Mimetype.TEXT_PLAIN; - -@AutoInjector(NextcloudTalkApplication.class) -public class ConversationsListController extends BaseController implements FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener, ConversationMenuInterface { - - public static final String TAG = "ConvListController"; - public static final int UNREAD_BUBBLE_DELAY = 2500; - private static final String KEY_SEARCH_QUERY = "ContactsController.searchQuery"; - - public static final int SEARCH_DEBOUNCE_INTERVAL_MS = 300; - public static final int SEARCH_MIN_CHARS = 2; - - private final Bundle bundle; - @Inject - UserManager userManager; - - @Inject - EventBus eventBus; - - @Inject - NcApi ncApi; - - @Inject - Context context; - - @Inject - AppPreferences appPreferences; - - @Inject - UnifiedSearchRepository unifiedSearchRepository; - - @Inject - ViewThemeUtils viewThemeUtils; - - @BindView(R.id.recycler_view) - RecyclerView recyclerView; - - @BindView(R.id.swipeRefreshLayoutView) - SwipeRefreshLayout swipeRefreshLayout; - - @BindView(R.id.loading_content) - LinearLayout loadingContent; - - @BindView(R.id.emptyLayout) - RelativeLayout emptyLayoutView; - - @BindView(R.id.floatingActionButton) - FloatingActionButton floatingActionButton; - - @BindView(R.id.newMentionPopupBubble) - PopupBubble newMentionPopupBubble; - - private User currentUser; - private Disposable roomsQueryDisposable; - private Disposable openConversationsQueryDisposable; - private FlexibleAdapter adapter; - private List conversationItems = new ArrayList<>(); - private List conversationItemsWithHeader = new ArrayList<>(); - private final List searchableConversationItems = new ArrayList<>(); - - private MenuItem searchItem; - private SearchView searchView; - private String searchQuery; - - private String credentials; - - private boolean adapterWasNull = true; - - private boolean isRefreshing; - - private Bundle conversationMenuBundle = null; - - private boolean showShareToScreen = false; - - private ArrayList filesToShare; - private Conversation selectedConversation; - - private String textToPaste = ""; - private String selectedMessageId = null; - - private boolean forwardMessage; - - private int nextUnreadConversationScrollPosition = 0; - - private SmoothScrollLinearLayoutManager layoutManager; - - private HashMap callHeaderItems = new HashMap<>(); - - private ConversationsListBottomDialog conversationsListBottomDialog; - - private HashMap userStatuses = new HashMap<>(); - - private MessageSearchHelper searchHelper; - private Disposable searchViewDisposable; - - public ConversationsListController(Bundle bundle) { - super(); - setHasOptionsMenu(true); - forwardMessage = bundle.getBoolean(BundleKeys.INSTANCE.getKEY_FORWARD_MSG_FLAG()); - this.bundle = bundle; - } - - @Override - protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { - return inflater.inflate(R.layout.controller_conversations_rv, container, false); - } - - @Override - protected void onViewBound(@NonNull View view) { - super.onViewBound(view); - NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); - - if (getActionBar() != null) { - getActionBar().show(); - } - - if (adapter == null) { - adapter = new FlexibleAdapter<>(conversationItems, getActivity(), true); - } else { - loadingContent.setVisibility(View.GONE); - } - - adapter.addListener(this); - prepareViews(); - } - - private void loadUserAvatar(MaterialButton button) { - if (getActivity() != null) { - ImageRequest imageRequest = DisplayUtils.getImageRequestForUrl( - ApiUtils.getUrlForAvatar( - currentUser.getBaseUrl(), - currentUser.getUserId(), - true), - currentUser); - - ImagePipeline imagePipeline = Fresco.getImagePipeline(); - DataSource> dataSource = imagePipeline.fetchDecodedImage(imageRequest, null); - dataSource.subscribe(new BaseBitmapDataSubscriber() { - @Override - protected void onNewResultImpl(Bitmap bitmap) { - if (bitmap != null && getResources() != null) { - RoundedBitmapDrawable roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(getResources(), bitmap); - roundedBitmapDrawable.setCircular(true); - roundedBitmapDrawable.setAntiAlias(true); - button.setIcon(roundedBitmapDrawable); - } - } - - @Override - protected void onFailureImpl(DataSource> dataSource) { - if (getResources() != null) { - button.setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_user, null)); - } - } - }, UiThreadImmediateExecutorService.getInstance()); - } - } - - @Override - protected void onAttach(@NonNull View view) { - Log.d(TAG, "onAttach: Controller: " + System.identityHashCode(this) + - " Activity: " + System.identityHashCode(getActivity())); - super.onAttach(view); - - new ClosedInterfaceImpl().setUpPushTokenRegistration(); - - if (!eventBus.isRegistered(this)) { - eventBus.register(this); - } - currentUser = userManager.getCurrentUser().blockingGet(); - - if (currentUser != null) { - if (CapabilitiesUtilNew.isServerEOL(currentUser)) { - showServerEOLDialog(); - return; - } - - if (CapabilitiesUtilNew.isUnifiedSearchAvailable(currentUser)) { - searchHelper = new MessageSearchHelper(unifiedSearchRepository); - } - - credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()); - if (getActivity() != null && getActivity() instanceof MainActivity) { - loadUserAvatar(((MainActivity) getActivity()).binding.switchAccountButton); - viewThemeUtils.colorMaterialTextButton(((MainActivity) getActivity()).binding.switchAccountButton); - } - fetchData(); - } - } - - @Override - protected void onDetach(@NonNull View view) { - Log.d(TAG, "onDetach: Controller: " + System.identityHashCode(this) + - " Activity: " + System.identityHashCode(getActivity())); - super.onDetach(view); - eventBus.unregister(this); - } - - private void initSearchView() { - if (getActivity() != null) { - SearchManager searchManager = (SearchManager) getActivity().getSystemService(Context.SEARCH_SERVICE); - if (searchItem != null) { - searchView = (SearchView) MenuItemCompat.getActionView(searchItem); - viewThemeUtils.themeSearchView(searchView); - 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(getSearchHint()); - if (searchManager != null) { - searchView.setSearchableInfo(searchManager.getSearchableInfo(getActivity().getComponentName())); - } - searchViewDisposable = SearchViewObservable.observeSearchView(searchView) - .debounce(query -> { - if (TextUtils.isEmpty(query)) { - return Observable.empty(); - } else { - return Observable.timer(SEARCH_DEBOUNCE_INTERVAL_MS, TimeUnit.MILLISECONDS); - } - }) - .distinctUntilChanged() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::onQueryTextChange); - } - } - } - - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.menu_conversation_plus_filter, menu); - searchItem = menu.findItem(R.id.action_search); - initSearchView(); - } - - @Override - public void onPrepareOptionsMenu(@NonNull Menu menu) { - super.onPrepareOptionsMenu(menu); - - searchView = (SearchView) MenuItemCompat.getActionView(searchItem); - - showShareToScreen = !showShareToScreen && hasActivityActionSendIntent(); - - if (showShareToScreen) { - hideSearchBar(); - getActionBar().setTitle(R.string.send_to_three_dots); - } else if (forwardMessage) { - hideSearchBar(); - getActionBar().setTitle(R.string.nc_forward_to_three_dots); - } else { - MainActivity activity = (MainActivity) getActivity(); - - searchItem.setVisible(conversationItems.size() > 0); - if (activity != null) { - if (adapter.hasFilter()) { - showSearchView(activity, searchView, searchItem); - searchView.setQuery(adapter.getFilter(String.class), false); - } - - activity.binding.searchText.setOnClickListener(v -> { - showSearchView(activity, searchView, searchItem); - viewThemeUtils.themeStatusBar(activity, searchView); - }); - } - - searchView.setOnCloseListener(() -> { - if (TextUtils.isEmpty(searchView.getQuery().toString())) { - searchView.onActionViewCollapsed(); - if (activity != null) { - viewThemeUtils.resetStatusBar(activity, searchView); - } - } else { - searchView.post(() -> searchView.setQuery(TAG, true)); - } - return true; - }); - - searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { - @Override - public boolean onMenuItemActionExpand(MenuItem item) { - adapter.setHeadersShown(true); - adapter.updateDataSet(searchableConversationItems, false); - adapter.showAllHeaders(); - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setEnabled(false); - } - return true; - } - - @Override - public boolean onMenuItemActionCollapse(MenuItem item) { - adapter.setHeadersShown(false); - adapter.updateDataSet(conversationItems, false); - adapter.hideAllHeaders(); - if (searchHelper != null) { - // cancel any pending searches - searchHelper.cancelSearch(); - swipeRefreshLayout.setRefreshing(false); - } - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setEnabled(true); - } - - searchView.onActionViewCollapsed(); - MainActivity activity = (MainActivity) getActivity(); - if (activity != null) { - activity.binding.appBar.setStateListAnimator(AnimatorInflater.loadStateListAnimator( - activity.binding.appBar.getContext(), - R.animator.appbar_elevation_off) - ); - activity.binding.toolbar.setVisibility(View.GONE); - activity.binding.searchToolbar.setVisibility(View.VISIBLE); - if (getResources() != null) { - viewThemeUtils.resetStatusBar(activity, activity.binding.searchToolbar); - } - } - SmoothScrollLinearLayoutManager layoutManager = - (SmoothScrollLinearLayoutManager) recyclerView.getLayoutManager(); - if (layoutManager != null) { - layoutManager.scrollToPositionWithOffset(0, 0); - } - return true; - } - }); - } - } - - private boolean hasActivityActionSendIntent() { - if (getActivity() != null) { - return Intent.ACTION_SEND.equals(getActivity().getIntent().getAction()) - || Intent.ACTION_SEND_MULTIPLE.equals(getActivity().getIntent().getAction()); - } - return false; - } - - @Override - protected void showSearchOrToolbar() { - if (TextUtils.isEmpty(searchQuery)) { - super.showSearchOrToolbar(); - } - } - - public void showSearchView(MainActivity activity, SearchView searchView, MenuItem searchItem) { - activity.binding.appBar.setStateListAnimator(AnimatorInflater.loadStateListAnimator( - activity.binding.appBar.getContext(), - R.animator.appbar_elevation_on)); - activity.binding.toolbar.setVisibility(View.VISIBLE); - activity.binding.searchToolbar.setVisibility(View.GONE); - searchItem.expandActionView(); - } - - @SuppressLint("LongLogTag") - public void fetchData() { - if (CapabilitiesUtilNew.isUserStatusAvailable(userManager.getCurrentUser().blockingGet())) { - fetchUserStatusesAndRooms(); - } else { - fetchRooms(); - } - } - - private void fetchUserStatusesAndRooms() { - ncApi.getUserStatuses(credentials, ApiUtils.getUrlForUserStatuses(currentUser.getBaseUrl())) - .subscribe(new Observer() { - @Override - public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) { - } - - @Override - public void onNext(@NonNull StatusesOverall statusesOverall) { - for (Status status : statusesOverall.getOcs().getData()) { - userStatuses.put(status.getUserId(), status); - } - fetchRooms(); - } - - @Override - public void onError(@io.reactivex.annotations.NonNull Throwable e) { - Log.e(TAG, "failed to fetch user statuses", e); - fetchRooms(); - } - - @Override - public void onComplete() { - } - }); - } - - private void fetchRooms() { - dispose(null); - - isRefreshing = true; - - conversationItems = new ArrayList<>(); - conversationItemsWithHeader = new ArrayList<>(); - - int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[]{ApiUtils.APIv4, ApiUtils.APIv3, 1}); - - long startNanoTime = System.nanoTime(); - Log.d(TAG, "fetchData - getRooms - calling: " + startNanoTime); - roomsQueryDisposable = ncApi.getRooms(credentials, ApiUtils.getUrlForRooms(apiVersion, - currentUser.getBaseUrl())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(roomsOverall -> { - Log.d(TAG, "fetchData - getRooms - got response: " + startNanoTime); - - // This is invoked asynchronously, when server returns a response the view might have been - // unbound in the meantime. Check if the view is still there. - // FIXME - does it make sense to update internal data structures even when view has been unbound? - if (getView() == null) { - Log.d(TAG, "fetchData - getRooms - view is not bound: " + startNanoTime); - return; - } - - if (adapterWasNull) { - adapterWasNull = false; - loadingContent.setVisibility(View.GONE); - } - - if (roomsOverall.getOcs().getData().size() > 0) { - if (emptyLayoutView.getVisibility() != View.GONE) { - emptyLayoutView.setVisibility(View.GONE); - } - - if (swipeRefreshLayout.getVisibility() != View.VISIBLE) { - swipeRefreshLayout.setVisibility(View.VISIBLE); - } - } else { - if (emptyLayoutView.getVisibility() != View.VISIBLE) { - emptyLayoutView.setVisibility(View.VISIBLE); - } - - if (swipeRefreshLayout.getVisibility() != View.GONE) { - swipeRefreshLayout.setVisibility(View.GONE); - } - } - - for (Conversation conversation : roomsOverall.getOcs().getData()) { - if (bundle.containsKey(BundleKeys.INSTANCE.getKEY_FORWARD_HIDE_SOURCE_ROOM()) && - conversation.getRoomId().equals(bundle.getString( - BundleKeys.INSTANCE.getKEY_FORWARD_HIDE_SOURCE_ROOM())) - ) { - continue; - } - - String headerTitle; - - headerTitle = getResources().getString(R.string.conversations); - - GenericTextHeaderItem genericTextHeaderItem; - if (!callHeaderItems.containsKey(headerTitle)) { - genericTextHeaderItem = new GenericTextHeaderItem(headerTitle, viewThemeUtils); - callHeaderItems.put(headerTitle, genericTextHeaderItem); - } - - if (getActivity() != null) { - ConversationItem conversationItem = new ConversationItem( - conversation, - currentUser, - getActivity(), - userStatuses.get(conversation.getName()), - viewThemeUtils); - conversationItems.add(conversationItem); - - ConversationItem conversationItemWithHeader = new ConversationItem( - conversation, - currentUser, - getActivity(), - callHeaderItems.get(headerTitle), - userStatuses.get(conversation.getName()), - viewThemeUtils); - conversationItemsWithHeader.add(conversationItemWithHeader); - } - } - - sortConversations(conversationItems); - sortConversations(conversationItemsWithHeader); - - adapter.updateDataSet(conversationItems, false); - - new Handler().postDelayed(this::checkToShowUnreadBubble, UNREAD_BUBBLE_DELAY); - - fetchOpenConversations(apiVersion); - - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - - }, throwable -> { - handleHttpExceptions(throwable); - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - dispose(roomsQueryDisposable); - }, () -> { - dispose(roomsQueryDisposable); - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - - isRefreshing = false; - }); - } - - private void sortConversations(List conversationItems) { - Collections.sort(conversationItems, (o1, o2) -> { - Conversation conversation1 = ((ConversationItem) o1).getModel(); - Conversation conversation2 = ((ConversationItem) o2).getModel(); - return new CompareToBuilder() - .append(conversation2.getFavorite(), conversation1.getFavorite()) - .append(conversation2.getLastActivity(), conversation1.getLastActivity()) - .toComparison(); - }); - } - - private void fetchOpenConversations(int apiVersion) { - searchableConversationItems.clear(); - searchableConversationItems.addAll(conversationItemsWithHeader); - - if (CapabilitiesUtilNew.hasSpreedFeatureCapability(currentUser, "listable-rooms")) { - List openConversationItems = new ArrayList<>(); - - openConversationsQueryDisposable = ncApi.getOpenConversations( - credentials, - ApiUtils.getUrlForOpenConversations(apiVersion, currentUser.getBaseUrl())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(roomsOverall -> { - - for (Conversation conversation : roomsOverall.getOcs().getData()) { - String headerTitle = getResources().getString(R.string.openConversations); - - GenericTextHeaderItem genericTextHeaderItem; - if (!callHeaderItems.containsKey(headerTitle)) { - genericTextHeaderItem = new GenericTextHeaderItem(headerTitle, viewThemeUtils); - callHeaderItems.put(headerTitle, genericTextHeaderItem); - } - - ConversationItem conversationItem = new ConversationItem( - conversation, - currentUser, - getActivity(), - callHeaderItems.get(headerTitle), - userStatuses.get(conversation.getName()), - viewThemeUtils); - - openConversationItems.add(conversationItem); - } - searchableConversationItems.addAll(openConversationItems); - - }, throwable -> { - Log.e(TAG, "fetchData - getRooms - ERROR", throwable); - handleHttpExceptions(throwable); - dispose(openConversationsQueryDisposable); - }, () -> { - dispose(openConversationsQueryDisposable); - }); - } else { - Log.d(TAG, "no open conversations fetched because of missing capability"); - } - } - - private void handleHttpExceptions(Throwable throwable) { - if (throwable instanceof HttpException) { - HttpException exception = (HttpException) throwable; - switch (exception.code()) { - case 401: - if (getParentController() != null && getParentController().getRouter() != null) { - Log.d(TAG, "Starting reauth webview via getParentController()"); - getParentController().getRouter().pushController((RouterTransaction.with - (new WebViewLoginController(currentUser.getBaseUrl(), true)) - .pushChangeHandler(new VerticalChangeHandler()) - .popChangeHandler(new VerticalChangeHandler()))); - } else { - Log.d(TAG, "Starting reauth webview via ConversationsListController"); - showUnauthorizedDialog(); - } - break; - default: - break; - } - } - } - - @SuppressLint("ClickableViewAccessibility") - private void prepareViews() { - layoutManager = new SmoothScrollLinearLayoutManager(Objects.requireNonNull(getActivity())); - recyclerView.setLayoutManager(layoutManager); - recyclerView.setHasFixedSize(true); - recyclerView.setAdapter(adapter); - recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrollStateChanged(@NotNull RecyclerView recyclerView, int newState) { - super.onScrollStateChanged(recyclerView, newState); - if (newState == RecyclerView.SCROLL_STATE_IDLE) { - checkToShowUnreadBubble(); - } - } - }); - - recyclerView.setOnTouchListener((v, event) -> { - if (isAttached() && (!isBeingDestroyed() || !isDestroyed())) { - InputMethodManager imm = - (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - imm.hideSoftInputFromWindow(v.getWindowToken(), 0); - } - } - return false; - }); - - swipeRefreshLayout.setOnRefreshListener(() -> fetchData()); - viewThemeUtils.themeSwipeRefreshLayout(swipeRefreshLayout); - - emptyLayoutView.setOnClickListener(v -> showNewConversationsScreen()); - floatingActionButton.setOnClickListener(v -> { - ContactAddressBookWorker.Companion.run(context); - showNewConversationsScreen(); - }); - - viewThemeUtils.themeFAB(floatingActionButton); - - if (getActivity() != null && getActivity() instanceof MainActivity) { - MainActivity activity = (MainActivity) getActivity(); - - activity.binding.switchAccountButton.setOnClickListener(v -> { - if (getResources() != null && getResources().getBoolean(R.bool.multiaccount_support)) { - DialogFragment newFragment = ChooseAccountDialogFragment.newInstance(); - newFragment.show(((MainActivity) getActivity()).getSupportFragmentManager(), - "ChooseAccountDialogFragment"); - } else { - getRouter().pushController((RouterTransaction.with(new SettingsController()) - .pushChangeHandler(new HorizontalChangeHandler()) - .popChangeHandler(new HorizontalChangeHandler()))); - } - }); - } - - newMentionPopupBubble.hide(); - newMentionPopupBubble.setPopupBubbleListener(new PopupBubble.PopupBubbleClickListener() { - @Override - public void bubbleClicked(Context context) { - recyclerView.smoothScrollToPosition(nextUnreadConversationScrollPosition); - } - }); - viewThemeUtils.colorMaterialButtonPrimaryFilled(newMentionPopupBubble); - } - - private void checkToShowUnreadBubble() { - try { - int lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition(); - for (AbstractFlexibleItem flexItem : conversationItems) { - Conversation conversationItem = ((ConversationItem) flexItem).getModel(); - int position = adapter.getGlobalPositionOf(flexItem); - if ((conversationItem.getUnreadMention() || - (conversationItem.getUnreadMessages() > 0 && - conversationItem.getType() == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL)) && - position > lastVisibleItem) { - nextUnreadConversationScrollPosition = position; - if (!newMentionPopupBubble.isShown()) { - newMentionPopupBubble.show(); - } - return; - } - } - nextUnreadConversationScrollPosition = 0; - newMentionPopupBubble.hide(); - } catch (NullPointerException e) { - Log.d(TAG, "A NPE was caught when trying to show the unread popup bubble. This might happen when the " + - "user already left the conversations-list screen so the popup bubble is not available anymore.", e); - } - } - - private void showNewConversationsScreen() { - Bundle bundle = new Bundle(); - bundle.putBoolean(BundleKeys.INSTANCE.getKEY_NEW_CONVERSATION(), true); - getRouter().pushController((RouterTransaction.with(new ContactsController(bundle)) - .pushChangeHandler(new HorizontalChangeHandler()) - .popChangeHandler(new HorizontalChangeHandler()))); - } - - private void dispose(@Nullable Disposable disposable) { - if (disposable != null && !disposable.isDisposed()) { - disposable.dispose(); - disposable = null; - } else if (disposable == null && - roomsQueryDisposable != null && !roomsQueryDisposable.isDisposed()) { - roomsQueryDisposable.dispose(); - roomsQueryDisposable = null; - } else if (disposable == null && - openConversationsQueryDisposable != null && !openConversationsQueryDisposable.isDisposed()) { - openConversationsQueryDisposable.dispose(); - openConversationsQueryDisposable = null; - } - } - - @Override - public void onSaveViewState(@NonNull View view, @NonNull Bundle outState) { - - if (searchView != null && !TextUtils.isEmpty(searchView.getQuery())) { - outState.putString(KEY_SEARCH_QUERY, searchView.getQuery().toString()); - } - - super.onSaveViewState(view, outState); - } - - @Override - public void onRestoreViewState(@NonNull View view, @NonNull Bundle savedViewState) { - super.onRestoreViewState(view, savedViewState); - if (savedViewState.containsKey(KEY_SEARCH_QUERY)) { - searchQuery = savedViewState.getString(KEY_SEARCH_QUERY, ""); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - dispose(null); - if (searchViewDisposable != null && !searchViewDisposable.isDisposed()) { - searchViewDisposable.dispose(); - } - } - - public void onQueryTextChange(final String newText) { - if (!TextUtils.isEmpty(searchQuery)) { - final String filter = searchQuery; - searchQuery = ""; - performFilterAndSearch(filter); - } else if (adapter.hasNewFilter(newText)) { - performFilterAndSearch(newText); - } - } - - private void performFilterAndSearch(String filter) { - if (filter.length() >= SEARCH_MIN_CHARS) { - clearMessageSearchResults(); - adapter.setFilter(filter); - adapter.filterItems(); - if (CapabilitiesUtilNew.isUnifiedSearchAvailable(currentUser)) { - startMessageSearch(filter); - } - } else { - resetSearchResults(); - } - } - - private void resetSearchResults() { - clearMessageSearchResults(); - adapter.setFilter(""); - adapter.filterItems(); - } - - private void clearMessageSearchResults() { - final IHeader firstHeader = adapter.getSectionHeader(0); - if (firstHeader != null && firstHeader.getItemViewType() == MessagesTextHeaderItem.VIEW_TYPE) { - adapter.removeSection(firstHeader); - } else { - adapter.removeItemsOfType(MessageResultItem.VIEW_TYPE); - } - adapter.removeItemsOfType(LoadMoreResultsItem.VIEW_TYPE); - } - - @SuppressLint("CheckResult") // handled by helper - private void startMessageSearch(final String search) { - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(true); - } - searchHelper - .startMessageSearch(search) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - this::onMessageSearchResult, - this::onMessageSearchError); - } - - @SuppressLint("CheckResult") // handled by helper - private void loadMoreMessages() { - swipeRefreshLayout.setRefreshing(true); - final Observable observable = searchHelper.loadMore(); - if (observable != null) { - observable - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - this::onMessageSearchResult, - this::onMessageSearchError); - } - } - - - @Override - protected String getTitle() { - return getResources().getString(R.string.nc_app_product_name); - } - - @Override - public boolean onItemClick(View view, int position) { - final AbstractFlexibleItem item = adapter.getItem(position); - if (item != null) { - final int viewType = item.getItemViewType(); - if (viewType == MessageResultItem.VIEW_TYPE) { - MessageResultItem messageItem = (MessageResultItem) item; - String conversationToken = messageItem.getMessageEntry().getConversationToken(); - selectedMessageId = messageItem.getMessageEntry().getMessageId(); - showConversationByToken(conversationToken); - } else if (viewType == LoadMoreResultsItem.VIEW_TYPE) { - loadMoreMessages(); - } else if (viewType == ConversationItem.VIEW_TYPE) { - showConversation(((ConversationItem) Objects.requireNonNull(item)).getModel()); - } - } - return true; - } - - private void showConversationByToken(String conversationToken) { - for (AbstractFlexibleItem absItem : conversationItems) { - ConversationItem conversationItem = ((ConversationItem) absItem); - if (conversationItem.getModel().getToken().equals(conversationToken)) { - final Conversation conversation = conversationItem.getModel(); - showConversation(conversation); - } - } - } - - private void showConversation(@Nullable final Conversation conversation) { - selectedConversation = conversation; - if (selectedConversation != null && getActivity() != null) { - boolean hasChatPermission = - new AttendeePermissionsUtil(selectedConversation.getPermissions()).hasChatPermission(currentUser); - - if (showShareToScreen) { - if (hasChatPermission && !isReadOnlyConversation(selectedConversation)) { - handleSharedData(); - showShareToScreen = false; - } else { - Toast.makeText(context, R.string.send_to_forbidden, Toast.LENGTH_LONG).show(); - } - } else if (forwardMessage) { - if (hasChatPermission && !isReadOnlyConversation(selectedConversation)) { - openConversation(bundle.getString(BundleKeys.INSTANCE.getKEY_FORWARD_MSG_TEXT())); - forwardMessage = false; - } else { - Toast.makeText(context, R.string.send_to_forbidden, Toast.LENGTH_LONG).show(); - } - } else { - openConversation(); - } - } - } - - private Boolean isReadOnlyConversation(Conversation conversation) { - return conversation.getConversationReadOnlyState() == - Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY; - } - - private void handleSharedData() { - collectDataFromIntent(); - if (!textToPaste.isEmpty()) { - openConversation(textToPaste); - } else if (filesToShare != null && !filesToShare.isEmpty()) { - showSendFilesConfirmDialog(); - } else { - Toast.makeText(context, context.getResources().getString(R.string.nc_common_error_sorry), Toast.LENGTH_LONG).show(); - } - } - - private void showSendFilesConfirmDialog() { - if (UploadAndShareFilesWorker.Companion.isStoragePermissionGranted(context)) { - StringBuilder fileNamesWithLineBreaks = new StringBuilder("\n"); - - for (String file : filesToShare) { - String filename = UriUtils.Companion.getFileName(Uri.parse(file), context); - fileNamesWithLineBreaks.append(filename).append("\n"); - } - - String confirmationQuestion; - if (filesToShare.size() == 1) { - confirmationQuestion = - String.format(getResources().getString(R.string.nc_upload_confirm_send_single), - selectedConversation.getDisplayName()); - } else { - confirmationQuestion = - String.format(getResources().getString(R.string.nc_upload_confirm_send_multiple), - selectedConversation.getDisplayName()); - } - - MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(floatingActionButton.getContext()) - .setIcon(viewThemeUtils.colorMaterialAlertDialogIcon(context, R.drawable.upload)) - .setTitle(confirmationQuestion) - .setMessage(fileNamesWithLineBreaks.toString()) - .setPositiveButton(R.string.nc_yes, (dialog, which) -> { - upload(); - openConversation(); - }) - .setNegativeButton(R.string.nc_no, (dialog, which) -> { - Log.d(TAG, "sharing files aborted, going back to share-to screen"); - showShareToScreen = true; - }); - - viewThemeUtils.colorMaterialAlertDialogBackground(floatingActionButton.getContext(), dialogBuilder); - - AlertDialog dialog = dialogBuilder.show(); - - viewThemeUtils.colorTextButtons( - dialog.getButton(AlertDialog.BUTTON_POSITIVE), - dialog.getButton(AlertDialog.BUTTON_NEGATIVE)); - } else { - UploadAndShareFilesWorker.Companion.requestStoragePermission(ConversationsListController.this); - } - } - - @Override - public void onItemLongClick(int position) { - if (showShareToScreen) { - Log.d(TAG, "sharing to multiple rooms not yet implemented. onItemLongClick is ignored."); - - } else { - Object clickedItem = adapter.getItem(position); - if (clickedItem != null) { - Conversation conversation = ((ConversationItem) clickedItem).getModel(); - conversationsListBottomDialog = new ConversationsListBottomDialog( - getActivity(), - this, - userManager.getCurrentUser().blockingGet(), - conversation); - conversationsListBottomDialog.show(); - } - } - } - - private void collectDataFromIntent() { - filesToShare = new ArrayList<>(); - if (getActivity() != null && getActivity().getIntent() != null) { - Intent intent = getActivity().getIntent(); - if (Intent.ACTION_SEND.equals(intent.getAction()) - || Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) { - try { - String mimeType = intent.getType(); - if (TEXT_PLAIN.equals(mimeType) && (intent.getStringExtra(Intent.EXTRA_TEXT) != null)) { - // Share from Google Chrome sets text/plain MIME type, but also provides a content:// URI - // with a *screenshot* of the current page in getClipData(). - // Here we assume that when sharing a web page the user would prefer to send the URL - // of the current page rather than a screenshot. - textToPaste = intent.getStringExtra(Intent.EXTRA_TEXT); - } else { - if (intent.getClipData() != null) { - for (int i = 0; i < intent.getClipData().getItemCount(); i++) { - ClipData.Item item = intent.getClipData().getItemAt(i); - if (item.getUri() != null) { - filesToShare.add(item.getUri().toString()); - } else if (item.getText() != null) { - textToPaste = item.getText().toString(); - break; - } else { - Log.w(TAG, "datatype not yet implemented for share-to"); - } - } - } else { - filesToShare.add(intent.getData().toString()); - } - } - if (filesToShare.isEmpty() && textToPaste.isEmpty()) { - Toast.makeText(context, context.getResources().getString(R.string.nc_common_error_sorry), - Toast.LENGTH_LONG).show(); - Log.e(TAG, "failed to get data from intent"); - } - } catch (Exception e) { - Toast.makeText(context, context.getResources().getString(R.string.nc_common_error_sorry), - Toast.LENGTH_LONG).show(); - Log.e(TAG, "Something went wrong when extracting data from intent"); - } - } - } - } - - private void upload() { - if (selectedConversation == null) { - Toast.makeText(context, context.getResources().getString(R.string.nc_common_error_sorry), - Toast.LENGTH_LONG).show(); - Log.e(TAG, "not able to upload any files because conversation was null."); - return; - } - - try { - String[] filesToShareArray = new String[filesToShare.size()]; - filesToShareArray = filesToShare.toArray(filesToShareArray); - - Data data = new Data.Builder() - .putStringArray(UploadAndShareFilesWorker.DEVICE_SOURCEFILES, filesToShareArray) - .putString( - UploadAndShareFilesWorker.NC_TARGETPATH, - CapabilitiesUtilNew.getAttachmentFolder(currentUser)) - .putString(UploadAndShareFilesWorker.ROOM_TOKEN, selectedConversation.getToken()) - .build(); - OneTimeWorkRequest uploadWorker = new OneTimeWorkRequest.Builder(UploadAndShareFilesWorker.class) - .setInputData(data) - .build(); - WorkManager.getInstance().enqueue(uploadWorker); - - Toast.makeText( - context, context.getResources().getString(R.string.nc_upload_in_progess), - Toast.LENGTH_LONG - ).show(); - - } catch (IllegalArgumentException e) { - Toast.makeText(context, context.getResources().getString(R.string.nc_upload_failed), Toast.LENGTH_LONG).show(); - Log.e(TAG, "Something went wrong when trying to upload file", e); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - if (requestCode == UploadAndShareFilesWorker.REQUEST_PERMISSION && - grantResults.length > 0 && - grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Log.d(TAG, "upload starting after permissions were granted"); - showSendFilesConfirmDialog(); - } else { - Toast.makeText(context, context.getString(R.string.read_storage_no_permission), Toast.LENGTH_LONG).show(); - } - } - - private void openConversation() { - openConversation(""); - } - - private void openConversation(String textToPaste) { - Bundle bundle = new Bundle(); - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY(), currentUser); - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_ACTIVE_CONVERSATION(), Parcels.wrap(selectedConversation)); - bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), selectedConversation.getToken()); - bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_ID(), selectedConversation.getRoomId()); - bundle.putString(BundleKeys.INSTANCE.getKEY_SHARED_TEXT(), textToPaste); - if (selectedMessageId != null) { - bundle.putString(BundleKeys.KEY_MESSAGE_ID, selectedMessageId); - selectedMessageId = null; - } - - ConductorRemapping.INSTANCE.remapChatController(getRouter(), currentUser.getId(), - selectedConversation.getToken(), bundle, false); - } - - @Subscribe(sticky = true, threadMode = ThreadMode.BACKGROUND) - public void onMessageEvent(EventStatus eventStatus) { - if (currentUser != null && eventStatus.getUserId() == currentUser.getId()) { - switch (eventStatus.getEventType()) { - case CONVERSATION_UPDATE: - if (eventStatus.isAllGood() && !isRefreshing) { - fetchData(); - } - break; - default: - break; - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onMessageEvent(ConversationsListFetchDataEvent conversationsListFetchDataEvent) { - fetchData(); - - new Handler().postDelayed(() -> { - if (conversationsListBottomDialog.isShowing()) { - conversationsListBottomDialog.dismiss(); - } - }, 2500); - } - - @Override - public void showDeleteConversationDialog(@NonNull Bundle bundle) { - conversationMenuBundle = bundle; - if (getActivity() != null && - conversationMenuBundle != null && - currentUser != null && - conversationMenuBundle.getLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID()) == currentUser.getId()) { - - Conversation conversation = - Parcels.unwrap(conversationMenuBundle.getParcelable(BundleKeys.INSTANCE.getKEY_ROOM())); - - if (conversation != null) { - MaterialAlertDialogBuilder dialogBuilder = - new MaterialAlertDialogBuilder(floatingActionButton.getContext()) - .setIcon(viewThemeUtils.colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp)) - .setTitle(R.string.nc_delete_call) - .setMessage(R.string.nc_delete_conversation_more) - .setPositiveButton(R.string.nc_delete, (dialog, which) -> { - Data.Builder data = new Data.Builder(); - data.putLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), - conversationMenuBundle.getLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID())); - data.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), conversation.getToken()); - conversationMenuBundle = null; - deleteConversation(data.build()); - }) - .setNegativeButton(R.string.nc_cancel, (dialog, which) -> { - conversationMenuBundle = null; - }); - - viewThemeUtils.colorMaterialAlertDialogBackground(floatingActionButton.getContext(), dialogBuilder); - - AlertDialog dialog = dialogBuilder.show(); - - viewThemeUtils.colorTextButtons( - dialog.getButton(AlertDialog.BUTTON_POSITIVE), - dialog.getButton(AlertDialog.BUTTON_NEGATIVE)); - } - } - } - - private void showUnauthorizedDialog() { - if (getActivity() != null) { - MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(floatingActionButton.getContext()) - .setIcon(viewThemeUtils.colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp)) - .setTitle(R.string.nc_dialog_invalid_password) - .setMessage(R.string.nc_dialog_reauth_or_delete) - .setCancelable(false) - .setPositiveButton(R.string.nc_delete, (dialog, which) -> { - boolean otherUserExists = userManager - .scheduleUserForDeletionWithId(currentUser.getId()) - .blockingGet(); - - OneTimeWorkRequest accountRemovalWork = new OneTimeWorkRequest.Builder(AccountRemovalWorker.class).build(); - WorkManager.getInstance().enqueue(accountRemovalWork); - - if (otherUserExists && getView() != null) { - onViewBound(getView()); - onAttach(getView()); - } else if (!otherUserExists) { - getRouter().setRoot(RouterTransaction.with( - new ServerSelectionController()) - .pushChangeHandler(new VerticalChangeHandler()) - .popChangeHandler(new VerticalChangeHandler())); - } - }) - .setNegativeButton(R.string.nc_settings_reauthorize, (dialog, which) -> { - getRouter().pushController(RouterTransaction.with( - new WebViewLoginController(currentUser.getBaseUrl(), true)) - .pushChangeHandler(new VerticalChangeHandler()) - .popChangeHandler(new VerticalChangeHandler())); - }); - - viewThemeUtils.colorMaterialAlertDialogBackground(floatingActionButton.getContext(), dialogBuilder); - - AlertDialog dialog = dialogBuilder.show(); - - viewThemeUtils.colorTextButtons( - dialog.getButton(AlertDialog.BUTTON_POSITIVE), - dialog.getButton(AlertDialog.BUTTON_NEGATIVE)); - } - } - - private void showServerEOLDialog() { - MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(floatingActionButton.getContext()) - .setIcon(viewThemeUtils.colorMaterialAlertDialogIcon(context, R.drawable.ic_warning_white)) - .setTitle(R.string.nc_settings_server_eol_title) - .setMessage(R.string.nc_settings_server_eol) - .setCancelable(false) - .setPositiveButton(R.string.nc_settings_remove_account, (dialog, which) -> { - boolean otherUserExists = userManager - .scheduleUserForDeletionWithId(currentUser.getId()) - .blockingGet(); - - OneTimeWorkRequest accountRemovalWork = new OneTimeWorkRequest.Builder(AccountRemovalWorker.class).build(); - WorkManager.getInstance().enqueue(accountRemovalWork); - - if (otherUserExists && getView() != null) { - onViewBound(getView()); - onAttach(getView()); - } else if (!otherUserExists) { - getRouter().setRoot(RouterTransaction.with( - new ServerSelectionController()) - .pushChangeHandler(new VerticalChangeHandler()) - .popChangeHandler(new VerticalChangeHandler())); - } - }) - .setNegativeButton(R.string.nc_cancel, (dialog, which) -> { - if (userManager.getUsers().blockingGet().size() > 0) { - getRouter().pushController(RouterTransaction.with(new SwitchAccountController())); - } else { - getActivity().finishAffinity(); - getActivity().finish(); - } - }); - - viewThemeUtils.colorMaterialAlertDialogBackground(floatingActionButton.getContext(), dialogBuilder); - - AlertDialog dialog = dialogBuilder.show(); - - viewThemeUtils.colorTextButtons( - dialog.getButton(AlertDialog.BUTTON_POSITIVE), - dialog.getButton(AlertDialog.BUTTON_NEGATIVE)); - } - - private void deleteConversation(Data data) { - OneTimeWorkRequest deleteConversationWorker = - new OneTimeWorkRequest.Builder(DeleteConversationWorker.class).setInputData(data).build(); - WorkManager.getInstance().enqueue(deleteConversationWorker); - } - - @Override - public AppBarLayoutType getAppBarLayoutType() { - return AppBarLayoutType.SEARCH_BAR; - } - - public void onMessageSearchResult(@NonNull MessageSearchHelper.MessageSearchResults results) { - if (searchView.getQuery().length() > 0) { - clearMessageSearchResults(); - final List entries = results.getMessages(); - if (entries.size() > 0) { - List adapterItems = new ArrayList<>(entries.size() + 1); - for (int i = 0; i < entries.size(); i++) { - final boolean showHeader = i == 0; - adapterItems.add(new MessageResultItem(context, currentUser, entries.get(i), showHeader, viewThemeUtils)); - } - if (results.getHasMore()) { - adapterItems.add(LoadMoreResultsItem.INSTANCE); - } - adapter.addItems(0, adapterItems); - recyclerView.scrollToPosition(0); - } - } - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - } - - public void onMessageSearchError(@NonNull Throwable throwable) { - handleHttpExceptions(throwable); - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - } -} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.kt new file mode 100644 index 000000000..854fadea6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.kt @@ -0,0 +1,1298 @@ +/* + * Nextcloud Talk application + * + * @author Álvaro Brey + * @author Andy Scherzinger + * @author Marcel Hibbe + * @author Mario Danic + * Copyright (C) 2022 Álvaro Brey + * Copyright (C) 2022 Andy Scherzinger (info@andy-scherzinger.de) + * Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de) + * Copyright (C) 2017-2020 Mario Danic (mario@lovelyhq.com) + * + * 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.animation.AnimatorInflater +import android.annotation.SuppressLint +import android.app.SearchManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.text.InputType +import android.text.TextUtils +import android.util.Log +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.SearchView +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory +import androidx.core.view.MenuItemCompat +import androidx.fragment.app.DialogFragment +import androidx.recyclerview.widget.RecyclerView +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import autodagger.AutoInjector +import com.bluelinelabs.conductor.RouterTransaction +import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler +import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler +import com.facebook.common.executors.UiThreadImmediateExecutorService +import com.facebook.common.references.CloseableReference +import com.facebook.datasource.DataSource +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber +import com.facebook.imagepipeline.image.CloseableImage +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.MainActivity +import com.nextcloud.talk.adapters.items.ConversationItem +import com.nextcloud.talk.adapters.items.GenericTextHeaderItem +import com.nextcloud.talk.adapters.items.LoadMoreResultsItem +import com.nextcloud.talk.adapters.items.MessageResultItem +import com.nextcloud.talk.adapters.items.MessagesTextHeaderItem +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.BaseController +import com.nextcloud.talk.controllers.util.viewBinding +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ControllerConversationsRvBinding +import com.nextcloud.talk.events.ConversationsListFetchDataEvent +import com.nextcloud.talk.events.EventStatus +import com.nextcloud.talk.interfaces.ConversationMenuInterface +import com.nextcloud.talk.jobs.AccountRemovalWorker +import com.nextcloud.talk.jobs.ContactAddressBookWorker.Companion.run +import com.nextcloud.talk.jobs.DeleteConversationWorker +import com.nextcloud.talk.jobs.UploadAndShareFilesWorker +import com.nextcloud.talk.jobs.UploadAndShareFilesWorker.Companion.isStoragePermissionGranted +import com.nextcloud.talk.jobs.UploadAndShareFilesWorker.Companion.requestStoragePermission +import com.nextcloud.talk.messagesearch.MessageSearchHelper +import com.nextcloud.talk.messagesearch.MessageSearchHelper.MessageSearchResults +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.RoomsOverall +import com.nextcloud.talk.models.json.status.Status +import com.nextcloud.talk.models.json.statuses.StatusesOverall +import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository +import com.nextcloud.talk.ui.dialog.ChooseAccountDialogFragment +import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.AttendeePermissionsUtil +import com.nextcloud.talk.utils.ClosedInterfaceImpl +import com.nextcloud.talk.utils.ConductorRemapping.remapChatController +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.Mimetype +import com.nextcloud.talk.utils.UriUtils.Companion.getFileName +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_MSG_FLAG +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_MSG_TEXT +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NEW_CONVERSATION +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SHARED_TEXT +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY +import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.getAttachmentFolder +import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isServerEOL +import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isUnifiedSearchAvailable +import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isUserStatusAvailable +import com.nextcloud.talk.utils.rx.SearchViewObservable.Companion.observeSearchView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import io.reactivex.Observable +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import org.apache.commons.lang3.builder.CompareToBuilder +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.parceler.Parcels +import retrofit2.HttpException +import java.util.Objects +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ConversationsListController(bundle: Bundle) : + BaseController(R.layout.controller_conversations_rv, bundle), + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + ConversationMenuInterface { + private val bundle: Bundle + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var eventBus: EventBus + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var unifiedSearchRepository: UnifiedSearchRepository + + private val binding: ControllerConversationsRvBinding by viewBinding(ControllerConversationsRvBinding::bind) + + override val title: String + get() = resources!!.getString(R.string.nc_app_product_name) + + override val appBarLayoutType: AppBarLayoutType + get() = AppBarLayoutType.SEARCH_BAR + + private var currentUser: User? = null + private var roomsQueryDisposable: Disposable? = null + private var openConversationsQueryDisposable: Disposable? = null + private var adapter: FlexibleAdapter>? = null + private var conversationItems: MutableList> = ArrayList() + private var conversationItemsWithHeader: MutableList> = ArrayList() + private val searchableConversationItems: MutableList> = ArrayList() + private var searchItem: MenuItem? = null + private var searchView: SearchView? = null + private var searchQuery: String? = null + private var credentials: String? = null + private var adapterWasNull = true + private var isRefreshing = false + private var conversationMenuBundle: Bundle? = null + private var showShareToScreen = false + private var filesToShare: ArrayList? = null + private var selectedConversation: Conversation? = null + private var textToPaste: String? = "" + private var selectedMessageId: String? = null + private var forwardMessage: Boolean + private var nextUnreadConversationScrollPosition = 0 + private var layoutManager: SmoothScrollLinearLayoutManager? = null + private val callHeaderItems = HashMap() + private var conversationsListBottomDialog: ConversationsListBottomDialog? = null + private val userStatuses = HashMap() + private var searchHelper: MessageSearchHelper? = null + private var searchViewDisposable: Disposable? = null + + override fun onViewBound(view: View) { + super.onViewBound(view) + sharedApplication!!.componentApplication.inject(this) + actionBar?.show() + if (adapter == null) { + adapter = FlexibleAdapter(conversationItems, activity, true) + } else { + binding.loadingContent.visibility = View.GONE + } + adapter!!.addListener(this) + prepareViews() + } + + private fun loadUserAvatar(button: MaterialButton) { + if (activity != null) { + val imageRequest = DisplayUtils.getImageRequestForUrl( + ApiUtils.getUrlForAvatar( + currentUser!!.baseUrl, + currentUser!!.userId, + true + ), + currentUser + ) + val imagePipeline = Fresco.getImagePipeline() + val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null) + dataSource.subscribe( + object : BaseBitmapDataSubscriber() { + override fun onNewResultImpl(bitmap: Bitmap?) { + if (bitmap != null && resources != null) { + val roundedBitmapDrawable = RoundedBitmapDrawableFactory.create( + resources!!, + bitmap + ) + roundedBitmapDrawable.isCircular = true + roundedBitmapDrawable.setAntiAlias(true) + button.icon = roundedBitmapDrawable + } + } + + override fun onFailureImpl(dataSource: DataSource>) { + if (resources != null) { + button.icon = ResourcesCompat.getDrawable(resources!!, R.drawable.ic_user, null) + } + } + }, + UiThreadImmediateExecutorService.getInstance() + ) + } + } + + override fun onAttach(view: View) { + Log.d( + TAG, + "onAttach: Controller: " + System.identityHashCode(this) + + " Activity: " + System.identityHashCode(activity) + ) + super.onAttach(view) + ClosedInterfaceImpl().setUpPushTokenRegistration() + if (!eventBus.isRegistered(this)) { + eventBus.register(this) + } + currentUser = userManager.currentUser.blockingGet() + if (currentUser != null) { + if (isServerEOL(currentUser!!)) { + showServerEOLDialog() + return + } + if (isUnifiedSearchAvailable(currentUser!!)) { + searchHelper = MessageSearchHelper(unifiedSearchRepository) + } + credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token) + if (activity != null && activity is MainActivity) { + loadUserAvatar((activity as MainActivity?)!!.binding.switchAccountButton) + viewThemeUtils.colorMaterialTextButton((activity as MainActivity?)!!.binding.switchAccountButton) + } + fetchData() + } + } + + override fun onDetach(view: View) { + Log.d( + TAG, + "onDetach: Controller: " + System.identityHashCode(this) + + " Activity: " + System.identityHashCode(activity) + ) + super.onDetach(view) + eventBus.unregister(this) + } + + private fun initSearchView() { + if (activity != null) { + val searchManager = activity!!.getSystemService(Context.SEARCH_SERVICE) as SearchManager? + if (searchItem != null) { + searchView = MenuItemCompat.getActionView(searchItem) as SearchView + viewThemeUtils.themeSearchView(searchView!!) + searchView!!.maxWidth = Int.MAX_VALUE + searchView!!.inputType = InputType.TYPE_TEXT_VARIATION_FILTER + var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences.isKeyboardIncognito) { + imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + } + searchView!!.imeOptions = imeOptions + searchView!!.queryHint = searchHint + if (searchManager != null) { + searchView!!.setSearchableInfo(searchManager.getSearchableInfo(activity!!.componentName)) + } + searchViewDisposable = observeSearchView(searchView!!) + .debounce { query: String? -> + if (TextUtils.isEmpty(query)) { + return@debounce Observable.empty() + } else { + return@debounce Observable.timer( + SEARCH_DEBOUNCE_INTERVAL_MS.toLong(), + TimeUnit.MILLISECONDS + ) + } + } + .distinctUntilChanged() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { newText: String? -> onQueryTextChange(newText) } + } + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.menu_conversation_plus_filter, menu) + searchItem = menu.findItem(R.id.action_search) + initSearchView() + } + + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + searchView = MenuItemCompat.getActionView(searchItem) as SearchView + showShareToScreen = !showShareToScreen && hasActivityActionSendIntent() + if (showShareToScreen) { + hideSearchBar() + actionBar?.setTitle(R.string.send_to_three_dots) + } else if (forwardMessage) { + hideSearchBar() + actionBar?.setTitle(R.string.nc_forward_to_three_dots) + } else { + val activity = activity as MainActivity? + searchItem!!.isVisible = conversationItems.size > 0 + if (activity != null) { + if (adapter!!.hasFilter()) { + showSearchView(activity, searchView, searchItem) + searchView!!.setQuery(adapter!!.getFilter(String::class.java), false) + } + activity.binding.searchText.setOnClickListener { + showSearchView(activity, searchView, searchItem) + viewThemeUtils.themeStatusBar(activity, searchView!!) + } + } + searchView!!.setOnCloseListener { + if (TextUtils.isEmpty(searchView!!.query.toString())) { + searchView!!.onActionViewCollapsed() + if (activity != null) { + viewThemeUtils.resetStatusBar(activity, searchView!!) + } + } else { + searchView!!.post { searchView!!.setQuery(TAG, true) } + } + true + } + searchItem!!.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { + adapter!!.setHeadersShown(true) + adapter!!.updateDataSet(searchableConversationItems, false) + adapter!!.showAllHeaders() + withNullableControllerViewBinding { + binding.swipeRefreshLayoutView.isEnabled = false + } + return true + } + + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + adapter!!.setHeadersShown(false) + adapter!!.updateDataSet(conversationItems, false) + adapter!!.hideAllHeaders() + if (searchHelper != null) { + // cancel any pending searches + searchHelper!!.cancelSearch() + binding.swipeRefreshLayoutView.isRefreshing = false + } + withNullableControllerViewBinding { + binding.swipeRefreshLayoutView.isEnabled = true + } + searchView!!.onActionViewCollapsed() + val mainActivity = getActivity() as MainActivity? + if (mainActivity != null) { + mainActivity.binding.appBar.stateListAnimator = AnimatorInflater.loadStateListAnimator( + mainActivity.binding.appBar.context, + R.animator.appbar_elevation_off + ) + mainActivity.binding.toolbar.visibility = View.GONE + mainActivity.binding.searchToolbar.visibility = View.VISIBLE + if (resources != null) { + viewThemeUtils.resetStatusBar(mainActivity, mainActivity.binding.searchToolbar) + } + } + val layoutManager = binding.recyclerView.layoutManager as SmoothScrollLinearLayoutManager? + layoutManager?.scrollToPositionWithOffset(0, 0) + return true + } + }) + } + } + + private fun hasActivityActionSendIntent(): Boolean { + return if (activity != null) { + Intent.ACTION_SEND == activity!!.intent.action || Intent.ACTION_SEND_MULTIPLE == activity!!.intent.action + } else false + } + + override fun showSearchOrToolbar() { + if (TextUtils.isEmpty(searchQuery)) { + super.showSearchOrToolbar() + } + } + + private fun showSearchView(activity: MainActivity, searchView: SearchView?, searchItem: MenuItem?) { + activity.binding.appBar.stateListAnimator = AnimatorInflater.loadStateListAnimator( + activity.binding.appBar.context, + R.animator.appbar_elevation_on + ) + activity.binding.toolbar.visibility = View.VISIBLE + activity.binding.searchToolbar.visibility = View.GONE + searchItem!!.expandActionView() + } + + fun fetchData() { + if (isUserStatusAvailable(userManager.currentUser.blockingGet())) { + fetchUserStatusesAndRooms() + } else { + fetchRooms() + } + } + + private fun fetchUserStatusesAndRooms() { + ncApi.getUserStatuses(credentials, ApiUtils.getUrlForUserStatuses(currentUser!!.baseUrl)) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + override fun onNext(statusesOverall: StatusesOverall) { + for (status in statusesOverall.ocs!!.data!!) { + userStatuses[status.userId] = status + } + fetchRooms() + } + + override fun onError(e: Throwable) { + Log.e(TAG, "failed to fetch user statuses", e) + fetchRooms() + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun fetchRooms() { + dispose(null) + isRefreshing = true + conversationItems = ArrayList() + conversationItemsWithHeader = ArrayList() + val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv3, 1)) + val startNanoTime = System.nanoTime() + Log.d(TAG, "fetchData - getRooms - calling: $startNanoTime") + roomsQueryDisposable = ncApi.getRooms( + credentials, + ApiUtils.getUrlForRooms( + apiVersion, + currentUser!!.baseUrl + ) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ (ocs): RoomsOverall -> + Log.d(TAG, "fetchData - getRooms - got response: $startNanoTime") + + // This is invoked asynchronously, when server returns a response the view might have been + // unbound in the meantime. Check if the view is still there. + // FIXME - does it make sense to update internal data structures even when view has been unbound? + if (view == null) { + Log.d(TAG, "fetchData - getRooms - view is not bound: $startNanoTime") + return@subscribe + } + if (adapterWasNull) { + adapterWasNull = false + binding.loadingContent.visibility = View.GONE + } + if (ocs!!.data!!.isNotEmpty()) { + if (binding.emptyLayout.visibility != View.GONE) { + binding.emptyLayout.visibility = View.GONE + } + if (binding.swipeRefreshLayoutView.visibility != View.VISIBLE) { + binding.swipeRefreshLayoutView.visibility = View.VISIBLE + } + } else { + if (binding.emptyLayout.visibility != View.VISIBLE) { + binding.emptyLayout.visibility = View.VISIBLE + } + if (binding.swipeRefreshLayoutView.visibility != View.GONE) { + binding.swipeRefreshLayoutView.visibility = View.GONE + } + } + for (conversation in ocs.data!!) { + if (bundle.containsKey(KEY_FORWARD_HIDE_SOURCE_ROOM) && conversation.roomId == bundle.getString( + KEY_FORWARD_HIDE_SOURCE_ROOM + ) + ) { + continue + } + val headerTitle: String = resources!!.getString(R.string.conversations) + var genericTextHeaderItem: GenericTextHeaderItem + if (!callHeaderItems.containsKey(headerTitle)) { + genericTextHeaderItem = GenericTextHeaderItem(headerTitle, viewThemeUtils) + callHeaderItems[headerTitle] = genericTextHeaderItem + } + if (activity != null) { + val conversationItem = ConversationItem( + conversation, + currentUser, + activity, + userStatuses[conversation.name], + viewThemeUtils + ) + conversationItems.add(conversationItem) + val conversationItemWithHeader = ConversationItem( + conversation, + currentUser, + activity, + callHeaderItems[headerTitle], + userStatuses[conversation.name], + viewThemeUtils + ) + conversationItemsWithHeader.add(conversationItemWithHeader) + } + } + sortConversations(conversationItems) + sortConversations(conversationItemsWithHeader) + adapter!!.updateDataSet(conversationItems, false) + Handler().postDelayed({ checkToShowUnreadBubble() }, UNREAD_BUBBLE_DELAY.toLong()) + fetchOpenConversations(apiVersion) + withNullableControllerViewBinding { + binding.swipeRefreshLayoutView.isRefreshing = false + } + }, { throwable: Throwable -> + handleHttpExceptions(throwable) + withNullableControllerViewBinding { + binding.swipeRefreshLayoutView.isRefreshing = false + } + dispose(roomsQueryDisposable) + }) { + dispose(roomsQueryDisposable) + withNullableControllerViewBinding { + binding.swipeRefreshLayoutView.isRefreshing = false + } + isRefreshing = false + } + } + + private fun sortConversations(conversationItems: MutableList>) { + conversationItems.sortWith { o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> -> + val conversation1 = (o1 as ConversationItem).model + val conversation2 = (o2 as ConversationItem).model + CompareToBuilder() + .append(conversation2.favorite, conversation1.favorite) + .append(conversation2.lastActivity, conversation1.lastActivity) + .toComparison() + } + } + + private fun fetchOpenConversations(apiVersion: Int) { + searchableConversationItems.clear() + searchableConversationItems.addAll(conversationItemsWithHeader) + if (hasSpreedFeatureCapability(currentUser, "listable-rooms")) { + val openConversationItems: MutableList> = ArrayList() + openConversationsQueryDisposable = ncApi.getOpenConversations( + credentials, + ApiUtils.getUrlForOpenConversations(apiVersion, currentUser!!.baseUrl) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ (ocs): RoomsOverall -> + for (conversation in ocs!!.data!!) { + val headerTitle = resources!!.getString(R.string.openConversations) + var genericTextHeaderItem: GenericTextHeaderItem + if (!callHeaderItems.containsKey(headerTitle)) { + genericTextHeaderItem = GenericTextHeaderItem(headerTitle, viewThemeUtils) + callHeaderItems[headerTitle] = genericTextHeaderItem + } + val conversationItem = ConversationItem( + conversation, + currentUser, + activity, + callHeaderItems[headerTitle], + userStatuses[conversation.name], + viewThemeUtils + ) + openConversationItems.add(conversationItem) + } + searchableConversationItems.addAll(openConversationItems) + }, { throwable: Throwable -> + Log.e(TAG, "fetchData - getRooms - ERROR", throwable) + handleHttpExceptions(throwable) + dispose(openConversationsQueryDisposable) + }) { dispose(openConversationsQueryDisposable) } + } else { + Log.d(TAG, "no open conversations fetched because of missing capability") + } + } + + private fun handleHttpExceptions(throwable: Throwable) { + if (throwable is HttpException) { + when (throwable.code()) { + HTTP_UNAUTHORIZED -> if (parentController != null && parentController!!.router != null) { + Log.d(TAG, "Starting reauth webview via getParentController()") + parentController!!.router.pushController( + RouterTransaction.with( + WebViewLoginController( + currentUser!!.baseUrl, + true + ) + ) + .pushChangeHandler(VerticalChangeHandler()) + .popChangeHandler(VerticalChangeHandler()) + ) + } else { + Log.d(TAG, "Starting reauth webview via ConversationsListController") + showUnauthorizedDialog() + } + else -> {} + } + } + } + + @SuppressLint("ClickableViewAccessibility") + private fun prepareViews() { + layoutManager = SmoothScrollLinearLayoutManager(Objects.requireNonNull(activity)) + binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.adapter = adapter + binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + checkToShowUnreadBubble() + } + } + }) + binding.recyclerView.setOnTouchListener { v: View, _: MotionEvent? -> + if (isAttached && (!isBeingDestroyed || !isDestroyed)) { + val imm = activity!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(v.windowToken, 0) + } + false + } + binding.swipeRefreshLayoutView.setOnRefreshListener { fetchData() } + viewThemeUtils.themeSwipeRefreshLayout(binding.swipeRefreshLayoutView) + binding.emptyLayout.setOnClickListener { showNewConversationsScreen() } + binding.floatingActionButton.setOnClickListener { + run(context) + showNewConversationsScreen() + } + viewThemeUtils.themeFAB(binding.floatingActionButton) + if (activity != null && activity is MainActivity) { + val activity = activity as MainActivity? + activity!!.binding.switchAccountButton.setOnClickListener { + if (resources != null && resources!!.getBoolean(R.bool.multiaccount_support)) { + val newFragment: DialogFragment = ChooseAccountDialogFragment.newInstance() + newFragment.show( + (getActivity() as MainActivity?)!!.supportFragmentManager, + "ChooseAccountDialogFragment" + ) + } else { + router.pushController( + RouterTransaction.with(SettingsController()) + .pushChangeHandler(HorizontalChangeHandler()) + .popChangeHandler(HorizontalChangeHandler()) + ) + } + } + } + binding.newMentionPopupBubble.hide() + binding.newMentionPopupBubble.setPopupBubbleListener { + binding.recyclerView.smoothScrollToPosition( + nextUnreadConversationScrollPosition + ) + } + viewThemeUtils.colorMaterialButtonPrimaryFilled(binding.newMentionPopupBubble) + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun checkToShowUnreadBubble() { + try { + val lastVisibleItem = layoutManager!!.findLastCompletelyVisibleItemPosition() + for (flexItem in conversationItems) { + val conversation: Conversation = (flexItem as ConversationItem).model + val position = adapter!!.getGlobalPositionOf(flexItem) + if (( + conversation.unreadMention || + conversation.unreadMessages > 0 && + conversation.type === Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + ) && position > lastVisibleItem + ) { + nextUnreadConversationScrollPosition = position + if (!binding.newMentionPopupBubble.isShown) { + binding.newMentionPopupBubble.show() + } + return + } + } + nextUnreadConversationScrollPosition = 0 + binding.newMentionPopupBubble.hide() + } catch (e: NullPointerException) { + Log.d( + TAG, + "A NPE was caught when trying to show the unread popup bubble. This might happen when the " + + "user already left the conversations-list screen so the popup bubble is not available anymore.", + e + ) + } + } + + private fun showNewConversationsScreen() { + val bundle = Bundle() + bundle.putBoolean(KEY_NEW_CONVERSATION, true) + router.pushController( + RouterTransaction.with(ContactsController(bundle)) + .pushChangeHandler(HorizontalChangeHandler()) + .popChangeHandler(HorizontalChangeHandler()) + ) + } + + private fun dispose(disposable: Disposable?) { + if (disposable != null && !disposable.isDisposed) { + disposable.dispose() + } else if (disposable == null && roomsQueryDisposable != null && !roomsQueryDisposable!!.isDisposed) { + roomsQueryDisposable!!.dispose() + roomsQueryDisposable = null + } else if (disposable == null && + openConversationsQueryDisposable != null && + !openConversationsQueryDisposable!!.isDisposed + ) { + openConversationsQueryDisposable!!.dispose() + openConversationsQueryDisposable = null + } + } + + public override fun onSaveViewState(view: View, outState: Bundle) { + if (searchView != null && !TextUtils.isEmpty(searchView!!.query)) { + outState.putString(KEY_SEARCH_QUERY, searchView!!.query.toString()) + } + super.onSaveViewState(view, outState) + } + + public override fun onRestoreViewState(view: View, savedViewState: Bundle) { + super.onRestoreViewState(view, savedViewState) + if (savedViewState.containsKey(KEY_SEARCH_QUERY)) { + searchQuery = savedViewState.getString(KEY_SEARCH_QUERY, "") + } + } + + public override fun onDestroy() { + super.onDestroy() + dispose(null) + if (searchViewDisposable != null && !searchViewDisposable!!.isDisposed) { + searchViewDisposable!!.dispose() + } + } + + private fun onQueryTextChange(newText: String?) { + if (!TextUtils.isEmpty(searchQuery)) { + val filter = searchQuery + searchQuery = "" + performFilterAndSearch(filter) + } else if (adapter!!.hasNewFilter(newText)) { + performFilterAndSearch(newText) + } + } + + private fun performFilterAndSearch(filter: String?) { + if (filter!!.length >= SEARCH_MIN_CHARS) { + clearMessageSearchResults() + adapter!!.setFilter(filter) + adapter!!.filterItems() + if (isUnifiedSearchAvailable(currentUser!!)) { + startMessageSearch(filter) + } + } else { + resetSearchResults() + } + } + + private fun resetSearchResults() { + clearMessageSearchResults() + adapter!!.setFilter("") + adapter!!.filterItems() + } + + private fun clearMessageSearchResults() { + val firstHeader = adapter!!.getSectionHeader(0) + if (firstHeader != null && firstHeader.itemViewType == MessagesTextHeaderItem.VIEW_TYPE) { + adapter!!.removeSection(firstHeader) + } else { + adapter!!.removeItemsOfType(MessageResultItem.VIEW_TYPE) + } + adapter!!.removeItemsOfType(LoadMoreResultsItem.VIEW_TYPE) + } + + @SuppressLint("CheckResult") // handled by helper + private fun startMessageSearch(search: String?) { + withNullableControllerViewBinding { + binding.swipeRefreshLayoutView.isRefreshing = true + } + searchHelper?.startMessageSearch(search!!) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe({ results: MessageSearchResults -> onMessageSearchResult(results) }) { throwable: Throwable -> + onMessageSearchError( + throwable + ) + } + } + + @SuppressLint("CheckResult") // handled by helper + private fun loadMoreMessages() { + binding.swipeRefreshLayoutView.isRefreshing = true + val observable = searchHelper!!.loadMore() + observable?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe({ results: MessageSearchResults -> onMessageSearchResult(results) }) { throwable: Throwable -> + onMessageSearchError( + throwable + ) + } + } + + override fun onItemClick(view: View, position: Int): Boolean { + val item = adapter!!.getItem(position) + if (item != null) { + when (item.itemViewType) { + MessageResultItem.VIEW_TYPE -> { + val messageItem: MessageResultItem = item as MessageResultItem + val conversationToken = messageItem.messageEntry.conversationToken + selectedMessageId = messageItem.messageEntry.messageId + showConversationByToken(conversationToken) + } + LoadMoreResultsItem.VIEW_TYPE -> { + loadMoreMessages() + } + ConversationItem.VIEW_TYPE -> { + showConversation((Objects.requireNonNull(item) as ConversationItem).model) + } + } + } + return true + } + + private fun showConversationByToken(conversationToken: String) { + for (absItem in conversationItems) { + val conversationItem = absItem as ConversationItem + if (conversationItem.model.token == conversationToken) { + val conversation = conversationItem.model + showConversation(conversation) + } + } + } + + private fun showConversation(conversation: Conversation?) { + selectedConversation = conversation + if (selectedConversation != null && activity != null) { + val hasChatPermission = AttendeePermissionsUtil(selectedConversation!!.permissions).hasChatPermission( + currentUser!! + ) + if (showShareToScreen) { + if (hasChatPermission && !isReadOnlyConversation(selectedConversation!!)) { + handleSharedData() + showShareToScreen = false + } else { + Toast.makeText(context, R.string.send_to_forbidden, Toast.LENGTH_LONG).show() + } + } else if (forwardMessage) { + if (hasChatPermission && !isReadOnlyConversation(selectedConversation!!)) { + openConversation(bundle.getString(KEY_FORWARD_MSG_TEXT)) + forwardMessage = false + } else { + Toast.makeText(context, R.string.send_to_forbidden, Toast.LENGTH_LONG).show() + } + } else { + openConversation() + } + } + } + + private fun isReadOnlyConversation(conversation: Conversation): Boolean { + return conversation.conversationReadOnlyState === + Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY + } + + private fun handleSharedData() { + collectDataFromIntent() + if (textToPaste!!.isNotEmpty()) { + openConversation(textToPaste) + } else if (filesToShare != null && filesToShare!!.isNotEmpty()) { + showSendFilesConfirmDialog() + } else { + Toast + .makeText(context, context.resources.getString(R.string.nc_common_error_sorry), Toast.LENGTH_LONG) + .show() + } + } + + private fun showSendFilesConfirmDialog() { + if (isStoragePermissionGranted(context)) { + val fileNamesWithLineBreaks = StringBuilder("\n") + for (file in filesToShare!!) { + val filename = getFileName(Uri.parse(file), context) + fileNamesWithLineBreaks.append(filename).append("\n") + } + val confirmationQuestion: String = if (filesToShare!!.size == 1) { + String.format( + resources!!.getString(R.string.nc_upload_confirm_send_single), + selectedConversation!!.displayName + ) + } else { + String.format( + resources!!.getString(R.string.nc_upload_confirm_send_multiple), + selectedConversation!!.displayName + ) + } + val dialogBuilder = MaterialAlertDialogBuilder(binding.floatingActionButton.context) + .setIcon(viewThemeUtils.colorMaterialAlertDialogIcon(context, R.drawable.upload)) + .setTitle(confirmationQuestion) + .setMessage(fileNamesWithLineBreaks.toString()) + .setPositiveButton(R.string.nc_yes) { _, _ -> + upload() + openConversation() + } + .setNegativeButton(R.string.nc_no) { _, _ -> + Log.d(TAG, "sharing files aborted, going back to share-to screen") + showShareToScreen = true + } + viewThemeUtils.colorMaterialAlertDialogBackground(binding.floatingActionButton.context, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } else { + requestStoragePermission(this@ConversationsListController) + } + } + + override fun onItemLongClick(position: Int) { + if (showShareToScreen) { + Log.d(TAG, "sharing to multiple rooms not yet implemented. onItemLongClick is ignored.") + } else { + val clickedItem: Any? = adapter!!.getItem(position) + if (clickedItem != null) { + val conversation = (clickedItem as ConversationItem).model + conversationsListBottomDialog = ConversationsListBottomDialog( + activity!!, + this, + userManager.currentUser.blockingGet(), + conversation + ) + conversationsListBottomDialog!!.show() + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun collectDataFromIntent() { + filesToShare = ArrayList() + if (activity != null && activity!!.intent != null) { + val intent = activity!!.intent + if (Intent.ACTION_SEND == intent.action || Intent.ACTION_SEND_MULTIPLE == intent.action) { + try { + val mimeType = intent.type + if (Mimetype.TEXT_PLAIN == mimeType && intent.getStringExtra(Intent.EXTRA_TEXT) != null) { + // Share from Google Chrome sets text/plain MIME type, but also provides a content:// URI + // with a *screenshot* of the current page in getClipData(). + // Here we assume that when sharing a web page the user would prefer to send the URL + // of the current page rather than a screenshot. + textToPaste = intent.getStringExtra(Intent.EXTRA_TEXT) + } else { + if (intent.clipData != null) { + for (i in 0 until intent.clipData!!.itemCount) { + val item = intent.clipData!!.getItemAt(i) + if (item.uri != null) { + filesToShare!!.add(item.uri.toString()) + } else if (item.text != null) { + textToPaste = item.text.toString() + break + } else { + Log.w(TAG, "datatype not yet implemented for share-to") + } + } + } else { + filesToShare!!.add(intent.data.toString()) + } + } + if (filesToShare!!.isEmpty() && textToPaste!!.isEmpty()) { + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_LONG + ).show() + Log.e(TAG, "failed to get data from intent") + } + } catch (e: Exception) { + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_LONG + ).show() + Log.e(TAG, "Something went wrong when extracting data from intent") + } + } + } + } + + private fun upload() { + if (selectedConversation == null) { + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_LONG + ).show() + Log.e(TAG, "not able to upload any files because conversation was null.") + return + } + try { + var filesToShareArray: Array = arrayOfNulls(filesToShare!!.size) + filesToShareArray = filesToShare!!.toArray(filesToShareArray) + val data = Data.Builder() + .putStringArray(UploadAndShareFilesWorker.DEVICE_SOURCEFILES, filesToShareArray) + .putString( + UploadAndShareFilesWorker.NC_TARGETPATH, + getAttachmentFolder(currentUser!!) + ) + .putString(UploadAndShareFilesWorker.ROOM_TOKEN, selectedConversation!!.token) + .build() + val uploadWorker = OneTimeWorkRequest.Builder(UploadAndShareFilesWorker::class.java) + .setInputData(data) + .build() + WorkManager.getInstance().enqueue(uploadWorker) + Toast.makeText( + context, + context.resources.getString(R.string.nc_upload_in_progess), + Toast.LENGTH_LONG + ).show() + } catch (e: IllegalArgumentException) { + Toast.makeText(context, context.resources.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG).show() + Log.e(TAG, "Something went wrong when trying to upload file", e) + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (requestCode == UploadAndShareFilesWorker.REQUEST_PERMISSION && + grantResults.isNotEmpty() && + grantResults[0] == PackageManager.PERMISSION_GRANTED + ) { + Log.d(TAG, "upload starting after permissions were granted") + showSendFilesConfirmDialog() + } else { + Toast.makeText(context, context.getString(R.string.read_storage_no_permission), Toast.LENGTH_LONG).show() + } + } + + private fun openConversation(textToPaste: String? = "") { + val bundle = Bundle() + bundle.putParcelable(KEY_USER_ENTITY, currentUser) + bundle.putParcelable(KEY_ACTIVE_CONVERSATION, Parcels.wrap(selectedConversation)) + bundle.putString(KEY_ROOM_TOKEN, selectedConversation!!.token) + bundle.putString(KEY_ROOM_ID, selectedConversation!!.roomId) + bundle.putString(KEY_SHARED_TEXT, textToPaste) + if (selectedMessageId != null) { + bundle.putString(BundleKeys.KEY_MESSAGE_ID, selectedMessageId) + selectedMessageId = null + } + remapChatController( + router, + currentUser!!.id!!, + selectedConversation!!.token!!, + bundle, + false + ) + } + + @Subscribe(sticky = true, threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(eventStatus: EventStatus) { + if (currentUser != null && eventStatus.userId == currentUser!!.id) { + when (eventStatus.eventType) { + EventStatus.EventType.CONVERSATION_UPDATE -> if (eventStatus.isAllGood && !isRefreshing) { + fetchData() + } + else -> {} + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(conversationsListFetchDataEvent: ConversationsListFetchDataEvent?) { + fetchData() + Handler().postDelayed({ + if (conversationsListBottomDialog!!.isShowing) { + conversationsListBottomDialog!!.dismiss() + } + }, BOTTOM_SHEET_DELAY) + } + + override fun showDeleteConversationDialog(bundle: Bundle) { + conversationMenuBundle = bundle + if (activity != null && + conversationMenuBundle != null && + currentUser != null && + conversationMenuBundle!!.getLong(KEY_INTERNAL_USER_ID) == currentUser!!.id + ) { + val conversation = Parcels.unwrap(conversationMenuBundle!!.getParcelable(KEY_ROOM)) + if (conversation != null) { + val dialogBuilder = MaterialAlertDialogBuilder(binding.floatingActionButton.context) + .setIcon(viewThemeUtils.colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp)) + .setTitle(R.string.nc_delete_call) + .setMessage(R.string.nc_delete_conversation_more) + .setPositiveButton(R.string.nc_delete) { _, _ -> + val data = Data.Builder() + data.putLong( + KEY_INTERNAL_USER_ID, + conversationMenuBundle!!.getLong(KEY_INTERNAL_USER_ID) + ) + data.putString(KEY_ROOM_TOKEN, conversation.token) + conversationMenuBundle = null + deleteConversation(data.build()) + } + .setNegativeButton(R.string.nc_cancel) { _, _ -> + conversationMenuBundle = null + } + viewThemeUtils.colorMaterialAlertDialogBackground(binding.floatingActionButton.context, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + } + } + + private fun showUnauthorizedDialog() { + if (activity != null) { + val dialogBuilder = MaterialAlertDialogBuilder(binding.floatingActionButton.context) + .setIcon(viewThemeUtils.colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp)) + .setTitle(R.string.nc_dialog_invalid_password) + .setMessage(R.string.nc_dialog_reauth_or_delete) + .setCancelable(false) + .setPositiveButton(R.string.nc_delete) { _, _ -> + val otherUserExists = userManager + .scheduleUserForDeletionWithId(currentUser!!.id!!) + .blockingGet() + val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() + WorkManager.getInstance().enqueue(accountRemovalWork) + if (otherUserExists && view != null) { + onViewBound(view!!) + onAttach(view!!) + } else if (!otherUserExists) { + router.setRoot( + RouterTransaction.with(ServerSelectionController()) + .pushChangeHandler(VerticalChangeHandler()) + .popChangeHandler(VerticalChangeHandler()) + ) + } + } + .setNegativeButton(R.string.nc_settings_reauthorize) { _, _ -> + router.pushController( + RouterTransaction.with( + WebViewLoginController(currentUser!!.baseUrl, true) + ) + .pushChangeHandler(VerticalChangeHandler()) + .popChangeHandler(VerticalChangeHandler()) + ) + } + viewThemeUtils.colorMaterialAlertDialogBackground(binding.floatingActionButton.context, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + } + + private fun showServerEOLDialog() { + val dialogBuilder = MaterialAlertDialogBuilder(binding.floatingActionButton.context) + .setIcon(viewThemeUtils.colorMaterialAlertDialogIcon(context, R.drawable.ic_warning_white)) + .setTitle(R.string.nc_settings_server_eol_title) + .setMessage(R.string.nc_settings_server_eol) + .setCancelable(false) + .setPositiveButton(R.string.nc_settings_remove_account) { _, _ -> + val otherUserExists = userManager + .scheduleUserForDeletionWithId(currentUser!!.id!!) + .blockingGet() + val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() + WorkManager.getInstance().enqueue(accountRemovalWork) + if (otherUserExists && view != null) { + onViewBound(view!!) + onAttach(view!!) + } else if (!otherUserExists) { + router.setRoot( + RouterTransaction.with( + ServerSelectionController() + ) + .pushChangeHandler(VerticalChangeHandler()) + .popChangeHandler(VerticalChangeHandler()) + ) + } + } + .setNegativeButton(R.string.nc_cancel) { _, _ -> + if (userManager.users.blockingGet().isNotEmpty()) { + router.pushController(RouterTransaction.with(SwitchAccountController())) + } else { + activity!!.finishAffinity() + activity!!.finish() + } + } + viewThemeUtils.colorMaterialAlertDialogBackground(binding.floatingActionButton.context, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + + private fun deleteConversation(data: Data) { + val deleteConversationWorker = + OneTimeWorkRequest.Builder(DeleteConversationWorker::class.java).setInputData(data).build() + WorkManager.getInstance().enqueue(deleteConversationWorker) + } + + private fun onMessageSearchResult(results: MessageSearchResults) { + if (searchView!!.query.isNotEmpty()) { + clearMessageSearchResults() + val entries = results.messages + if (entries.isNotEmpty()) { + val adapterItems: MutableList> = ArrayList(entries.size + 1) + for (i in entries.indices) { + val showHeader = i == 0 + adapterItems.add( + MessageResultItem( + context, + currentUser!!, + entries[i], + showHeader, + viewThemeUtils + ) + ) + } + if (results.hasMore) { + adapterItems.add(LoadMoreResultsItem) + } + adapter!!.addItems(0, adapterItems) + binding.recyclerView.scrollToPosition(0) + } + } + withNullableControllerViewBinding { + binding.swipeRefreshLayoutView.isRefreshing = false + } + } + + private fun onMessageSearchError(throwable: Throwable) { + handleHttpExceptions(throwable) + withNullableControllerViewBinding { + binding.swipeRefreshLayoutView.isRefreshing = false + } + } + + companion object { + const val TAG = "ConvListController" + const val UNREAD_BUBBLE_DELAY = 2500 + const val BOTTOM_SHEET_DELAY: Long = 2500 + private const val KEY_SEARCH_QUERY = "ContactsController.searchQuery" + const val SEARCH_DEBOUNCE_INTERVAL_MS = 300 + const val SEARCH_MIN_CHARS = 2 + const val HTTP_UNAUTHORIZED = 401 + } + + init { + setHasOptionsMenu(true) + forwardMessage = bundle.getBoolean(KEY_FORWARD_MSG_FLAG) + this.bundle = bundle + } +} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/GeocodingController.kt b/app/src/main/java/com/nextcloud/talk/controllers/GeocodingController.kt index 7d29ae162..592b729e0 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/GeocodingController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/GeocodingController.kt @@ -41,7 +41,7 @@ import com.nextcloud.talk.R import com.nextcloud.talk.adapters.GeocodingAdapter import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication -import com.nextcloud.talk.controllers.base.NewBaseController +import com.nextcloud.talk.controllers.base.BaseController import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.databinding.ControllerGeocodingBinding import com.nextcloud.talk.utils.bundle.BundleKeys @@ -58,7 +58,7 @@ import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class GeocodingController(args: Bundle) : - NewBaseController( + BaseController( R.layout.controller_geocoding, args ), diff --git a/app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt b/app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt index 3efcbe349..714e2fa4b 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt @@ -49,7 +49,7 @@ import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.nextcloud.talk.R import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication -import com.nextcloud.talk.controllers.base.NewBaseController +import com.nextcloud.talk.controllers.base.BaseController import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.databinding.ControllerLocationBinding import com.nextcloud.talk.models.json.generic.GenericOverall @@ -83,7 +83,7 @@ import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class LocationPickerController(args: Bundle) : - NewBaseController( + BaseController( R.layout.controller_location, args ), diff --git a/app/src/main/java/com/nextcloud/talk/controllers/LockedController.kt b/app/src/main/java/com/nextcloud/talk/controllers/LockedController.kt index 7744da6fa..9b9276ed5 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/LockedController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/LockedController.kt @@ -39,7 +39,7 @@ import autodagger.AutoInjector import com.nextcloud.talk.R 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.base.BaseController import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.databinding.ControllerLockedBinding import com.nextcloud.talk.utils.DisplayUtils @@ -48,7 +48,7 @@ import java.util.concurrent.Executor import java.util.concurrent.Executors @AutoInjector(NextcloudTalkApplication::class) -class LockedController : NewBaseController(R.layout.controller_locked) { +class LockedController : BaseController(R.layout.controller_locked) { private val binding: ControllerLockedBinding by viewBinding(ControllerLockedBinding::bind) override val appBarLayoutType: AppBarLayoutType 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 caa597562..71e01ca3d 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt @@ -52,7 +52,7 @@ import com.nextcloud.talk.activities.TakePhotoActivity 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.base.BaseController import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ControllerProfileBinding @@ -94,7 +94,7 @@ import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @Suppress("Detekt.TooManyFunctions") -class ProfileController : NewBaseController(R.layout.controller_profile) { +class ProfileController : BaseController(R.layout.controller_profile) { private val binding: ControllerProfileBinding by viewBinding(ControllerProfileBinding::bind) @Inject diff --git a/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.kt b/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.kt index 3bf99a0dc..a41834f27 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.kt @@ -38,7 +38,7 @@ import com.nextcloud.talk.R import com.nextcloud.talk.adapters.items.NotificationSoundItem 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.base.BaseController import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.databinding.ControllerGenericRvBinding import com.nextcloud.talk.models.RingtoneSettings @@ -52,7 +52,7 @@ import java.io.IOException @AutoInjector(NextcloudTalkApplication::class) class RingtoneSelectionController(args: Bundle) : - NewBaseController( + BaseController( R.layout.controller_generic_rv, args ), diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ServerSelectionController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ServerSelectionController.kt index 14ca85082..e8f643260 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ServerSelectionController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ServerSelectionController.kt @@ -41,7 +41,7 @@ import com.nextcloud.talk.R 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.base.BaseController import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.databinding.ControllerServerSelectionBinding import com.nextcloud.talk.models.json.generic.Status @@ -61,7 +61,7 @@ import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class ServerSelectionController : - NewBaseController(R.layout.controller_server_selection) { + BaseController(R.layout.controller_server_selection) { private val binding: ControllerServerSelectionBinding by viewBinding(ControllerServerSelectionBinding::bind) diff --git a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt b/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt index 2ba2cebbc..34d0804db 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt @@ -68,7 +68,7 @@ import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.setAppTheme import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication -import com.nextcloud.talk.controllers.base.NewBaseController +import com.nextcloud.talk.controllers.base.BaseController import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ControllerSettingsBinding @@ -105,7 +105,7 @@ import java.util.Locale import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) -class SettingsController : NewBaseController(R.layout.controller_settings) { +class SettingsController : BaseController(R.layout.controller_settings) { private val binding: ControllerSettingsBinding by viewBinding(ControllerSettingsBinding::bind) @Inject diff --git a/app/src/main/java/com/nextcloud/talk/controllers/SwitchAccountController.kt b/app/src/main/java/com/nextcloud/talk/controllers/SwitchAccountController.kt index 0c16840a0..d1bb10b8e 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/SwitchAccountController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/SwitchAccountController.kt @@ -37,7 +37,7 @@ import com.nextcloud.talk.R import com.nextcloud.talk.adapters.items.AdvancedUserItem 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.base.BaseController import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ControllerGenericRvBinding @@ -59,7 +59,7 @@ import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class SwitchAccountController(args: Bundle? = null) : - NewBaseController( + BaseController( R.layout.controller_generic_rv, args ) { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.kt b/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.kt index e7c28718f..466653833 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.kt @@ -51,7 +51,7 @@ import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.nextcloud.talk.R 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.base.BaseController import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.databinding.ControllerWebViewLoginBinding import com.nextcloud.talk.events.CertificateEvent @@ -78,7 +78,7 @@ import java.util.Locale import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) -class WebViewLoginController(args: Bundle? = null) : NewBaseController( +class WebViewLoginController(args: Bundle? = null) : BaseController( R.layout.controller_web_view_login, args ) { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.java b/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.java deleted file mode 100644 index d88b143ec..000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Andy Scherzinger - * @author BlueLine Labs, Inc. - * @author Mario Danic - * Copyright (C) 2021 Andy Scherzinger (info@andy-scherzinger.de) - * Copyright (C) 2020 Mario Danic (mario@lovelyhq.com) - * Copyright (C) 2016 BlueLine Labs, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.nextcloud.talk.controllers.base; - -import android.animation.AnimatorInflater; -import android.content.Context; -import android.os.Build; -import android.os.Bundle; -import android.util.Log; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import androidx.appcompat.app.ActionBar; -import androidx.core.content.res.ResourcesCompat; - -import com.bluelinelabs.conductor.Controller; -import com.google.android.material.appbar.AppBarLayout; -import com.nextcloud.talk.R; -import com.nextcloud.talk.activities.MainActivity; -import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.controllers.AccountVerificationController; -import com.nextcloud.talk.controllers.ServerSelectionController; -import com.nextcloud.talk.controllers.SwitchAccountController; -import com.nextcloud.talk.controllers.WebViewLoginController; -import com.nextcloud.talk.controllers.base.providers.ActionBarProvider; -import com.nextcloud.talk.ui.theme.ViewThemeUtils; -import com.nextcloud.talk.utils.DisplayUtils; -import com.nextcloud.talk.utils.preferences.AppPreferences; - -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; - -import autodagger.AutoInjector; - -@AutoInjector(NextcloudTalkApplication.class) -public abstract class BaseController extends ButterKnifeController { - public enum AppBarLayoutType { - TOOLBAR, - SEARCH_BAR, - EMPTY - } - - private static final String TAG = "BaseController"; - @Inject - AppPreferences appPreferences; - - @Inject - Context context; - - @Inject - ViewThemeUtils viewThemeUtils; - - protected BaseController() { - cleanTempCertPreference(); - } - - protected BaseController(Bundle args) { - super(args); - cleanTempCertPreference(); - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == android.R.id.home) { - getRouter().popCurrentController(); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void cleanTempCertPreference() { - NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); - - List temporaryClassNames = new ArrayList<>(); - temporaryClassNames.add(ServerSelectionController.class.getName()); - temporaryClassNames.add(AccountVerificationController.class.getName()); - temporaryClassNames.add(WebViewLoginController.class.getName()); - temporaryClassNames.add(SwitchAccountController.class.getName()); - - if (!temporaryClassNames.contains(getClass().getName())) { - appPreferences.removeTemporaryClientCertAlias(); - } - - } - - @Override - protected void onViewBound(@NonNull View view) { - super.onViewBound(view); - MainActivity activity = null; - - if (getActivity() != null && getActivity() instanceof MainActivity) { - activity = (MainActivity) getActivity(); - viewThemeUtils.themeCardView(activity.binding.searchToolbar); - viewThemeUtils.themeToolbar(activity.binding.toolbar); - viewThemeUtils.themeSearchBarText(activity.binding.searchText); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences.getIsKeyboardIncognito()) { - disableKeyboardPersonalisedLearning((ViewGroup) view); - - if (activity != null) { - disableKeyboardPersonalisedLearning(activity.binding.appBar); - } - } - } - - // Note: This is just a quick demo of how an ActionBar *can* be accessed, not necessarily how it *should* - // be accessed. In a production app, this would use Dagger instead. - protected ActionBar getActionBar() { - ActionBarProvider actionBarProvider = null; - try { - actionBarProvider = ((ActionBarProvider) getActivity()); - } catch (Exception exception) { - Log.d(TAG, "Failed to fetch the action bar provider"); - } - return actionBarProvider != null ? actionBarProvider.getSupportActionBar() : null; - } - - @Override - protected void onAttach(@NonNull View view) { - showSearchOrToolbar(); - - setTitle(); - if (getActionBar() != null) { - getActionBar().setDisplayHomeAsUpEnabled(getParentController() != null || getRouter().getBackstackSize() > 1); - } - - super.onAttach(view); - } - - protected void showSearchOrToolbar() { - if (getActivity() != null && getActivity() instanceof MainActivity) { - boolean showSearchBar = getAppBarLayoutType() == AppBarLayoutType.SEARCH_BAR; - MainActivity activity = (MainActivity) getActivity(); - - if (getAppBarLayoutType() == AppBarLayoutType.EMPTY) { - activity.binding.toolbar.setVisibility(View.GONE); - activity.binding.searchToolbar.setVisibility(View.GONE); - } else { - AppBarLayout.LayoutParams layoutParams = - (AppBarLayout.LayoutParams) activity.binding.searchToolbar.getLayoutParams(); - - if (showSearchBar) { - activity.binding.searchToolbar.setVisibility(View.VISIBLE); - activity.binding.searchText.setHint(getSearchHint()); - activity.binding.toolbar.setVisibility(View.GONE); - //layoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS); - layoutParams.setScrollFlags(0); - activity.binding.appBar.setStateListAnimator(AnimatorInflater.loadStateListAnimator( - activity.binding.appBar.getContext(), - R.animator.appbar_elevation_off) - ); - } else { - hideSearchBar(); - } - - activity.binding.searchToolbar.setLayoutParams(layoutParams); - - if ((getResources() != null)) { - if (showSearchBar) { - viewThemeUtils.resetStatusBar(activity, activity.binding.searchToolbar); - } else { - viewThemeUtils.themeStatusBar(activity, activity.binding.searchToolbar); - } - } - } - - if ((getResources() != null)) { - DisplayUtils.applyColorToNavigationBar( - activity.getWindow(), - ResourcesCompat.getColor(getResources(), R.color.bg_default, null) - ); - } - } - } - - protected void hideSearchBar() { - MainActivity activity = (MainActivity) getActivity(); - AppBarLayout.LayoutParams layoutParams = - (AppBarLayout.LayoutParams) activity.binding.searchToolbar.getLayoutParams(); - - activity.binding.searchToolbar.setVisibility(View.GONE); - activity.binding.toolbar.setVisibility(View.VISIBLE); - layoutParams.setScrollFlags(0); - activity.binding.appBar.setStateListAnimator(AnimatorInflater.loadStateListAnimator( - activity.binding.appBar.getContext(), - R.animator.appbar_elevation_on) - ); - } - - @Override - protected void onDetach(@NonNull View view) { - super.onDetach(view); - InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - imm.hideSoftInputFromWindow(view.getWindowToken(), 0); - } - } - - protected void setTitle() { - Controller parentController = getParentController(); - while (parentController != null) { - if (parentController instanceof BaseController && ((BaseController) parentController).getTitle() != null) { - return; - } - parentController = parentController.getParentController(); - } - - String title = getTitle(); - ActionBar actionBar = getActionBar(); - if (title != null && actionBar != null) { - actionBar.setTitle(title); - } - } - - protected String getTitle() { - return null; - } - - @RequiresApi(api = Build.VERSION_CODES.O) - private void disableKeyboardPersonalisedLearning(final ViewGroup viewGroup) { - View view; - EditText editText; - - for (int i = 0; i < viewGroup.getChildCount(); i++) { - view = viewGroup.getChildAt(i); - if (view instanceof EditText) { - editText = (EditText) view; - editText.setImeOptions(editText.getImeOptions() | EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING); - } else if (view instanceof ViewGroup) { - disableKeyboardPersonalisedLearning((ViewGroup) view); - } - } - } - - public AppBarLayoutType getAppBarLayoutType() { - return AppBarLayoutType.TOOLBAR; - } - - public String getSearchHint() { - return context.getString(R.string.appbar_search_in, context.getString(R.string.nc_app_product_name)); - } -} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/base/NewBaseController.kt b/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt similarity index 92% rename from app/src/main/java/com/nextcloud/talk/controllers/base/NewBaseController.kt rename to app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt index 065c3b078..f9ae3b379 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/base/NewBaseController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt @@ -63,7 +63,7 @@ import javax.inject.Inject import kotlin.jvm.internal.Intrinsics @AutoInjector(NextcloudTalkApplication::class) -abstract class NewBaseController(@LayoutRes var layoutRes: Int, args: Bundle? = null) : Controller(args) { +abstract class BaseController(@LayoutRes var layoutRes: Int, args: Bundle? = null) : Controller(args) { enum class AppBarLayoutType { TOOLBAR, SEARCH_BAR, EMPTY } @@ -145,7 +145,7 @@ abstract class NewBaseController(@LayoutRes var layoutRes: Int, args: Bundle? = super.onAttach(view) } - protected fun showSearchOrToolbar() { + open fun showSearchOrToolbar() { if (isValidActivity(activity)) { val showSearchBar = appBarLayoutType == AppBarLayoutType.SEARCH_BAR val activity = activity as MainActivity @@ -202,6 +202,18 @@ abstract class NewBaseController(@LayoutRes var layoutRes: Int, args: Bundle? = binding.searchToolbar.visibility = View.GONE } + fun hideSearchBar() { + val activity = activity as MainActivity? + val layoutParams = activity!!.binding.searchToolbar.layoutParams as AppBarLayout.LayoutParams + activity.binding.searchToolbar.visibility = View.GONE + activity.binding.toolbar.visibility = View.VISIBLE + layoutParams.scrollFlags = 0 + activity.binding.appBar.stateListAnimator = AnimatorInflater.loadStateListAnimator( + activity.binding.appBar.context, + R.animator.appbar_elevation_on + ) + } + private fun colorizeStatusBar(showSearchBar: Boolean, activity: Activity?, resources: Resources?) { if (activity != null && resources != null) { if (showSearchBar) { @@ -223,7 +235,7 @@ abstract class NewBaseController(@LayoutRes var layoutRes: Int, args: Bundle? = override fun onDetach(view: View) { super.onDetach(view) - val imm = context!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(view.windowToken, 0) } @@ -239,17 +251,10 @@ abstract class NewBaseController(@LayoutRes var layoutRes: Int, args: Bundle? = private fun calculateValidParentController() { var parentController = parentController while (parentController != null) { - if (isValidController(parentController)) { - return - } parentController = parentController.parentController } } - private fun isValidController(parentController: Controller): Boolean { - return parentController is BaseController && parentController.title != null - } - private fun isTitleSetable(): Boolean { return title != null && actionBar != null } @@ -280,7 +285,7 @@ abstract class NewBaseController(@LayoutRes var layoutRes: Int, args: Bundle? = temporaryClassNames.add(WebViewLoginController::class.java.name) temporaryClassNames.add(SwitchAccountController::class.java.name) if (!temporaryClassNames.contains(javaClass.name)) { - appPreferences!!.removeTemporaryClientCertAlias() + appPreferences.removeTemporaryClientCertAlias() } } @@ -323,7 +328,7 @@ abstract class NewBaseController(@LayoutRes var layoutRes: Int, args: Bundle? = open val appBarLayoutType: AppBarLayoutType get() = AppBarLayoutType.TOOLBAR val searchHint: String - get() = context!!.getString(R.string.appbar_search_in, context!!.getString(R.string.nc_app_product_name)) + get() = context.getString(R.string.appbar_search_in, context.getString(R.string.nc_app_product_name)) companion object { private val TAG = BaseController::class.java.simpleName diff --git a/app/src/main/java/com/nextcloud/talk/controllers/base/ButterKnifeController.kt b/app/src/main/java/com/nextcloud/talk/controllers/base/ButterKnifeController.kt deleted file mode 100644 index 1226965ac..000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/base/ButterKnifeController.kt +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Nextcloud Talk application - * - * @author BlueLine Labs, Inc. - * Copyright (C) 2016 BlueLine Labs, Inc. - * - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.nextcloud.talk.controllers.base - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import butterknife.ButterKnife -import butterknife.Unbinder -import com.bluelinelabs.conductor.Controller - -abstract class ButterKnifeController : Controller { - - private var unbinder: Unbinder? = null - - constructor() - - constructor(args: Bundle) : super(args) - - protected abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View { - val view = inflateView(inflater, container) - unbinder = ButterKnife.bind(this, view) - onViewBound(view) - return view - } - - protected open fun onViewBound(view: View) { - // unused atm - } - - override fun onDestroyView(view: View) { - super.onDestroyView(view) - unbinder!!.unbind() - unbinder = null - } -} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/EntryMenuController.kt b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/EntryMenuController.kt index 79b6a15f2..c169b7d05 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/EntryMenuController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/EntryMenuController.kt @@ -42,7 +42,7 @@ import com.google.android.material.textfield.TextInputLayout import com.nextcloud.talk.R 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.base.BaseController import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.databinding.ControllerEntryMenuBinding import com.nextcloud.talk.models.json.conversations.Conversation @@ -60,7 +60,7 @@ import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class EntryMenuController(args: Bundle) : - NewBaseController( + BaseController( R.layout.controller_entry_menu, args ) { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.kt b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.kt index 89f9402af..5ed400524 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.kt @@ -34,7 +34,7 @@ import com.nextcloud.talk.R 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.base.BaseController import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ControllerOperationsMenuBinding @@ -79,7 +79,7 @@ import java.util.Collections import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) -class OperationsMenuController(args: Bundle) : NewBaseController( +class OperationsMenuController(args: Bundle) : BaseController( R.layout.controller_operations_menu, args ) { diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt index 1e77a7b38..c3e8bd1b7 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt @@ -310,8 +310,4 @@ class ConversationsListBottomDialog( val behavior = BottomSheetBehavior.from(bottomSheet as View) behavior.state = BottomSheetBehavior.STATE_COLLAPSED } - - companion object { - private const val TAG = "ConversationOperationDialog" - } } diff --git a/detekt.yml b/detekt.yml index 6cc523fa7..e8e167c4b 100644 --- a/detekt.yml +++ b/detekt.yml @@ -1,5 +1,5 @@ build: - maxIssues: 77 + maxIssues: 94 weights: # complexity: 2 # LongParameterList: 1