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

OcFileListFragment: throttle overflow menu clicks
This commit is contained in:
Álvaro Brey 2021-12-15 13:08:16 +01:00 committed by GitHub
commit 3b947b981b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 158 additions and 13 deletions

View file

@ -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);
}
}

View file

@ -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
}
}
}

View file

@ -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

View file

@ -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() }
}
}