Explorar o código

Unified search: transform results in ViewModel and fix "load more" behaviour

Signed-off-by: Álvaro Brey Vilas <alvaro.brey@nextcloud.com>
Álvaro Brey Vilas %!s(int64=3) %!d(string=hai) anos
pai
achega
e898965dc1

+ 2 - 1
src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchFooterViewHolder.kt

@@ -4,7 +4,7 @@
  *
  * @author Álvaro Brey Vilas
  * Copyright (C) 2021 Álvaro Brey Vilas
- * Copyright (C) 2020 Nextcloud GmbH
+ * 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
@@ -25,6 +25,7 @@ import android.content.Context
 import com.afollestad.sectionedrecyclerview.SectionedViewHolder
 import com.owncloud.android.databinding.UnifiedSearchFooterBinding
 import com.owncloud.android.ui.interfaces.UnifiedSearchListInterface
+import com.owncloud.android.ui.unifiedsearch.UnifiedSearchSection
 
 class UnifiedSearchFooterViewHolder(
     val binding: UnifiedSearchFooterBinding,

+ 1 - 0
src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchHeaderViewHolder.kt

@@ -24,6 +24,7 @@ package com.owncloud.android.ui.adapter
 import android.content.Context
 import com.afollestad.sectionedrecyclerview.SectionedViewHolder
 import com.owncloud.android.databinding.UnifiedSearchHeaderBinding
+import com.owncloud.android.ui.unifiedsearch.UnifiedSearchSection
 import com.owncloud.android.utils.theme.ThemeColorUtils
 
 class UnifiedSearchHeaderViewHolder(val binding: UnifiedSearchHeaderBinding, val context: Context) :

+ 7 - 37
src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchListAdapter.kt

@@ -30,7 +30,6 @@ import com.nextcloud.client.account.User
 import com.nextcloud.client.network.ClientFactory
 import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter
 import com.afollestad.sectionedrecyclerview.SectionedViewHolder
-import com.owncloud.android.lib.common.SearchResult
 import android.view.ViewGroup
 import android.view.LayoutInflater
 import android.view.View
@@ -40,24 +39,9 @@ import com.owncloud.android.databinding.UnifiedSearchFooterBinding
 import com.owncloud.android.databinding.UnifiedSearchHeaderBinding
 import com.owncloud.android.databinding.UnifiedSearchItemBinding
 import com.owncloud.android.datamodel.ThumbnailsCacheManager.InitDiskCacheTask
-import com.owncloud.android.ui.unifiedsearch.ProviderID
+import com.owncloud.android.ui.unifiedsearch.UnifiedSearchSection
 import java.lang.IllegalArgumentException
 
-data class UnifiedSearchSection(val providerID: ProviderID, val results: List<SearchResult>) {
-    val itemCount: Int = results.sumOf { it.entries.size }
-
-    val name: String = results.first().name
-
-    val nextCursor: Int? = results.lastOrNull()?.cursor?.toInt()
-
-    fun getItem(index: Int) = results.flatMap { it.entries }[index]
-
-    // FIXME the logic for this is actually more complicated
-    fun hasMoreResults(): Boolean {
-        return results.last().isPaginated && nextCursor == itemCount
-    }
-}
-
 /**
  * This Adapter populates a SectionedRecyclerView with search results by unified search
  */
@@ -69,11 +53,9 @@ class UnifiedSearchListAdapter(
     private val context: Context
 ) : SectionedRecyclerViewAdapter<SectionedViewHolder>() {
     companion object {
-        private const val FILES_PROVIDER_ID = "files"
         private const val VIEW_TYPE_EMPTY = Int.MAX_VALUE
     }
 
-    private var data: Map<ProviderID, List<SearchResult>> = emptyMap()
     private var sections: List<UnifiedSearchSection> = emptyList()
 
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionedViewHolder {
@@ -122,7 +104,7 @@ class UnifiedSearchListAdapter(
     }
 
     override fun getItemCount(section: Int): Int {
-        return sections[section].itemCount
+        return sections[section].entries.size
     }
 
     override fun onBindHeaderViewHolder(holder: SectionedViewHolder, section: Int, expanded: Boolean) {
@@ -131,14 +113,14 @@ class UnifiedSearchListAdapter(
     }
 
     override fun onBindFooterViewHolder(holder: SectionedViewHolder, section: Int) {
-        if (sections[section].hasMoreResults()) {
+        if (sections[section].hasMoreResults) {
             val footerViewHolder = holder as UnifiedSearchFooterViewHolder
             footerViewHolder.bind(sections[section])
         }
     }
 
     override fun getFooterViewType(section: Int): Int = when {
-        sections[section].hasMoreResults() -> VIEW_TYPE_FOOTER
+        sections[section].hasMoreResults -> VIEW_TYPE_FOOTER
         else -> VIEW_TYPE_EMPTY
     }
 
@@ -150,7 +132,7 @@ class UnifiedSearchListAdapter(
     ) {
         // TODO different binding (and also maybe diff UI) for non-file results
         val itemViewHolder = holder as UnifiedSearchItemViewHolder
-        val entry = sections[section].getItem(relativePosition)
+        val entry = sections[section].entries[relativePosition]
         itemViewHolder.bind(entry)
     }
 
@@ -164,23 +146,11 @@ class UnifiedSearchListAdapter(
         }
     }
 
-    fun setInitialData(results: Map<String, List<SearchResult>>) {
-        data = results
-        buildSectionList()
+    fun setData(sections: List<UnifiedSearchSection>) {
+        this.sections = sections
         notifyDataSetChanged()
     }
 
-    private fun buildSectionList() {
-        // sort so that files is always first
-        sections = data.map { UnifiedSearchSection(it.key, it.value) }.sortedWith { o1, o2 ->
-            when {
-                o1.providerID == FILES_PROVIDER_ID -> -1
-                o2.providerID == FILES_PROVIDER_ID -> 1
-                else -> 0
-            }
-        }
-    }
-
     init {
         // initialise thumbnails cache on background thread
         InitDiskCacheTask().execute()

+ 3 - 7
src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt

@@ -35,13 +35,13 @@ import com.nextcloud.client.di.ViewModelFactory
 import com.nextcloud.client.network.ClientFactory
 import com.owncloud.android.databinding.ListFragmentBinding
 import com.owncloud.android.datamodel.FileDataStorageManager
-import com.owncloud.android.lib.common.SearchResult
 import com.owncloud.android.lib.common.SearchResultEntry
 import com.owncloud.android.ui.activity.FileDisplayActivity
 import com.owncloud.android.ui.adapter.UnifiedSearchListAdapter
 import com.owncloud.android.ui.asynctasks.GetRemoteFileTask
 import com.owncloud.android.ui.interfaces.UnifiedSearchListInterface
 import com.owncloud.android.ui.unifiedsearch.ProviderID
+import com.owncloud.android.ui.unifiedsearch.UnifiedSearchSection
 import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel
 import javax.inject.Inject
 
@@ -110,10 +110,6 @@ class UnifiedSearchFragment : Fragment(), Injectable, UnifiedSearchListInterface
         binding.listRoot.adapter = adapter
     }
 
-    override fun onPause() {
-        super.onPause()
-        // photoSearchTask?.cancel(true)
-    }
 
     override fun onDestroyView() {
         super.onDestroyView()
@@ -151,10 +147,10 @@ class UnifiedSearchFragment : Fragment(), Injectable, UnifiedSearchListInterface
     }
 
     @VisibleForTesting
-    fun onSearchResultChanged(result: Map<String, List<SearchResult>>) {
+    fun onSearchResultChanged(result: List<UnifiedSearchSection>) {
         binding.emptyList.emptyListView.visibility = View.GONE
 
-        adapter.setInitialData(result)
+        adapter.setData(result)
     }
 
     @VisibleForTesting

+ 0 - 3
src/main/java/com/owncloud/android/ui/unifiedsearch/IUnifiedSearchRepository.kt

@@ -24,14 +24,11 @@ package com.owncloud.android.ui.unifiedsearch
 
 import com.owncloud.android.lib.common.SearchResult
 
-typealias ProviderID = String
 
 data class UnifiedSearchResult(val provider: ProviderID, val success: Boolean, val result: SearchResult)
 
 @Suppress("LongParameterList")
 interface IUnifiedSearchRepository {
-    fun refresh()
-    fun startLoading()
     fun queryAll(
         query: String,
         onResult: (UnifiedSearchResult) -> Unit,

+ 12 - 0
src/main/java/com/owncloud/android/ui/unifiedsearch/UnifiedSearchModel.kt

@@ -0,0 +1,12 @@
+package com.owncloud.android.ui.unifiedsearch
+
+import com.owncloud.android.lib.common.SearchResultEntry
+
+typealias ProviderID = String
+
+data class UnifiedSearchSection(
+    val providerID: ProviderID,
+    val name: String,
+    val entries: List<SearchResultEntry>,
+    val hasMoreResults: Boolean
+)

+ 0 - 8
src/main/java/com/owncloud/android/ui/unifiedsearch/UnifiedSearchRemoteRepository.kt

@@ -35,14 +35,6 @@ class UnifiedSearchRemoteRepository(
 
     private var providers: SearchProviders? = null
 
-    override fun refresh() {
-        TODO("Not yet implemented")
-    }
-
-    override fun startLoading() {
-        TODO("Not yet implemented")
-    }
-
     override fun queryAll(
         query: String,
         onResult: (UnifiedSearchResult) -> Unit,

+ 65 - 37
src/main/java/com/owncloud/android/ui/unifiedsearch/UnifiedSearchViewModel.kt

@@ -36,6 +36,30 @@ import javax.inject.Inject
 
 @Suppress("LongParameterList")
 class UnifiedSearchViewModel() : ViewModel() {
+    companion object {
+        private const val TAG = "UnifiedSearchViewModel"
+        private const val DEFAULT_LIMIT = 5
+        private const val FILES_PROVIDER_ID = "files"
+    }
+
+    private data class UnifiedSearchMetadata(
+        var results: MutableList<SearchResult> = mutableListOf()
+    ) {
+        fun nextCursor(): Int? = results.lastOrNull()?.cursor?.toInt()
+        fun name(): String? = results.lastOrNull()?.name
+        fun isFinished(): Boolean {
+            if (results.isEmpty()) {
+                return false
+            }
+            val lastResult = results.last()
+            return when {
+                !lastResult.isPaginated -> true
+                lastResult.entries.size < DEFAULT_LIMIT -> true
+                else -> false
+            }
+        }
+    }
+
     lateinit var currentAccountProvider: CurrentAccountProvider
     lateinit var runner: AsyncRunner
     lateinit var clientFactory: ClientFactory
@@ -44,17 +68,13 @@ class UnifiedSearchViewModel() : ViewModel() {
 
     private lateinit var repository: IUnifiedSearchRepository
     private var loadingStarted: Boolean = false
-    private var last: Int = -1
+    private var metaResults: MutableMap<ProviderID, UnifiedSearchMetadata> = mutableMapOf()
 
     val isLoading: MutableLiveData<Boolean> = MutableLiveData<Boolean>(false)
-    val searchResults = MutableLiveData<MutableMap<ProviderID, MutableList<SearchResult>>>(mutableMapOf())
+    val searchResults = MutableLiveData<List<UnifiedSearchSection>>(mutableListOf())
     val error: MutableLiveData<String> = MutableLiveData<String>("")
     val query: MutableLiveData<String> = MutableLiveData()
 
-    companion object {
-        private const val TAG = "UnifiedSearchViewModel"
-    }
-
     @Inject
     constructor(
         currentAccountProvider: CurrentAccountProvider,
@@ -77,8 +97,7 @@ class UnifiedSearchViewModel() : ViewModel() {
     }
 
     open fun refresh() {
-        last = -1
-        searchResults.value = mutableMapOf()
+        searchResults.value = mutableListOf()
         startLoading(query.value.orEmpty())
     }
 
@@ -103,17 +122,17 @@ class UnifiedSearchViewModel() : ViewModel() {
         val queryTerm = query.value.orEmpty()
 
         if (isLoading.value != true && queryTerm.isNotBlank()) {
-            isLoading.value = true
-            val providerResults = searchResults.value?.get(provider)
-            val cursor = providerResults?.filter { it.cursor != null }?.maxOfOrNull { it.cursor!!.toInt() }
-            repository.queryProvider(
-                queryTerm,
-                provider,
-                cursor,
-                this::onSearchResult,
-                this::onError,
-                this::onSearchFinished
-            )
+            metaResults[provider]?.nextCursor()?.let { cursor ->
+                isLoading.value = true
+                repository.queryProvider(
+                    queryTerm,
+                    provider,
+                    cursor,
+                    this::onSearchResult,
+                    this::onError,
+                    this::onSearchFinished
+                )
+            }
         }
     }
 
@@ -137,7 +156,7 @@ class UnifiedSearchViewModel() : ViewModel() {
     }
 
     fun onError(error: Throwable) {
-        Log_OC.d(TAG, "Error: " + error.stackTrace)
+        Log_OC.e(TAG, "Error: " + error.stackTrace)
     }
 
     @Synchronized
@@ -145,11 +164,11 @@ class UnifiedSearchViewModel() : ViewModel() {
         isLoading.value = false
 
         if (result.success) {
-            val currentValues: MutableMap<ProviderID, MutableList<SearchResult>> = searchResults.value ?: mutableMapOf()
-            val providerValues = currentValues[result.provider] ?: mutableListOf()
-            providerValues.add(result.result)
-            currentValues.put(result.provider, providerValues)
-            searchResults.value = currentValues
+            val providerMeta = metaResults[result.provider] ?: UnifiedSearchMetadata()
+            providerMeta.results.add(result.result)
+
+            metaResults[result.provider] = providerMeta
+            genSearchResultsFromMeta()
         }
 
         Log_OC.d(TAG, "onSearchResult: Provider '${result.provider}', success: ${result.success}")
@@ -158,6 +177,27 @@ class UnifiedSearchViewModel() : ViewModel() {
         }
     }
 
+    private fun genSearchResultsFromMeta() {
+        searchResults.value = metaResults
+            .filter { it.value.results.isNotEmpty() }
+            .map { (key, value) ->
+                UnifiedSearchSection(
+                    providerID = key,
+                    name = value.name()!!,
+                    entries = value.results.flatMap { it.entries },
+                    hasMoreResults = !value.isFinished()
+                )
+            }
+            .sortedWith { o1, o2 ->
+                // TODO sort with sort order from server providers?
+                when {
+                    o1.providerID == FILES_PROVIDER_ID -> -1
+                    o2.providerID == FILES_PROVIDER_ID -> 1
+                    else -> 0
+                }
+            }
+    }
+
     fun onSearchFinished(success: Boolean) {
         Log_OC.d(TAG, "onSearchFinished: success: $success")
         isLoading.value = false
@@ -171,18 +211,6 @@ class UnifiedSearchViewModel() : ViewModel() {
         this.repository = repository
     }
 
-//    private fun onActivitiesRequestResult(result: GetActivityListTask.Result) {
-//        isLoading.value = false
-//        if (result.success) {
-//            val existingActivities = activities.value ?: emptyList()
-//            val newActivities = listOf(existingActivities, result.activities).flatten()
-//            last = result.last
-//            activities.value = newActivities
-//        } else {
-//            error.value = resources.getString(R.string.activities_error)
-//        }
-//    }
-
     private fun onFileRequestResult(result: GetRemoteFileTask.Result) {
         isLoading.value = false
         if (result.success) {