diff --git a/src/main/java/com/nextcloud/client/di/AppModule.java b/src/main/java/com/nextcloud/client/di/AppModule.java
index 44faf419c7..780ed42d43 100644
--- a/src/main/java/com/nextcloud/client/di/AppModule.java
+++ b/src/main/java/com/nextcloud/client/di/AppModule.java
@@ -61,6 +61,7 @@ import com.owncloud.android.ui.activities.data.activities.RemoteActivitiesReposi
import com.owncloud.android.ui.activities.data.files.FilesRepository;
import com.owncloud.android.ui.activities.data.files.FilesServiceApiImpl;
import com.owncloud.android.ui.activities.data.files.RemoteFilesRepository;
+import com.nextcloud.client.utils.Throttler;
import org.greenrobot.eventbus.EventBus;
@@ -231,4 +232,9 @@ class AppModule {
LocalBroadcastManager localBroadcastManager(Context context) {
return LocalBroadcastManager.getInstance(context);
}
+
+ @Provides
+ Throttler throttler(Clock clock) {
+ return new Throttler(clock);
+ }
}
diff --git a/src/main/java/com/nextcloud/client/utils/Throttler.kt b/src/main/java/com/nextcloud/client/utils/Throttler.kt
new file mode 100644
index 0000000000..bf424cd39a
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/utils/Throttler.kt
@@ -0,0 +1,48 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Álvaro Brey Vilas
+ * Copyright (C) 2021 Álvaro Brey Vilas
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.utils
+
+import com.nextcloud.client.core.Clock
+
+/**
+ * Simple throttler that just discards new calls until interval has passed.
+ *
+ * @param clock the Clock to provide timestamps
+ */
+class Throttler(private val clock: Clock) {
+
+ /**
+ * The interval, in milliseconds, between accepted calls
+ */
+ @Suppress("MagicNumber")
+ var intervalMillis = 150L
+ private val timestamps: MutableMap = mutableMapOf()
+
+ @Synchronized
+ fun run(key: String, runnable: Runnable) {
+ val time = clock.currentTime
+ val lastCallTimestamp = timestamps[key] ?: 0
+ if (time - lastCallTimestamp > intervalMillis) {
+ runnable.run()
+ timestamps[key] = time
+ }
+ }
+}
diff --git a/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
index 7f7239e04a..3dd03626fd 100644
--- a/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
+++ b/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
@@ -101,6 +101,7 @@ import com.owncloud.android.utils.EncryptionUtils;
import com.owncloud.android.utils.FileSortOrder;
import com.owncloud.android.utils.FileStorageUtils;
import com.owncloud.android.utils.MimeTypeUtil;
+import com.nextcloud.client.utils.Throttler;
import com.owncloud.android.utils.theme.ThemeColorUtils;
import com.owncloud.android.utils.theme.ThemeFabUtils;
import com.owncloud.android.utils.theme.ThemeToolbarUtils;
@@ -180,6 +181,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
@Inject AppPreferences preferences;
@Inject UserAccountManager accountManager;
@Inject ClientFactory clientFactory;
+ @Inject Throttler throttler;
protected FileFragment.ContainerActivity mContainerActivity;
protected OCFile mFile;
@@ -199,6 +201,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
protected String mLimitToMimeType;
private FloatingActionButton mFabMain;
+
@Inject DeviceInfo deviceInfo;
protected enum MenuItemAddRemove {
@@ -518,20 +521,22 @@ public class OCFileListFragment extends ExtendedListFragment implements
@Override
public void onOverflowIconClicked(OCFile file, View view) {
- PopupMenu popup = new PopupMenu(getActivity(), view);
- popup.inflate(R.menu.item_file);
- FileMenuFilter mf = new FileMenuFilter(mAdapter.getFiles().size(),
- Collections.singleton(file),
- mContainerActivity, getActivity(),
- true,
- accountManager.getUser());
- mf.filter(popup.getMenu(), true);
- popup.setOnMenuItemClickListener(item -> {
- Set checkedFiles = new HashSet<>();
- checkedFiles.add(file);
- return onFileActionChosen(item, checkedFiles);
+ throttler.run("overflowClick", () -> {
+ PopupMenu popup = new PopupMenu(getActivity(), view);
+ popup.inflate(R.menu.item_file);
+ FileMenuFilter mf = new FileMenuFilter(mAdapter.getFiles().size(),
+ Collections.singleton(file),
+ mContainerActivity, getActivity(),
+ true,
+ accountManager.getUser());
+ mf.filter(popup.getMenu(), true);
+ popup.setOnMenuItemClickListener(item -> {
+ Set checkedFiles = new HashSet<>();
+ checkedFiles.add(file);
+ return onFileActionChosen(item, checkedFiles);
+ });
+ popup.show();
});
- popup.show();
}
@Override
diff --git a/src/test/java/com/nextcloud/client/utils/ThrottlerTest.kt b/src/test/java/com/nextcloud/client/utils/ThrottlerTest.kt
new file mode 100644
index 0000000000..072277d7bf
--- /dev/null
+++ b/src/test/java/com/nextcloud/client/utils/ThrottlerTest.kt
@@ -0,0 +1,86 @@
+package com.nextcloud.client.utils
+
+import com.nextcloud.client.core.Clock
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.every
+
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import io.mockk.verify
+import org.junit.Before
+import org.junit.Test
+
+class ThrottlerTest {
+ companion object {
+ private const val KEY = "TEST"
+ }
+
+ @MockK
+ lateinit var runnable: Runnable
+
+ @MockK
+ lateinit var clock: Clock
+
+ @Before
+ fun setUp() {
+ MockKAnnotations.init(this, relaxed = true)
+ every { runnable.run() } just Runs
+ }
+
+ private fun runWithThrottler(throttler: Throttler) {
+ throttler.run(KEY, runnable)
+ }
+
+ @Test
+ fun unchangingTime_multipleCalls_calledExactlyOnce() {
+ // given
+ every { clock.currentTime } returns 300
+
+ val sut = Throttler(clock).apply {
+ intervalMillis = 150
+ }
+
+ // when
+ repeat(10) {
+ runWithThrottler(sut)
+ }
+
+ // then
+ verify(exactly = 1) { runnable.run() }
+ }
+
+ @Test
+ fun spacedCalls_noThrottle() {
+ // given
+ val sut = Throttler(clock).apply {
+ intervalMillis = 150
+ }
+ every { clock.currentTime } returnsMany listOf(200, 400, 600, 800)
+
+ // when
+ repeat(4) {
+ runWithThrottler(sut)
+ }
+
+ // then
+ verify(exactly = 4) { runnable.run() }
+ }
+
+ @Test
+ fun mixedIntervals_sometimesThrottled() {
+ // given
+ val sut = Throttler(clock).apply {
+ intervalMillis = 150
+ }
+ every { clock.currentTime } returnsMany listOf(200, 300, 400, 500)
+
+ // when
+ repeat(4) {
+ runWithThrottler(sut)
+ }
+
+ // then
+ verify(exactly = 2) { runnable.run() }
+ }
+}