Skip to content

Commit eed4cbe

Browse files
Merge pull request #17474 from nextcloud/fix/search-encrypted-files-endless-indicator
fix(unified-search): handle slow server search for loader spinner
2 parents 4a80d44 + dd68a06 commit eed4cbe

9 files changed

Lines changed: 108 additions & 55 deletions

File tree

app/src/androidTest/java/com/owncloud/android/ui/adapter/UnifiedSearchListAdapterIT.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.owncloud.android.ui.interfaces.UnifiedSearchCurrentDirItemAction
2121
import com.owncloud.android.ui.interfaces.UnifiedSearchListInterface
2222
import com.owncloud.android.ui.unifiedsearch.ProviderID
2323
import com.owncloud.android.ui.unifiedsearch.UnifiedSearchSection
24+
import com.owncloud.android.ui.unifiedsearch.toUnifiedSearchEntry
2425
import com.owncloud.android.utils.MimeType
2526
import com.owncloud.android.utils.ScreenshotTest
2627
import com.nextcloud.utils.thumbnail.FileThumbnailGenerator
@@ -101,7 +102,7 @@ class UnifiedSearchListAdapterIT : AbstractIT() {
101102
UnifiedSearchSection(
102103
providerID = name.lowercase().replace(" ", "_"),
103104
name = name,
104-
entries = entries,
105+
entries = entries.map { it.toUnifiedSearchEntry(storageManager) },
105106
hasMoreResults = false
106107
)
107108
}
@@ -111,7 +112,7 @@ class UnifiedSearchListAdapterIT : AbstractIT() {
111112
UnifiedSearchSection(
112113
providerID = name.lowercase().replace(" ", "_"),
113114
name = name,
114-
entries = entries,
115+
entries = entries.map { it.toUnifiedSearchEntry(storageManager) },
115116
hasMoreResults = true
116117
)
117118
}
@@ -164,12 +165,11 @@ class UnifiedSearchListAdapterIT : AbstractIT() {
164165
onClientReady: (com.nextcloud.common.NextcloudClient) -> Unit
165166
) = Unit
166167
},
167-
user = sut.user.get(),
168168
context = sut,
169169
viewThemeUtils = sut.viewThemeUtils,
170-
appPreferences = preferences,
171170
currentDirItemAction = noopCurrentDirAction,
172-
thumbnailGenerator = thumbnailGenerator
171+
thumbnailGenerator = thumbnailGenerator,
172+
user = sut.user.get()
173173
)
174174

175175
adapter.shouldShowFooters(true)

app/src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentIT.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import com.owncloud.android.AbstractIT
1919
import com.owncloud.android.datamodel.OCFile
2020
import com.owncloud.android.lib.common.SearchResultEntry
2121
import com.owncloud.android.ui.unifiedsearch.UnifiedSearchSection
22+
import com.owncloud.android.ui.unifiedsearch.toUnifiedSearchEntry
2223
import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel
2324
import org.junit.Test
2425
import java.io.File
@@ -47,7 +48,7 @@ class UnifiedSearchFragmentIT : AbstractIT() {
4748
"http://localhost/nc/index.php/apps/files/?dir=/Files&scrollto=Test",
4849
"icon",
4950
false
50-
)
51+
).toUnifiedSearchEntry(storageManager)
5152
),
5253
hasMoreResults = false
5354
)

app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,37 +15,33 @@ import androidx.core.widget.ImageViewCompat
1515
import com.afollestad.sectionedrecyclerview.SectionedViewHolder
1616
import com.bumptech.glide.Glide
1717
import com.nextcloud.android.common.ui.theme.utils.ColorRole
18-
import com.nextcloud.client.account.User
19-
import com.nextcloud.client.preferences.AppPreferences
2018
import com.nextcloud.common.NextcloudClient
2119
import com.nextcloud.model.SearchResultEntryType
2220
import com.nextcloud.utils.CalendarEventManager
2321
import com.nextcloud.utils.ContactManager
2422
import com.nextcloud.utils.GlideHelper
2523
import com.nextcloud.utils.extensions.getType
2624
import com.nextcloud.utils.extensions.setVisibleIf
25+
import com.nextcloud.utils.thumbnail.ThumbnailGenerator
2726
import com.owncloud.android.R
2827
import com.owncloud.android.databinding.UnifiedSearchItemBinding
29-
import com.owncloud.android.datamodel.FileDataStorageManager
3028
import com.owncloud.android.datamodel.OCFile
3129
import com.owncloud.android.lib.common.SearchResultEntry
3230
import com.owncloud.android.ui.interfaces.UnifiedSearchListInterface
31+
import com.owncloud.android.ui.unifiedsearch.UnifiedSearchEntry
3332
import com.owncloud.android.utils.MimeTypeUtil
34-
import com.nextcloud.utils.thumbnail.ThumbnailGenerator
3533
import com.owncloud.android.utils.theme.ViewThemeUtils
3634

3735
@Suppress("LongParameterList")
3836
class UnifiedSearchItemViewHolder(
3937
private val supportsOpeningCalendarContactsLocally: Boolean,
4038
val binding: UnifiedSearchItemBinding,
41-
private val storageManager: FileDataStorageManager,
4239
private val listInterface: UnifiedSearchListInterface,
4340
private val filesAction: FilesAction,
4441
val context: Context,
4542
private val viewThemeUtils: ViewThemeUtils,
4643
private val thumbnailGenerator: ThumbnailGenerator,
47-
private val user: User,
48-
private val preferences: AppPreferences
44+
private val isE2EEActivate: Boolean
4945
) : SectionedViewHolder(binding.root) {
5046

5147
interface FilesAction {
@@ -56,14 +52,17 @@ class UnifiedSearchItemViewHolder(
5652
private val contactManager = ContactManager(context)
5753
private val calendarEventManager = CalendarEventManager(context)
5854

59-
fun bind(entry: SearchResultEntry) {
55+
fun bind(unifiedSearchEntry: UnifiedSearchEntry) {
56+
val entry = unifiedSearchEntry.searchResult
57+
val file = unifiedSearchEntry.localFile
58+
6059
bindTextView(binding.title, entry.title)
6160
bindTextView(binding.subline, entry.subline)
62-
bindLocalFileIndicator(entry)
61+
bindLocalFileIndicator(entry, file)
6362

6463
val entryType = entry.getType()
65-
bindThumbnail(entry, entryType)
66-
bindMoreButton(entry)
64+
bindThumbnail(entry, file, entryType)
65+
bindMoreButton(entry, file)
6766
binding.unifiedSearchItemLayout.setOnClickListener {
6867
searchEntryOnClick(entry, entryType)
6968
}
@@ -78,14 +77,11 @@ class UnifiedSearchItemViewHolder(
7877
}
7978
}
8079

81-
private fun bindLocalFileIndicator(entry: SearchResultEntry) {
82-
val showLocalFileIndicator =
83-
(entry.isFile && storageManager.getFileByDecryptedRemotePath(entry.remotePath()) != null)
84-
binding.localFileIndicator.setVisibleIf(showLocalFileIndicator)
80+
private fun bindLocalFileIndicator(entry: SearchResultEntry, file: OCFile?) {
81+
binding.localFileIndicator.setVisibleIf(entry.isFile && file != null)
8582
}
8683

87-
private fun bindThumbnail(entry: SearchResultEntry, entryType: SearchResultEntryType) {
88-
val file = storageManager.getFileByRemotePath(entry.remotePath())
84+
private fun bindThumbnail(entry: SearchResultEntry, file: OCFile?, entryType: SearchResultEntryType) {
8985
Glide.with(context).clear(binding.thumbnail)
9086
binding.thumbnailOverlayIcon.setVisibleIf(false)
9187

@@ -148,14 +144,12 @@ class UnifiedSearchItemViewHolder(
148144
}
149145
}
150146

151-
private fun bindMoreButton(entry: SearchResultEntry) {
152-
if (entry.isFile) {
153-
binding.more.visibility = View.VISIBLE
154-
binding.more.setOnClickListener {
155-
filesAction.showFilesAction(entry)
156-
}
157-
} else {
158-
binding.more.visibility = View.GONE
147+
private fun bindMoreButton(entry: SearchResultEntry, file: OCFile?) {
148+
val isEncryptedWithoutKeys = file?.isEncrypted == true && !isE2EEActivate
149+
binding.more.setVisibleIf(entry.isFile && !isEncryptedWithoutKeys)
150+
151+
binding.more.setOnClickListener {
152+
filesAction.showFilesAction(entry)
159153
}
160154
}
161155

app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchListAdapter.kt

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter
1818
import com.afollestad.sectionedrecyclerview.SectionedViewHolder
1919
import com.bumptech.glide.Glide
2020
import com.nextcloud.client.account.User
21-
import com.nextcloud.client.preferences.AppPreferences
21+
import com.nextcloud.utils.thumbnail.ThumbnailGenerator
2222
import com.owncloud.android.R
2323
import com.owncloud.android.databinding.UnifiedSearchCurrentDirectoryItemBinding
2424
import com.owncloud.android.databinding.UnifiedSearchEmptyBinding
@@ -28,11 +28,11 @@ import com.owncloud.android.databinding.UnifiedSearchItemBinding
2828
import com.owncloud.android.datamodel.FileDataStorageManager
2929
import com.owncloud.android.datamodel.OCFile
3030
import com.owncloud.android.datamodel.ThumbnailsCacheManager
31+
import com.owncloud.android.ui.helpers.FileOperationsHelper
3132
import com.owncloud.android.ui.interfaces.UnifiedSearchCurrentDirItemAction
3233
import com.owncloud.android.ui.interfaces.UnifiedSearchListInterface
3334
import com.owncloud.android.ui.unifiedsearch.UnifiedSearchSection
3435
import com.owncloud.android.utils.DisplayUtils
35-
import com.nextcloud.utils.thumbnail.ThumbnailGenerator
3636
import com.owncloud.android.utils.theme.ViewThemeUtils
3737

3838
/**
@@ -44,12 +44,11 @@ class UnifiedSearchListAdapter(
4444
private val storageManager: FileDataStorageManager,
4545
private val listInterface: UnifiedSearchListInterface,
4646
private val filesAction: UnifiedSearchItemViewHolder.FilesAction,
47-
private val user: User,
4847
private val context: Context,
4948
private val viewThemeUtils: ViewThemeUtils,
50-
private val appPreferences: AppPreferences,
5149
private val currentDirItemAction: UnifiedSearchCurrentDirItemAction,
52-
private val thumbnailGenerator: ThumbnailGenerator
50+
private val thumbnailGenerator: ThumbnailGenerator,
51+
private val user: User
5352
) : SectionedRecyclerViewAdapter<SectionedViewHolder>() {
5453
companion object {
5554
private const val VIEW_TYPE_EMPTY = Int.MAX_VALUE
@@ -77,7 +76,7 @@ class UnifiedSearchListAdapter(
7776
val index = getSectionIndex(section)
7877
sections.getOrNull(index)
7978
?.entries
80-
?.getOrNull(position)?.hashCode()?.toLong() ?: RecyclerView.NO_ID
79+
?.getOrNull(position)?.searchResult?.hashCode()?.toLong() ?: RecyclerView.NO_ID
8180
}
8281
}
8382

@@ -132,14 +131,12 @@ class UnifiedSearchListAdapter(
132131
UnifiedSearchItemViewHolder(
133132
supportsOpeningCalendarContactsLocally,
134133
binding,
135-
storageManager,
136134
listInterface,
137135
filesAction,
138136
context,
139137
viewThemeUtils,
140138
thumbnailGenerator,
141-
user,
142-
appPreferences
139+
FileOperationsHelper.isEndToEndEncryptionSetup(context, user)
143140
)
144141
}
145142

app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import com.nextcloud.utils.extensions.getTypedActivity
4141
import com.nextcloud.utils.extensions.searchFilesByName
4242
import com.nextcloud.utils.extensions.setVisibleIf
4343
import com.nextcloud.utils.extensions.typedActivity
44+
import com.nextcloud.utils.thumbnail.ThumbnailGenerator
4445
import com.owncloud.android.R
4546
import com.owncloud.android.databinding.ListFragmentBinding
4647
import com.owncloud.android.datamodel.FileDataStorageManager
@@ -62,12 +63,14 @@ import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel
6263
import com.owncloud.android.ui.unifiedsearch.filterOutHiddenFiles
6364
import com.owncloud.android.utils.DisplayUtils
6465
import com.owncloud.android.utils.PermissionUtil
65-
import com.nextcloud.utils.thumbnail.ThumbnailGenerator
6666
import com.owncloud.android.utils.theme.ViewThemeUtils
6767
import kotlinx.coroutines.Dispatchers
68+
import kotlinx.coroutines.Job
69+
import kotlinx.coroutines.delay
6870
import kotlinx.coroutines.launch
6971
import kotlinx.coroutines.withContext
7072
import javax.inject.Inject
73+
import kotlin.time.Duration.Companion.milliseconds
7174

7275
/**
7376
* Starts query to all capable unified search providers and displays them Opens result in our app, redirect to other
@@ -93,6 +96,7 @@ class UnifiedSearchFragment :
9396
private const val ARG_QUERY = "ARG_QUERY"
9497
private const val ARG_HIDDEN_FILES = "ARG_HIDDEN_FILES"
9598
private const val CURRENT_DIR_PATH = "CURRENT_DIR"
99+
private const val SEARCH_TIMEOUT_MS = 30_000L
96100

97101
fun newInstance(
98102
query: String?,
@@ -143,6 +147,7 @@ class UnifiedSearchFragment :
143147
private var showMoreActions = false
144148
private var currentDir: OCFile? = null
145149
private var initialQuery: String? = null
150+
private var searchJob: Job? = null
146151

147152
override fun onCreate(savedInstanceState: Bundle?) {
148153
super.onCreate(savedInstanceState)
@@ -319,6 +324,11 @@ class UnifiedSearchFragment :
319324
vm.searchResults.observe(viewLifecycleOwner, this::onSearchResultChanged)
320325
vm.isLoading.observe(viewLifecycleOwner) { loading ->
321326
binding.swipeContainingList.isRefreshing = loading
327+
if (loading) {
328+
startSearchTimeout()
329+
} else {
330+
searchJob?.cancel()
331+
}
322332
}
323333
vm.screenState.observe(viewLifecycleOwner) {
324334
handleScreenState(it)
@@ -353,6 +363,19 @@ class UnifiedSearchFragment :
353363
}
354364
}
355365

366+
private fun startSearchTimeout() {
367+
searchJob?.cancel()
368+
searchJob = viewLifecycleOwner.lifecycleScope.launch {
369+
delay(SEARCH_TIMEOUT_MS.milliseconds)
370+
val currentBinding = _binding ?: return@launch
371+
currentBinding.swipeContainingList.isRefreshing = false
372+
DisplayUtils.showSnackMessage(
373+
currentBinding.root,
374+
R.string.unified_search_fragment_search_takes_long
375+
)
376+
}
377+
}
378+
356379
private fun setUpBinding() {
357380
binding.swipeContainingList.setOnRefreshListener {
358381
vm.initialQuery()
@@ -377,12 +400,11 @@ class UnifiedSearchFragment :
377400
storageManager,
378401
this@UnifiedSearchFragment,
379402
this@UnifiedSearchFragment,
380-
currentAccountProvider.user,
381403
requireContext(),
382404
viewThemeUtils,
383-
appPreferences,
384405
this@UnifiedSearchFragment,
385-
thumbnailGenerator
406+
thumbnailGenerator,
407+
currentAccountProvider.user
386408
)
387409

388410
adapter.shouldShowFooters(true)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.owncloud.android.ui.unifiedsearch
8+
9+
import com.owncloud.android.datamodel.FileDataStorageManager
10+
import com.owncloud.android.datamodel.OCFile
11+
import com.owncloud.android.lib.common.SearchResultEntry
12+
13+
data class UnifiedSearchEntry(val searchResult: SearchResultEntry, val localFile: OCFile?)
14+
15+
fun SearchResultEntry.toUnifiedSearchEntry(storageManager: FileDataStorageManager): UnifiedSearchEntry =
16+
UnifiedSearchEntry(this, storageManager.getFileByRemotePath(remotePath()))

app/src/main/java/com/owncloud/android/ui/unifiedsearch/UnifiedSearchModel.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,19 @@
77
*/
88
package com.owncloud.android.ui.unifiedsearch
99

10-
import com.owncloud.android.lib.common.SearchResultEntry
11-
1210
typealias ProviderID = String
1311

1412
data class UnifiedSearchSection(
1513
val providerID: ProviderID,
1614
val name: String,
17-
val entries: List<SearchResultEntry>,
15+
val entries: List<UnifiedSearchEntry>,
1816
val hasMoreResults: Boolean
1917
)
2018

2119
fun List<UnifiedSearchSection>.filterOutHiddenFiles(listOfHiddenFiles: List<String>): List<UnifiedSearchSection> =
2220
map { searchSection ->
2321
val entriesWithoutHiddenFiles = searchSection.entries.filterNot { entry ->
24-
listOfHiddenFiles.contains(entry.title)
22+
listOfHiddenFiles.contains(entry.searchResult.title)
2523
}
2624

2725
searchSection.copy(entries = entriesWithoutHiddenFiles)

0 commit comments

Comments
 (0)