summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2022-09-21 02:20:31 +0530
committerGitHub <noreply@github.com>2022-09-20 20:50:31 +0000
commit1e033792d61cb2b1f592ee0b236aebf9ae02d861 (patch)
tree0c3fb2276126aac9f266348d46a1650ce55fd3a4
parentde1325e1fc6405dee80e61122eea7ef028639eb8 (diff)
Refactor navigation and search code (#2134)
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt37
-rw-r--r--app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderFragment.kt12
-rw-r--r--app/src/main/java/app/passwordstore/ui/passwords/PasswordFragment.kt56
-rw-r--r--app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt24
-rw-r--r--app/src/main/java/app/passwordstore/util/Perf.kt39
-rw-r--r--app/src/main/java/app/passwordstore/util/extensions/Extensions.kt22
-rw-r--r--app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt72
7 files changed, 153 insertions, 109 deletions
diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt
index 8825bc58..3795f7e6 100644
--- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt
@@ -21,7 +21,7 @@ import androidx.core.text.bold
import androidx.core.text.buildSpannedString
import androidx.core.text.underline
import androidx.core.widget.addTextChangedListener
-import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import app.passwordstore.R
@@ -38,6 +38,8 @@ import app.passwordstore.util.viewmodel.SearchableRepositoryAdapter
import app.passwordstore.util.viewmodel.SearchableRepositoryViewModel
import com.github.androidpasswordstore.autofillparser.FormOrigin
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import logcat.LogPriority.ERROR
import logcat.logcat
@@ -85,9 +87,7 @@ class AutofillFilterView : AppCompatActivity() {
private lateinit var directoryStructure: DirectoryStructure
private val binding by viewBinding(ActivityOreoAutofillFilterBinding::inflate)
- private val model: SearchableRepositoryViewModel by viewModels {
- ViewModelProvider.AndroidViewModelFactory(application)
- }
+ private val model: SearchableRepositoryViewModel by viewModels()
private val decryptAction =
registerForActivityResult(StartActivityForResult()) { result ->
@@ -193,20 +193,23 @@ class AutofillFilterView : AppCompatActivity() {
R.string.oreo_autofill_match_with,
formOrigin.getPrettyIdentifier(applicationContext)
)
- model.searchResult.observe(this@AutofillFilterView) { result ->
- val list = result.passwordItems
- (rvPassword.adapter as SearchableRepositoryAdapter).submitList(list) {
- rvPassword.scrollToPosition(0)
- }
- // Switch RecyclerView out for a "no results" message if the new list is empty and
- // the message is not yet shown (and vice versa).
- if (
- (list.isEmpty() && rvPasswordSwitcher.nextView.id == rvPasswordEmpty.id) ||
- (list.isNotEmpty() && rvPasswordSwitcher.nextView.id == rvPassword.id)
- ) {
- rvPasswordSwitcher.showNext()
+ model.searchResult
+ .flowWithLifecycle(lifecycle)
+ .onEach { result ->
+ val list = result.passwordItems
+ (rvPassword.adapter as SearchableRepositoryAdapter).submitList(list) {
+ rvPassword.scrollToPosition(0)
+ }
+ // Switch RecyclerView out for a "no results" message if the new list is empty and
+ // the message is not yet shown (and vice versa).
+ if (
+ (list.isEmpty() && rvPasswordSwitcher.nextView.id == rvPasswordEmpty.id) ||
+ (list.isNotEmpty() && rvPasswordSwitcher.nextView.id == rvPassword.id)
+ ) {
+ rvPasswordSwitcher.showNext()
+ }
}
- }
+ .launchIn(lifecycleScope)
}
}
diff --git a/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderFragment.kt b/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderFragment.kt
index 577777f7..44aaa63f 100644
--- a/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderFragment.kt
+++ b/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderFragment.kt
@@ -10,6 +10,7 @@ import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import app.passwordstore.R
@@ -23,6 +24,8 @@ import app.passwordstore.util.viewmodel.SearchableRepositoryViewModel
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching
import java.io.File
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import me.zhanghai.android.fastscroll.FastScrollerBuilder
class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
@@ -51,9 +54,10 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
val path = requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH))
model.navigateTo(File(path), listMode = ListMode.DirectoriesOnly, pushPreviousLocation = false)
- model.searchResult.observe(viewLifecycleOwner) { result ->
- recyclerAdapter.submitList(result.passwordItems)
- }
+ model.searchResult
+ .flowWithLifecycle(lifecycle)
+ .onEach { result -> recyclerAdapter.submitList(result.passwordItems) }
+ .launchIn(lifecycleScope)
}
override fun onAttach(context: Context) {
@@ -77,7 +81,7 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
}
val currentDir: File
- get() = model.currentDir.value!!
+ get() = model.currentDir.value
interface OnFragmentInteractionListener {
diff --git a/app/src/main/java/app/passwordstore/ui/passwords/PasswordFragment.kt b/app/src/main/java/app/passwordstore/ui/passwords/PasswordFragment.kt
index 9c7011a4..65570bcc 100644
--- a/app/src/main/java/app/passwordstore/ui/passwords/PasswordFragment.kt
+++ b/app/src/main/java/app/passwordstore/ui/passwords/PasswordFragment.kt
@@ -18,6 +18,7 @@ import androidx.appcompat.view.ActionMode
import androidx.core.content.edit
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@@ -48,6 +49,8 @@ import com.github.michaelbull.result.runCatching
import dagger.hilt.android.AndroidEntryPoint
import java.io.File
import javax.inject.Inject
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import me.zhanghai.android.fastscroll.FastScrollerBuilder
@@ -74,7 +77,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
}
val currentDir: File
- get() = model.currentDir.value!!
+ get() = model.currentDir.value
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -174,36 +177,37 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
val path = requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH))
model.navigateTo(File(path), pushPreviousLocation = false)
- model.searchResult.observe(viewLifecycleOwner) { result ->
- // Only run animations when the new list is filtered, i.e., the user submitted a search,
- // and not on folder navigations since the latter leads to too many removal animations.
- (recyclerView.itemAnimator as OnOffItemAnimator).isEnabled = result.isFiltered
- recyclerAdapter.submitList(result.passwordItems) {
- when {
- result.isFiltered -> {
- // When the result is filtered, we always scroll to the top since that is
- // where
- // the best fuzzy match appears.
- recyclerView.scrollToPosition(0)
- }
- scrollTarget != null -> {
- scrollTarget?.let {
- recyclerView.scrollToPosition(recyclerAdapter.getPositionForFile(it))
+ model.searchResult
+ .flowWithLifecycle(lifecycle)
+ .onEach { result ->
+ // Only run animations when the new list is filtered, i.e., the user submitted a search,
+ // and not on folder navigation since the latter leads to too many removal animations.
+ (recyclerView.itemAnimator as OnOffItemAnimator).isEnabled = result.isFiltered
+ recyclerAdapter.submitList(result.passwordItems) {
+ when {
+ result.isFiltered -> {
+ // When the result is filtered, we always scroll to the top since that is
+ // where the best fuzzy match appears.
+ recyclerView.scrollToPosition(0)
}
- scrollTarget = null
- }
- else -> {
- // When the result is not filtered and there is a saved scroll position for
- // it,
- // we try to restore it.
- recyclerViewStateToRestore?.let {
- recyclerView.layoutManager!!.onRestoreInstanceState(it)
+ scrollTarget != null -> {
+ scrollTarget?.let {
+ recyclerView.scrollToPosition(recyclerAdapter.getPositionForFile(it))
+ }
+ scrollTarget = null
+ }
+ else -> {
+ // When the result is not filtered and there is a saved scroll position for
+ // it, we try to restore it.
+ recyclerViewStateToRestore?.let {
+ recyclerView.layoutManager!!.onRestoreInstanceState(it)
+ }
+ recyclerViewStateToRestore = null
}
- recyclerViewStateToRestore = null
}
}
}
- }
+ .launchIn(lifecycleScope)
}
private val actionModeCallback =
diff --git a/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt b/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt
index c97782b0..e09ece7e 100644
--- a/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt
+++ b/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt
@@ -20,7 +20,7 @@ import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.core.content.edit
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.commit
-import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import app.passwordstore.R
import app.passwordstore.data.password.PasswordItem
@@ -71,9 +71,7 @@ class PasswordStore : BaseGitActivity() {
private lateinit var searchItem: MenuItem
private val settings by lazy { sharedPrefs }
- private val model: SearchableRepositoryViewModel by viewModels {
- ViewModelProvider.AndroidViewModelFactory(application)
- }
+ private val model: SearchableRepositoryViewModel by viewModels()
private val listRefreshAction =
registerForActivityResult(StartActivityForResult()) { result ->
@@ -186,10 +184,12 @@ class PasswordStore : BaseGitActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pwdstore)
- model.currentDir.observe(this) { dir ->
- val basePath = PasswordRepository.getRepositoryDirectory().absoluteFile
- supportActionBar?.apply {
- if (dir != basePath) title = dir.name else setTitle(R.string.app_name)
+ lifecycleScope.launchWhenCreated {
+ model.currentDir.flowWithLifecycle(lifecycle).collect { dir ->
+ val basePath = PasswordRepository.getRepositoryDirectory().absoluteFile
+ supportActionBar?.apply {
+ if (dir != basePath) title = dir.name else setTitle(R.string.app_name)
+ }
}
}
}
@@ -238,7 +238,7 @@ class PasswordStore : BaseGitActivity() {
// List the contents of the current directory if the user enters a blank
// search term.
if (filter.isEmpty())
- model.navigateTo(newDirectory = model.currentDir.value!!, pushPreviousLocation = false)
+ model.navigateTo(newDirectory = model.currentDir.value, pushPreviousLocation = false)
else model.search(filter)
return true
}
@@ -544,12 +544,12 @@ class PasswordStore : BaseGitActivity() {
*/
fun refreshPasswordList(target: File? = null) {
val plist = getPasswordFragment()
- if (target?.isDirectory == true && model.currentDir.value?.contains(target) == true) {
+ if (target?.isDirectory == true && model.currentDir.value.contains(target)) {
plist?.navigateTo(target)
- } else if (target?.isFile == true && model.currentDir.value?.contains(target) == true) {
+ } else if (target?.isFile == true && model.currentDir.value.contains(target)) {
// Creating new passwords is handled by an activity, so we will refresh in onStart.
plist?.scrollToOnNextRefresh(target)
- } else if (model.currentDir.value?.isDirectory == true) {
+ } else if (model.currentDir.value.isDirectory) {
model.forceRefresh()
} else {
model.reset()
diff --git a/app/src/main/java/app/passwordstore/util/Perf.kt b/app/src/main/java/app/passwordstore/util/Perf.kt
new file mode 100644
index 00000000..8b3769e9
--- /dev/null
+++ b/app/src/main/java/app/passwordstore/util/Perf.kt
@@ -0,0 +1,39 @@
+// It's okay if this stays unused for the most part since it is development tooling.
+@file:Suppress("Unused")
+
+package app.passwordstore.util
+
+import android.os.Looper
+import android.os.SystemClock
+import logcat.logcat
+
+/**
+ * Small helper to execute a given [block] and log the time it took to execute it. Intended for use
+ * in day-to-day perf investigations and code using it should probably not be shipped.
+ */
+suspend fun <T> logExecutionTime(tag: String, block: suspend () -> T): T {
+ val start = SystemClock.uptimeMillis()
+ val res = block()
+ val end = SystemClock.uptimeMillis()
+ logcat(tag) { "Finished in ${end - start}ms" }
+ return res
+}
+
+fun <T> logExecutionTimeBlocking(tag: String, block: () -> T): T {
+ val start = SystemClock.uptimeMillis()
+ val res = block()
+ val end = SystemClock.uptimeMillis()
+ logcat(tag) { "Finished in ${end - start}ms" }
+ return res
+}
+
+/**
+ * Throws if called on the main thread, used to ensure an operation being offloaded to a background
+ * thread is correctly being moved off the main thread.
+ */
+@Suppress("NOTHING_TO_INLINE")
+inline fun checkMainThread() {
+ require(Looper.myLooper() != Looper.getMainLooper()) {
+ "This operation must not run on the main thread"
+ }
+}
diff --git a/app/src/main/java/app/passwordstore/util/extensions/Extensions.kt b/app/src/main/java/app/passwordstore/util/extensions/Extensions.kt
index 9d81587d..c0b40aa4 100644
--- a/app/src/main/java/app/passwordstore/util/extensions/Extensions.kt
+++ b/app/src/main/java/app/passwordstore/util/extensions/Extensions.kt
@@ -6,7 +6,6 @@ package app.passwordstore.util.extensions
import app.passwordstore.data.repo.PasswordRepository
import com.github.michaelbull.result.Err
-import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.runCatching
@@ -16,14 +15,6 @@ import logcat.asLog
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.revwalk.RevCommit
-/** The default OpenPGP provider for the app */
-const val OPENPGP_PROVIDER = "org.sufficientlysecure.keychain"
-
-/** Clears the given [flag] from the value of this [Int] */
-fun Int.clearFlag(flag: Int): Int {
- return this and flag.inv()
-}
-
/** Checks if this [Int] contains the given [flag] */
infix fun Int.hasFlag(flag: Int): Boolean {
return this and flag == flag
@@ -73,25 +64,12 @@ val RevCommit.time: Date
return Date(epochMilliseconds)
}
-/**
- * Splits this [String] into an [Array] of [String] s, split on the UNIX LF line ending and stripped
- * of any empty lines.
- */
-fun String.splitLines(): Array<String> {
- return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
-}
-
/** Alias to [lazy] with thread safety mode always set to [LazyThreadSafetyMode.NONE]. */
fun <T> unsafeLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE) { initializer.invoke() }
/** A convenience extension to turn a [Throwable] with a message into a loggable string. */
fun Throwable.asLog(message: String): String = "$message\n${asLog()}"
-/** Extension on [Result] that returns if the type is [Ok] */
-fun <V, E> Result<V, E>.isOk(): Boolean {
- return this is Ok<V>
-}
-
/** Extension on [Result] that returns if the type is [Err] */
fun <V, E> Result<V, E>.isErr(): Boolean {
return this is Err<E>
diff --git a/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt b/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt
index 8cb18309..ba42bb34 100644
--- a/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt
+++ b/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt
@@ -5,16 +5,13 @@
package app.passwordstore.util.viewmodel
import android.app.Application
+import android.content.SharedPreferences
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.asFlow
-import androidx.lifecycle.asLiveData
import androidx.recyclerview.selection.ItemDetailsLookup
import androidx.recyclerview.selection.ItemKeyProvider
import androidx.recyclerview.selection.Selection
@@ -26,30 +23,35 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import app.passwordstore.data.password.PasswordItem
import app.passwordstore.data.repo.PasswordRepository
+import app.passwordstore.injection.prefs.SettingsPreferences
import app.passwordstore.util.autofill.AutofillPreferences
import app.passwordstore.util.autofill.DirectoryStructure
-import app.passwordstore.util.extensions.sharedPrefs
-import app.passwordstore.util.extensions.unsafeLazy
+import app.passwordstore.util.checkMainThread
+import app.passwordstore.util.coroutines.DispatcherProvider
import app.passwordstore.util.settings.PasswordSortOrder
import app.passwordstore.util.settings.PreferenceKeys
import com.github.androidpasswordstore.sublimefuzzy.Fuzzy
+import dagger.hilt.android.lifecycle.HiltViewModel
import java.io.File
import java.text.Collator
import java.util.Locale
import java.util.Stack
+import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asFlow
-import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import me.zhanghai.android.fastscroll.PopupTextProvider
@@ -108,8 +110,15 @@ enum class ListMode {
AllEntries
}
-@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
-class SearchableRepositoryViewModel(application: Application) : AndroidViewModel(application) {
+@OptIn(ExperimentalCoroutinesApi::class)
+@HiltViewModel
+class SearchableRepositoryViewModel
+@Inject
+constructor(
+ application: Application,
+ dispatcherProvider: DispatcherProvider,
+ @SettingsPreferences private val settings: SharedPreferences,
+) : AndroidViewModel(application) {
private var _updateCounter = 0
private val updateCounter: Int
@@ -121,7 +130,6 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
private val root
get() = PasswordRepository.getRepositoryDirectory()
- private val settings by unsafeLazy { application.sharedPrefs }
private val showHiddenContents
get() = settings.getBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, false)
private val defaultSearchMode
@@ -169,8 +177,8 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
private fun updateSearchAction(action: SearchAction) = action.copy(updateCounter = updateCounter)
- private val searchAction =
- MutableLiveData(
+ private val searchActionFlow =
+ MutableStateFlow(
makeSearchAction(
baseDirectory = root,
filter = "",
@@ -179,13 +187,13 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
listMode = ListMode.AllEntries
)
)
- private val searchActionFlow = searchAction.asFlow().distinctUntilChanged()
data class SearchResult(val passwordItems: List<PasswordItem>, val isFiltered: Boolean)
val searchResult =
searchActionFlow
.mapLatest { searchAction ->
+ checkMainThread()
val listResultFlow =
when (searchAction.searchMode) {
SearchMode.RecursivelyInSubdirectories ->
@@ -194,14 +202,20 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
}
val prefilteredResultFlow =
when (searchAction.listMode) {
- ListMode.FilesOnly -> listResultFlow.filter { it.isFile }
- ListMode.DirectoriesOnly -> listResultFlow.filter { it.isDirectory }
+ ListMode.FilesOnly ->
+ listResultFlow.filter { it.isFile }.flowOn(dispatcherProvider.io())
+ ListMode.DirectoriesOnly ->
+ listResultFlow.filter { it.isDirectory }.flowOn(dispatcherProvider.io())
ListMode.AllEntries -> listResultFlow
}
val passwordList =
when (if (searchAction.filter == "") FilterMode.NoFilter else searchAction.filterMode) {
FilterMode.NoFilter -> {
- prefilteredResultFlow.map { it.toPasswordItem() }.toList().sortedWith(itemComparator)
+ prefilteredResultFlow
+ .map { it.toPasswordItem() }
+ .flowOn(dispatcherProvider.io())
+ .toList()
+ .sortedWith(itemComparator)
}
FilterMode.StrictDomain -> {
check(searchAction.listMode == ListMode.FilesOnly) {
@@ -214,6 +228,7 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
regex.containsMatchIn(absoluteFile.relativeTo(root).path)
}
.map { it.toPasswordItem() }
+ .flowOn(dispatcherProvider.io())
.toList()
.sortedWith(itemComparator)
} else {
@@ -227,6 +242,7 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
Pair(item.fuzzyMatch(searchAction.filter), item)
}
.filter { it.first > 0 }
+ .flowOn(dispatcherProvider.io())
.toList()
.sortedWith(
compareByDescending<Pair<Int, PasswordItem>> { it.first }
@@ -237,7 +253,7 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
}
SearchResult(passwordList, isFiltered = searchAction.filterMode != FilterMode.NoFilter)
}
- .asLiveData(Dispatchers.IO)
+ .flowOn(dispatcherProvider.io())
private fun shouldTake(file: File) =
with(file) {
@@ -270,8 +286,8 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
.filter(::shouldTake)
}
- private val _currentDir = MutableLiveData(root)
- val currentDir = _currentDir as LiveData<File>
+ private val _currentDir = MutableStateFlow(root)
+ val currentDir = _currentDir.asStateFlow()
data class NavigationStackEntry(val dir: File, val recyclerViewState: Parcelable?)
@@ -286,9 +302,9 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
if (!newDirectory.exists()) return
require(newDirectory.isDirectory) { "Can only navigate to a directory" }
if (pushPreviousLocation) {
- navigationStack.push(NavigationStackEntry(_currentDir.value!!, recyclerViewState))
+ navigationStack.push(NavigationStackEntry(_currentDir.value, recyclerViewState))
}
- searchAction.postValue(
+ searchActionFlow.update {
makeSearchAction(
filter = "",
baseDirectory = newDirectory,
@@ -296,8 +312,8 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
searchMode = SearchMode.InCurrentDirectoryOnly,
listMode = listMode
)
- )
- _currentDir.postValue(newDirectory)
+ }
+ _currentDir.update { newDirectory }
}
val canNavigateBack
@@ -330,20 +346,20 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
listMode: ListMode = ListMode.AllEntries
) {
require(baseDirectory?.isDirectory != false) { "Can only search in a directory" }
- searchAction.postValue(
+ searchActionFlow.update {
makeSearchAction(
filter = filter,
- baseDirectory = baseDirectory ?: _currentDir.value!!,
+ baseDirectory = baseDirectory ?: _currentDir.value,
filterMode = filterMode,
searchMode = searchMode ?: defaultSearchMode,
listMode = listMode
)
- )
+ }
}
fun forceRefresh() {
forceUpdateOnNextSearchAction()
- searchAction.postValue(updateSearchAction(searchAction.value!!))
+ searchActionFlow.update { updateSearchAction(searchActionFlow.value) }
}
companion object {