Skip to content
Open
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
17 changes: 17 additions & 0 deletions app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,23 @@ interface FileDao {
)
suspend fun getGalleryItemsSuspended(startDate: Long, endDate: Long, fileOwner: String): List<FileEntity>

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

@Query("SELECT * FROM filelist WHERE file_owner = :fileOwner ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}")
fun getAllFiles(fileOwner: String): List<FileEntity>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,33 @@ suspend fun FileDataStorageManager.saveShares(shares: List<OCShare>, accountName
}
}

suspend fun FileDataStorageManager.getAllGalleryItemsSuspended(): List<OCFile> {
val fileEntities = fileDao.getGalleryItemsSuspended(0, Long.MAX_VALUE, user.accountName)
return fileEntities.map { createFileInstance(it) }
private const val GALLERY_DB_CHUNK_SIZE = 500

suspend fun FileDataStorageManager.getGalleryItemsPageSuspended(
pathPrefix: String,
mimeFilter: String?,
limit: Int
): List<OCFile> {
val result = ArrayList<OCFile>(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<OCFile> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ private void openFavoritesTab() {
}

private void openMediaTab(int menuItemId) {
GalleryFragment.Companion.setLastMediaItemPosition(null);
GalleryFragment.Companion.clearSavedScrollState();
resetOnlyPersonalAndOnDevice();
setupToolbar();
startPhotoSearch(menuItemId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import com.owncloud.android.datamodel.ThumbnailsCacheManager
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.ui.activity.ComponentsGetter
import com.owncloud.android.ui.activity.FolderPickerActivity
import com.owncloud.android.ui.fragment.GalleryFragment
import com.owncloud.android.ui.fragment.SearchType
import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface
import com.owncloud.android.utils.DisplayUtils
Expand Down Expand Up @@ -166,7 +165,6 @@ class OCFileListDelegate(

imageView.setOnClickListener {
ocFileListFragmentInterface.onItemClicked(file)
GalleryFragment.setLastMediaItemPosition(galleryRowHolder.absoluteAdapterPosition)
}

if (!hideItemOptions) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2023 TSI-mc
* SPDX-FileCopyrightText: 2019 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH
Expand All @@ -14,6 +15,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
Expand All @@ -28,7 +30,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
Expand All @@ -47,7 +49,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
Expand All @@ -61,6 +62,8 @@ class GalleryFragment :
private var showGalleryJob: Job? = null
private var endDate: Long = 0
private val limit = 150
private var loadedItemCount = INITIAL_GALLERY_WINDOW
private var restoreScrollPending = false
private var adapter: GalleryAdapter? = null

private var bottomSheet: GalleryFragmentBottomSheetDialog? = null
Expand Down Expand Up @@ -148,6 +151,8 @@ class GalleryFragment :
override fun onPause() {
super.onPause()
photoSearchTask?.cancel()
savedScrollState = recyclerView?.layoutManager?.onSaveInstanceState()
savedLoadedItemCount = loadedItemCount
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Expand All @@ -173,6 +178,10 @@ class GalleryFragment :

updateSubtitle(bottomSheet?.currMediaState)

// Restore the previously loaded window so a saved scroll position still resolves to a valid item.
loadedItemCount = savedLoadedItemCount ?: INITIAL_GALLERY_WINDOW
restoreScrollPending = savedScrollState != null

handleSearchEvent()
}

Expand All @@ -198,11 +207,6 @@ class GalleryFragment :
val layoutManager = GridLayoutManager(context, 1)
adapter?.setLayoutManager(layoutManager)
recyclerView?.setLayoutManager(layoutManager)
recyclerView?.post {
lastMediaItemPosition?.let { position ->
recyclerView?.layoutManager?.scrollToPosition(position)
}
}
}

override fun onConfigurationChanged(newConfig: Configuration) {
Expand Down Expand Up @@ -308,6 +312,9 @@ class GalleryFragment :

private fun searchAndDisplayAfterChangingFolder() {
// TODO: Fix folder change, it seems it doesn't work at all
loadedItemCount = INITIAL_GALLERY_WINDOW
restoreScrollPending = false
clearSavedScrollState()
endDate = System.currentTimeMillis() / 1000
isPhotoSearchQueryRunning = true
runGallerySearchTask()
Expand Down Expand Up @@ -367,44 +374,56 @@ 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()
}
}

override fun updateMediaContent(mediaState: MediaState) {
loadedItemCount = INITIAL_GALLERY_WINDOW
restoreScrollPending = false
clearSavedScrollState()
showAllGalleryItems()
}

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 isPhotosOnly = mediaState == MediaState.MEDIA_STATE_PHOTOS_ONLY
val isVideosOnly = mediaState == MediaState.MEDIA_STATE_VIDEOS_ONLY
val items = mContainerActivity.storageManager.getGalleryItemsPageSuspended(
remotePath,
mimeFilter,
loadedItemCount
)

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()) {
setEmptyListMessage(SearchType.GALLERY_SEARCH)
}
adapter?.updateList(galleryItems)
updateSubtitle(mediaState)

if (restoreScrollPending && galleryItems.isNotEmpty()) {
restoreScrollPending = false
savedScrollState?.let { state ->
savedScrollState = null
recyclerView?.layoutManager?.onRestoreInstanceState(state)
}
}
}
}
}
Expand Down Expand Up @@ -448,14 +467,24 @@ class GalleryFragment :
private const val MAX_ITEMS_PER_ROW = 10
private const val FRAGMENT_TAG_BOTTOM_SHEET = "data"

private var lastMediaItemPosition: Int? = null
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/%"

const val REFRESH_SEARCH_EVENT_RECEIVER: String = "refreshSearchEventReceiver"

private const val MAX_LANDSCAPE_COLUMN_SIZE = 5
private const val MAX_PORTRAIT_COLUMN_SIZE = 2

fun setLastMediaItemPosition(position: Int?) {
lastMediaItemPosition = position
// Kept across the activity recreation that happens when returning from the media preview,
// so the grid reopens at the same scroll position and with the same loaded window.
private var savedScrollState: Parcelable? = null
private var savedLoadedItemCount: Int? = null

fun clearSavedScrollState() {
savedScrollState = null
savedLoadedItemCount = null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
import static com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment.SETUP_ENCRYPTION_DIALOG_TAG;
import static com.owncloud.android.ui.fragment.SearchType.FAVORITE_SEARCH;
import static com.owncloud.android.ui.fragment.SearchType.FILE_SEARCH;
import static com.owncloud.android.ui.fragment.SearchType.GALLERY_SEARCH;
import static com.owncloud.android.ui.fragment.SearchType.NO_SEARCH;
import static com.owncloud.android.ui.fragment.SearchType.RECENT_FILES_SEARCH;
import static com.owncloud.android.ui.fragment.SearchType.SHARED_FILTER;
Expand Down Expand Up @@ -1196,7 +1197,7 @@ private void fileOnItemClick(OCFile file) {
return;
}

if (PreviewImageFragment.canBePreviewed(file) && mContainerActivity instanceof FileDisplayActivity fda) {
if (canPreviewInVirtualFolderPager(file) && mContainerActivity instanceof FileDisplayActivity fda) {
fda.previewImageWithSearchContext(file, searchFragment, currentSearchType);
} else if (file.isDown() && mContainerActivity instanceof FileDisplayActivity fda) {
fda.previewFile(file, this::setFabVisible);
Expand All @@ -1205,6 +1206,20 @@ private void fileOnItemClick(OCFile file) {
}
}

/**
* In a gallery or favorites search the preview pager is built from the whole virtual folder, so a directly
* tapped video must open through the same pager as the images.
*/
private boolean canPreviewInVirtualFolderPager(OCFile file) {
if (PreviewImageFragment.canBePreviewed(file)) {
return true;
}

boolean virtualFolderSearch = searchFragment
&& (currentSearchType == GALLERY_SEARCH || currentSearchType == FAVORITE_SEARCH);
return virtualFolderSearch && MimeTypeUtil.isVideo(file);
}

private void handlePendingDownloadFile(OCFile file) {
if (!isAccountManagerInitialized()) {
Log_OC.e(TAG, "AccountManager not yet initialized");
Expand Down
Loading