Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions app/src/main/java/com/nextcloud/client/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import com.nextcloud.client.network.ConnectivityService
import com.nextcloud.client.notifications.AppNotificationManager
import com.nextcloud.client.notifications.AppNotificationManagerImpl
import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.client.utils.Throttler
import com.nextcloud.repository.ClientRepository
import com.nextcloud.repository.RemoteClientRepository
import com.nextcloud.utils.e2ee.E2EEActionResolver
Expand Down Expand Up @@ -205,9 +204,6 @@ internal class AppModule {
@Provides
fun localBroadcastManager(context: Context): LocalBroadcastManager = LocalBroadcastManager.getInstance(context)

@Provides
fun throttler(clock: Clock): Throttler = Throttler(clock)

@Provides
@Singleton
fun passCodeManager(preferences: AppPreferences, clock: Clock): PassCodeManager =
Expand Down
48 changes: 48 additions & 0 deletions app/src/main/java/com/nextcloud/client/utils/Throttle.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.utils

import androidx.annotation.MainThread
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.TimeMark
import kotlin.time.TimeSource

/** Runs an action at most once per interval and ignores calls while a launched action is still running. */
class Throttle(
private val interval: Duration = DEFAULT_INTERVAL,
private val timeSource: TimeSource = TimeSource.Monotonic
) {
private var lastAccepted: TimeMark? = null
private var inFlight: Job? = null

@MainThread
fun run(action: () -> Unit) {
if (tryAcquire()) action()
}

@MainThread
fun launch(scope: CoroutineScope, block: suspend CoroutineScope.() -> Unit) {
if (!tryAcquire()) return
inFlight = scope.launch(block = block)
}

private fun tryAcquire(): Boolean {
if (inFlight?.isActive == true) return false
if (lastAccepted?.let { it.elapsedNow() < interval } == true) return false

lastAccepted = timeSource.markNow()
return true
}

companion object {
val DEFAULT_INTERVAL = 150.milliseconds
}
}
35 changes: 0 additions & 35 deletions app/src/main/java/com/nextcloud/client/utils/Throttler.kt

This file was deleted.

32 changes: 32 additions & 0 deletions app/src/main/java/com/nextcloud/client/utils/TrailingThrottle.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.utils

import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.transform
import kotlin.time.Duration

/** Emits the first request immediately and merges later ones into a single emission after the interval. */
class TrailingThrottle(private val interval: Duration) {

private val requests = MutableSharedFlow<Unit>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)

val events: Flow<Unit> = requests.transform {
emit(it)
delay(interval)
}

fun request() {
requests.tryEmit(Unit)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import com.nextcloud.client.device.DeviceInfo
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.documentscan.AppScanOptionalFeature
import com.nextcloud.client.documentscan.DocumentScanActivity
import com.nextcloud.client.utils.Throttler
import com.nextcloud.client.utils.Throttle
import com.nextcloud.repository.ClientRepository
import com.nextcloud.ui.fileactions.FileAction
import com.nextcloud.ui.fileactions.FileActionHandler
Expand Down Expand Up @@ -136,9 +136,6 @@ open class OCFileListFragment :
@Inject
lateinit var clientRepository: ClientRepository

@Inject
lateinit var throttler: Throttler

@Inject
lateinit var themeUtils: ThemeUtils

Expand Down Expand Up @@ -217,6 +214,8 @@ open class OCFileListFragment :
private val multiChoiceModeListener by lazy { OCFileListMultiChoiceModeListener(this) }
private val mainHandler = Handler(Looper.getMainLooper())

private val actionsMenuThrottle = Throttle()

private val activeActionMode: ActionMode?
get() = multiChoiceModeListener.activeActionMode

Expand Down Expand Up @@ -874,10 +873,14 @@ open class OCFileListFragment :

//region File actions
fun openActionsMenu(filesCount: Int, checkedFiles: Set<OCFile>, isOverflow: Boolean) {
throttler.run(OVERFLOW_CLICK_THROTTLE_KEY) {
val storageManager = requireContainerActivity().storageManager
val accountName = accountManager.user.accountName
actionsMenuThrottle.launch(viewLifecycleOwner.lifecycleScope) {
val actionsToHide = FileAction.getFileListActionsToHide(checkedFiles)
val mimeType = checkedFiles.first().resolveMimeType()
val endpoints = capabilities.getClientIntegrationEndpoints(Type.CONTEXT_MENU, mimeType)
val endpoints = withContext(Dispatchers.IO) {
storageManager.getCapability(accountName).getClientIntegrationEndpoints(Type.CONTEXT_MENU, mimeType)
}
val bottomSheet = FileActionsBottomSheet.newInstance(
filesCount,
checkedFiles,
Expand All @@ -894,7 +897,7 @@ open class OCFileListFragment :
if (currentFolder == null || currentFolder.isRootDirectory) return

val checkedFiles = hashSetOf(currentFolder)
throttler.run(OVERFLOW_CLICK_THROTTLE_KEY) {
actionsMenuThrottle.run {
val actionsToHide = FileAction.getCurrentFolderActionsToHide(currentFolder)
val bottomSheet = FileActionsBottomSheet.newInstance(currentFolder, true, actionsToHide)
showActionsBottomSheet(bottomSheet, checkedFiles)
Expand Down Expand Up @@ -1527,7 +1530,6 @@ open class OCFileListFragment :
private const val DIALOG_CREATE_FOLDER = "DIALOG_CREATE_FOLDER"
private const val DIALOG_CREATE_DOCUMENT = "DIALOG_CREATE_DOCUMENT"
private const val ACTIONS_BOTTOM_SHEET_TAG = "actions"
private const val OVERFLOW_CLICK_THROTTLE_KEY = "overflowClick"

private const val ALL_MIME_TYPES = "*/*"
private const val SCAN_TO_PDF_ACTION = "org.fairscan.app.action.SCAN_TO_PDF"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.nextcloud.android.lib.resources.clientintegration.Endpoint
import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.jobs.upload.FileUploadWorker
import com.nextcloud.client.utils.IntentUtil
import com.nextcloud.client.utils.Throttler
import com.nextcloud.client.utils.Throttle
import com.nextcloud.repository.ClientRepository
import com.nextcloud.ui.albumItemActions.AlbumItemActionsBottomSheet
import com.nextcloud.ui.fileactions.FileActionHandler
Expand Down Expand Up @@ -128,8 +129,9 @@ class AlbumItemsFragment :
@Inject
lateinit var clientRepository: ClientRepository

@Inject
lateinit var throttler: Throttler
private val addMediaThrottle = Throttle()
private val actionsMenuThrottle = Throttle()
private val sharingSheetThrottle = Throttle()

@Inject
lateinit var thumbnailGenerator: ThumbnailGenerator
Expand Down Expand Up @@ -496,7 +498,7 @@ class AlbumItemsFragment :
}

private fun openAddMediaMenu() {
throttler.run(THROTTLE_ADD_MEDIA) {
addMediaThrottle.run {
val fragmentManager = requireActivity().supportFragmentManager

AlbumItemActionsBottomSheet.newInstance()
Expand Down Expand Up @@ -596,15 +598,17 @@ class AlbumItemsFragment :

//region File actions
private fun openActionsMenu(filesCount: Int, checkedFiles: Set<OCFile>) {
throttler.run(THROTTLE_OVERFLOW_CLICK) {
actionsMenuThrottle.launch(viewLifecycleOwner.lifecycleScope) {
val actionsSheet = FileActionsBottomSheet.newInstance(
filesCount,
checkedFiles,
true,
hiddenFileActions(checkedFiles),
false,
contextMenuEndpoints(checkedFiles)
).setResultListener(childFragmentManager, this) { id -> onFileActionChosen(id, checkedFiles) }
).setResultListener(childFragmentManager, this@AlbumItemsFragment) { id ->
onFileActionChosen(id, checkedFiles)
}

if (isDialogFragmentReady()) {
actionsSheet.show(childFragmentManager, TAG_FILE_ACTIONS)
Expand All @@ -622,11 +626,15 @@ class AlbumItemsFragment :
return hidden
}

private fun contextMenuEndpoints(checkedFiles: Set<OCFile>) = containerActivity
?.storageManager
?.getCapability(optionalUser?.get())
?.getClientIntegrationEndpoints(Type.CONTEXT_MENU, checkedFiles.first().mimeType)
.orEmpty()
private suspend fun contextMenuEndpoints(checkedFiles: Set<OCFile>): List<Endpoint> {
val storageManager = containerActivity?.storageManager ?: return emptyList()
val user = optionalUser?.get()
val mimeType = checkedFiles.first().mimeType

return withContext(Dispatchers.IO) {
storageManager.getCapability(user).getClientIntegrationEndpoints(Type.CONTEXT_MENU, mimeType)
}
}

private fun onFileActionChosen(@IdRes itemId: Int, checkedFiles: Set<OCFile>) {
if (checkedFiles.isEmpty()) return
Expand Down Expand Up @@ -828,7 +836,7 @@ class AlbumItemsFragment :

//region Album sharing
private fun openAlbumSharingBottomSheet() {
throttler.run(THROTTLE_SHARING_SHEET) {
sharingSheetThrottle.run {
val album = photoAlbumEntry ?: return@run
val fragmentManager = requireActivity().supportFragmentManager

Expand Down Expand Up @@ -883,10 +891,6 @@ class AlbumItemsFragment :
private const val TAG_ALBUM_SHARING = "album_sharing_sheet"
private const val TAG_FILE_ACTIONS = "actions"

private const val THROTTLE_ADD_MEDIA = "addMediaClick"
private const val THROTTLE_SHARING_SHEET = "albumSharingSheet"
private const val THROTTLE_OVERFLOW_CLICK = "overflowClick"

private const val ALBUMS_REMOTE_PATH = "/albums/"
private const val ANY_MIME_TYPE = "*/*"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.client.utils.Throttler
import com.nextcloud.client.utils.Throttle
import com.nextcloud.utils.extensions.getTypedActivity
import com.nextcloud.utils.extensions.isLandscape
import com.nextcloud.utils.extensions.setVisibleIf
Expand Down Expand Up @@ -77,8 +77,7 @@ class AlbumsFragment :
@Inject
lateinit var thumbnailGenerator: ThumbnailGenerator

@Inject
lateinit var throttler: Throttler
private val createAlbumThrottle = Throttle()

private var containerActivity: FileFragment.ContainerActivity? = null

Expand Down Expand Up @@ -142,7 +141,7 @@ class AlbumsFragment :
}

private fun showCreateAlbumDialog() {
throttler.run("onCreateAlbumClick") {
createAlbumThrottle.run {
val fragment = requireActivity().supportFragmentManager.findFragmentByTag(CreateAlbumDialogFragment.TAG)
if (fragment == null) {
CreateAlbumDialogFragment.newInstance()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ import com.nextcloud.client.jobs.upload.FileUploadEventBroadcaster
import com.nextcloud.client.jobs.upload.FileUploadHelper
import com.nextcloud.client.network.ConnectivityService
import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.client.utils.Throttler
import com.nextcloud.client.utils.TrailingThrottle
import com.nextcloud.ui.component.UploadWarningCard
import com.nextcloud.utils.extensions.collectWhenStarted
import com.nextcloud.utils.extensions.getParcelableArgument
import com.nextcloud.utils.extensions.getTypedActivity
import com.nextcloud.utils.thumbnail.ThumbnailGenerator
Expand Down Expand Up @@ -64,7 +65,7 @@ class UploadListFragment :

@Inject lateinit var localBroadcastManager: LocalBroadcastManager

@Inject lateinit var throttler: Throttler
private val uploadListRefresh = TrailingThrottle(ONE_SECOND)

@Inject lateinit var uploadFileOperationFactory: UploadFileOperationFactory

Expand Down Expand Up @@ -98,19 +99,16 @@ class UploadListFragment :

private val uploadFinishReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == FileUploadEventBroadcaster.ACTION_UPLOAD_COMPLETED) {
uploadListAdapter.loadUploadItemsFromDb()
return
when (intent?.action) {
FileUploadEventBroadcaster.ACTION_UPLOAD_COMPLETED -> uploadListAdapter.loadUploadItemsFromDb()
else -> uploadListRefresh.request()
}

throttler.run(UPDATE_UPLOAD_LIST_KEY) { uploadListAdapter.loadUploadItemsFromDb() }
}
}

// region Lifecycle
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
throttler.intervalMillis = ONE_SECOND.inWholeMilliseconds
if (savedInstanceState == null) {
setUserFromIntent()
}
Expand All @@ -130,6 +128,7 @@ class UploadListFragment :
setupWarningCard(activity, binding)
setupEmptyList(binding)
setupList(activity, binding)
viewLifecycleOwner.collectWhenStarted(uploadListRefresh.events) { uploadListAdapter.loadUploadItemsFromDb() }
addMenuProvider()
}

Expand Down Expand Up @@ -296,6 +295,5 @@ class UploadListFragment :
companion object {
private const val SPAN_COUNT = 1
private const val EMPTY_LIST_ICON_ALPHA = 0.5f
private const val UPDATE_UPLOAD_LIST_KEY = "update_upload_list"
}
}
Loading
Loading