Selaa lähdekoodia

Merge pull request #9435 from nextcloud/fix/multi-tap-overflow

OcFileListFragment: throttle overflow menu clicks
Álvaro Brey 3 vuotta sitten
vanhempi
commit
3b947b981b

+ 6 - 0
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);
+    }
 }

+ 48 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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<String, Long> = 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
+        }
+    }
+}

+ 18 - 13
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<OCFile> 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<OCFile> checkedFiles = new HashSet<>();
+                checkedFiles.add(file);
+                return onFileActionChosen(item, checkedFiles);
+            });
+            popup.show();
         });
-        popup.show();
     }
 
     @Override

+ 86 - 0
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() }
+    }
+}