From 468e208a701917603f3adb8224203571c3bba938 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 8 Jul 2026 10:38:27 +0200 Subject: [PATCH 1/2] feat(gallery): pagination Signed-off-by: alperozturk96 --- .../nextcloud/client/database/dao/FileDao.kt | 17 ++++++++ .../FileDataStorageManagerExtensions.kt | 41 +++++++++++++++++-- .../android/ui/fragment/GalleryFragment.kt | 41 +++++++++++-------- 3 files changed, 80 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index f8d5f3855c30..164df5698362 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -65,6 +65,23 @@ interface FileDao { ) suspend fun getGalleryItemsSuspended(startDate: Long, endDate: Long, fileOwner: String): List + @Query( + "SELECT * FROM filelist" + + " WHERE (content_type LIKE 'image/%' OR content_type LIKE 'video/%')" + + " AND file_owner = :fileOwner" + + " AND path LIKE :pathPrefix || '%'" + + " AND (:mimeFilter IS NULL OR content_type LIKE :mimeFilter)" + + " ORDER BY modified DESC" + + " LIMIT :limit OFFSET :offset" + ) + suspend fun getGalleryItemsPageSuspended( + fileOwner: String, + pathPrefix: String, + mimeFilter: String?, + limit: Int, + offset: Int + ): List + @Query("SELECT * FROM filelist WHERE file_owner = :fileOwner ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}") fun getAllFiles(fileOwner: String): List diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt index e163c5a861a8..2cfab09afcfe 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt @@ -25,9 +25,44 @@ suspend fun FileDataStorageManager.saveShares(shares: List, accountName } } -suspend fun FileDataStorageManager.getAllGalleryItemsSuspended(): List { - val fileEntities = fileDao.getGalleryItemsSuspended(0, Long.MAX_VALUE, user.accountName) - return fileEntities.map { createFileInstance(it) } +private const val GALLERY_DB_CHUNK_SIZE = 500 + +/** + * Loads gallery items ordered by modification date (newest first), bounded to [limit] entries. + * + * The database is read in chunks of [GALLERY_DB_CHUNK_SIZE] rows so that a single query never + * materializes a result set larger than the Android [android.database.CursorWindow] can hold, + * which otherwise crashes on very large galleries. + * + * @param pathPrefix only items whose remote path starts with this prefix are returned ("/" matches all) + * @param mimeFilter optional content-type LIKE pattern (e.g. "image/%" or "video/%"); null keeps images and videos + * @param limit maximum number of items to load + */ +suspend fun FileDataStorageManager.getGalleryItemsPageSuspended( + pathPrefix: String, + mimeFilter: String?, + limit: Int +): List { + val result = ArrayList(minOf(limit, GALLERY_DB_CHUNK_SIZE)) + var offset = 0 + + while (offset < limit) { + val chunkLimit = minOf(GALLERY_DB_CHUNK_SIZE, limit - offset) + val entities = fileDao.getGalleryItemsPageSuspended( + user.accountName, + pathPrefix, + mimeFilter, + chunkLimit, + offset + ) + + entities.mapTo(result) { createFileInstance(it) } + offset += entities.size + + if (entities.size < chunkLimit) break + } + + return result } fun FileDataStorageManager.searchFilesByName(file: OCFile, accountName: String, query: String): List = diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt index 1ab8e8334dde..5077ea746186 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 TSI-mc * SPDX-FileCopyrightText: 2019 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Nextcloud GmbH @@ -28,7 +29,7 @@ import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.nextcloud.utils.extensions.getAllGalleryItemsSuspended +import com.nextcloud.utils.extensions.getGalleryItemsPageSuspended import com.nextcloud.utils.extensions.getParcelableArgument import kotlinx.coroutines.Job import com.nextcloud.utils.extensions.getTypedActivity @@ -47,7 +48,6 @@ import com.owncloud.android.ui.adapter.GalleryAdapter import com.owncloud.android.ui.asynctasks.GallerySearchTask import com.owncloud.android.ui.events.ChangeMenuEvent import com.owncloud.android.ui.fragment.GalleryFragmentBottomSheetDialog.MediaState -import com.owncloud.android.utils.MimeTypeUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -61,6 +61,7 @@ class GalleryFragment : private var showGalleryJob: Job? = null private var endDate: Long = 0 private val limit = 150 + private var loadedItemCount = INITIAL_GALLERY_WINDOW private var adapter: GalleryAdapter? = null private var bottomSheet: GalleryFragmentBottomSheetDialog? = null @@ -241,6 +242,8 @@ class GalleryFragment : prepareCurrentSearch(searchEvent) setEmptyListMessage(EmptyListState.LOADING) + loadedItemCount = INITIAL_GALLERY_WINDOW + // always show first stored items showAllGalleryItems() @@ -367,8 +370,10 @@ class GalleryFragment : Log_OC.d(this, "Gallery swipe: retrieve items because end of gallery display") } - // Almost reached the end, continue to load new photos + // Almost reached the end, widen the display window and continue to load new photos endDate = lastItemTimestamp + loadedItemCount += GALLERY_WINDOW_INCREMENT + showAllGalleryItems() isPhotoSearchQueryRunning = true runGallerySearchTask() } @@ -381,23 +386,22 @@ class GalleryFragment : fun showAllGalleryItems() { val mediaState = bottomSheet?.currMediaState ?: return + val mimeFilter = when (mediaState) { + MediaState.MEDIA_STATE_PHOTOS_ONLY -> IMAGE_MIME_FILTER + MediaState.MEDIA_STATE_VIDEOS_ONLY -> VIDEO_MIME_FILTER + else -> null + } + showGalleryJob?.cancel() showGalleryJob = lifecycleScope.launch(Dispatchers.Default) { val remotePath = preferences.getLastSelectedMediaFolder() - val items = mContainerActivity.storageManager.getAllGalleryItemsSuspended() + val items = mContainerActivity.storageManager.getGalleryItemsPageSuspended( + remotePath, + mimeFilter, + loadedItemCount + ) - val isPhotosOnly = mediaState == MediaState.MEDIA_STATE_PHOTOS_ONLY - val isVideosOnly = mediaState == MediaState.MEDIA_STATE_VIDEOS_ONLY - - val filteredItems = items.filter { - if (!it.remotePath.startsWith(remotePath)) return@filter false - if (isPhotosOnly) return@filter MimeTypeUtil.isImage(it.mimeType) - if (isVideosOnly) return@filter MimeTypeUtil.isVideo(it.mimeType) - true - } - - val galleryItems = - filteredItems.toGalleryItems(columnsCount, ThumbnailsCacheManager.getThumbnailDimension()) + val galleryItems = items.toGalleryItems(columnsCount, ThumbnailsCacheManager.getThumbnailDimension()) withContext(Dispatchers.Main) { if (galleryItems.isEmpty()) { @@ -448,6 +452,11 @@ class GalleryFragment : private const val MAX_ITEMS_PER_ROW = 10 private const val FRAGMENT_TAG_BOTTOM_SHEET = "data" + private const val INITIAL_GALLERY_WINDOW = 500 + private const val GALLERY_WINDOW_INCREMENT = 500 + private const val IMAGE_MIME_FILTER = "image/%" + private const val VIDEO_MIME_FILTER = "video/%" + private var lastMediaItemPosition: Int? = null const val REFRESH_SEARCH_EVENT_RECEIVER: String = "refreshSearchEventReceiver" From 2d34b6f21ab162238f0ef660160f20511be89839 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 15 Jul 2026 13:03:19 +0200 Subject: [PATCH 2/2] wip Signed-off-by: alperozturk96 --- .../extensions/FileDataStorageManagerExtensions.kt | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt index 2cfab09afcfe..403736d83333 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt @@ -27,17 +27,6 @@ suspend fun FileDataStorageManager.saveShares(shares: List, accountName private const val GALLERY_DB_CHUNK_SIZE = 500 -/** - * Loads gallery items ordered by modification date (newest first), bounded to [limit] entries. - * - * The database is read in chunks of [GALLERY_DB_CHUNK_SIZE] rows so that a single query never - * materializes a result set larger than the Android [android.database.CursorWindow] can hold, - * which otherwise crashes on very large galleries. - * - * @param pathPrefix only items whose remote path starts with this prefix are returned ("/" matches all) - * @param mimeFilter optional content-type LIKE pattern (e.g. "image/%" or "video/%"); null keeps images and videos - * @param limit maximum number of items to load - */ suspend fun FileDataStorageManager.getGalleryItemsPageSuspended( pathPrefix: String, mimeFilter: String?,