aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/dev/msfjarvis/aps/util/viewmodel
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2021-03-09 14:53:11 +0530
committerHarsh Shandilya <me@msfjarvis.dev>2021-03-18 11:40:30 +0530
commit774fda83ac276a635e3402034b1eedbd10be916f (patch)
tree57783a953fcd193d32f1e3dc8df26e07f03df328 /app/src/main/java/dev/msfjarvis/aps/util/viewmodel
parentbe31ae37f443982b377d027cd613f04d0926568d (diff)
downloadHardenedPass-774fda83ac276a635e3402034b1eedbd10be916f.tar.gz
HardenedPass-774fda83ac276a635e3402034b1eedbd10be916f.zip
all: reformat with ktfmt
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Diffstat (limited to 'app/src/main/java/dev/msfjarvis/aps/util/viewmodel')
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/viewmodel/SearchableRepositoryViewModel.kt729
1 files changed, 363 insertions, 366 deletions
diff --git a/app/src/main/java/dev/msfjarvis/aps/util/viewmodel/SearchableRepositoryViewModel.kt b/app/src/main/java/dev/msfjarvis/aps/util/viewmodel/SearchableRepositoryViewModel.kt
index 0734c1d6..c3998d2a 100644
--- a/app/src/main/java/dev/msfjarvis/aps/util/viewmodel/SearchableRepositoryViewModel.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/util/viewmodel/SearchableRepositoryViewModel.kt
@@ -50,425 +50,422 @@ import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.yield
import me.zhanghai.android.fastscroll.PopupTextProvider
-private fun File.toPasswordItem() = if (isFile)
- PasswordItem.newPassword(name, this, PasswordRepository.getRepositoryDirectory())
-else
- PasswordItem.newCategory(name, this, PasswordRepository.getRepositoryDirectory())
+private fun File.toPasswordItem() =
+ if (isFile) PasswordItem.newPassword(name, this, PasswordRepository.getRepositoryDirectory())
+ else PasswordItem.newCategory(name, this, PasswordRepository.getRepositoryDirectory())
private fun PasswordItem.fuzzyMatch(filter: String): Int {
- var i = 0
- var j = 0
- var score = 0
- var bonus = 0
- var bonusIncrement = 0
-
- val toMatch = longName
-
- while (i < filter.length && j < toMatch.length) {
- when {
- filter[i].isWhitespace() -> i++
- filter[i].toLowerCase() == toMatch[j].toLowerCase() -> {
- i++
- bonusIncrement += 1
- bonus += bonusIncrement
- score += bonus
- }
- else -> {
- bonus = 0
- bonusIncrement = 0
- }
- }
- j++
+ var i = 0
+ var j = 0
+ var score = 0
+ var bonus = 0
+ var bonusIncrement = 0
+
+ val toMatch = longName
+
+ while (i < filter.length && j < toMatch.length) {
+ when {
+ filter[i].isWhitespace() -> i++
+ filter[i].toLowerCase() == toMatch[j].toLowerCase() -> {
+ i++
+ bonusIncrement += 1
+ bonus += bonusIncrement
+ score += bonus
+ }
+ else -> {
+ bonus = 0
+ bonusIncrement = 0
+ }
}
- return if (i == filter.length) score else 0
+ j++
+ }
+ return if (i == filter.length) score else 0
}
-private val CaseInsensitiveComparator = Collator.getInstance().apply {
- strength = Collator.PRIMARY
-}
+private val CaseInsensitiveComparator = Collator.getInstance().apply { strength = Collator.PRIMARY }
private fun PasswordItem.Companion.makeComparator(
- typeSortOrder: PasswordSortOrder,
- directoryStructure: DirectoryStructure
+ typeSortOrder: PasswordSortOrder,
+ directoryStructure: DirectoryStructure
): Comparator<PasswordItem> {
- return when (typeSortOrder) {
- PasswordSortOrder.FOLDER_FIRST -> compareBy { it.type }
- // In order to let INDEPENDENT not distinguish between items based on their type, we simply
- // declare them all equal at this stage.
- PasswordSortOrder.INDEPENDENT -> Comparator { _, _ -> 0 }
- PasswordSortOrder.FILE_FIRST -> compareByDescending { it.type }
- PasswordSortOrder.RECENTLY_USED -> PasswordSortOrder.RECENTLY_USED.comparator
+ return when (typeSortOrder) {
+ PasswordSortOrder.FOLDER_FIRST -> compareBy { it.type }
+ // In order to let INDEPENDENT not distinguish between items based on their type, we
+ // simply
+ // declare them all equal at this stage.
+ PasswordSortOrder.INDEPENDENT -> Comparator { _, _ -> 0 }
+ PasswordSortOrder.FILE_FIRST -> compareByDescending { it.type }
+ PasswordSortOrder.RECENTLY_USED -> PasswordSortOrder.RECENTLY_USED.comparator
}
- .then(compareBy(nullsLast(CaseInsensitiveComparator)) {
- directoryStructure.getIdentifierFor(it.file)
- })
- .then(compareBy(nullsLast(CaseInsensitiveComparator)) {
- directoryStructure.getUsernameFor(it.file)
- })
+ .then(compareBy(nullsLast(CaseInsensitiveComparator)) { directoryStructure.getIdentifierFor(it.file) })
+ .then(compareBy(nullsLast(CaseInsensitiveComparator)) { directoryStructure.getUsernameFor(it.file) })
}
val PasswordItem.stableId: String
- get() = file.absolutePath
+ get() = file.absolutePath
enum class FilterMode {
- NoFilter,
- StrictDomain,
- Fuzzy
+ NoFilter,
+ StrictDomain,
+ Fuzzy
}
enum class SearchMode {
- RecursivelyInSubdirectories,
- InCurrentDirectoryOnly
+ RecursivelyInSubdirectories,
+ InCurrentDirectoryOnly
}
enum class ListMode {
- FilesOnly,
- DirectoriesOnly,
- AllEntries
+ FilesOnly,
+ DirectoriesOnly,
+ AllEntries
}
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
class SearchableRepositoryViewModel(application: Application) : AndroidViewModel(application) {
- private var _updateCounter = 0
- private val updateCounter: Int
- get() = _updateCounter
-
- private fun forceUpdateOnNextSearchAction() {
- _updateCounter++
- }
-
- private val root
- get() = PasswordRepository.getRepositoryDirectory()
- private val settings by lazy(LazyThreadSafetyMode.NONE) { application.sharedPrefs }
- private val showHiddenContents
- get() = settings.getBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, false)
- private val defaultSearchMode
- get() = if (settings.getBoolean(PreferenceKeys.FILTER_RECURSIVELY, true)) {
- SearchMode.RecursivelyInSubdirectories
- } else {
- SearchMode.InCurrentDirectoryOnly
- }
-
- private val typeSortOrder
- get() = PasswordSortOrder.getSortOrder(settings)
- private val directoryStructure
- get() = AutofillPreferences.directoryStructure(getApplication())
- private val itemComparator
- get() = PasswordItem.makeComparator(typeSortOrder, directoryStructure)
-
- private data class SearchAction(
- val baseDirectory: File,
- val filter: String,
- val filterMode: FilterMode,
- val searchMode: SearchMode,
- val listMode: ListMode,
- // This counter can be increased to force a reexecution of the search action even if all
- // other arguments are left unchanged.
- val updateCounter: Int
+ private var _updateCounter = 0
+ private val updateCounter: Int
+ get() = _updateCounter
+
+ private fun forceUpdateOnNextSearchAction() {
+ _updateCounter++
+ }
+
+ private val root
+ get() = PasswordRepository.getRepositoryDirectory()
+ private val settings by lazy(LazyThreadSafetyMode.NONE) { application.sharedPrefs }
+ private val showHiddenContents
+ get() = settings.getBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, false)
+ private val defaultSearchMode
+ get() =
+ if (settings.getBoolean(PreferenceKeys.FILTER_RECURSIVELY, true)) {
+ SearchMode.RecursivelyInSubdirectories
+ } else {
+ SearchMode.InCurrentDirectoryOnly
+ }
+
+ private val typeSortOrder
+ get() = PasswordSortOrder.getSortOrder(settings)
+ private val directoryStructure
+ get() = AutofillPreferences.directoryStructure(getApplication())
+ private val itemComparator
+ get() = PasswordItem.makeComparator(typeSortOrder, directoryStructure)
+
+ private data class SearchAction(
+ val baseDirectory: File,
+ val filter: String,
+ val filterMode: FilterMode,
+ val searchMode: SearchMode,
+ val listMode: ListMode,
+ // This counter can be increased to force a reexecution of the search action even if all
+ // other arguments are left unchanged.
+ val updateCounter: Int
+ )
+
+ private fun makeSearchAction(
+ baseDirectory: File,
+ filter: String,
+ filterMode: FilterMode,
+ searchMode: SearchMode,
+ listMode: ListMode
+ ): SearchAction {
+ return SearchAction(
+ baseDirectory = baseDirectory,
+ filter = filter,
+ filterMode = filterMode,
+ searchMode = searchMode,
+ listMode = listMode,
+ updateCounter = updateCounter
)
-
- private fun makeSearchAction(
- baseDirectory: File,
- filter: String,
- filterMode: FilterMode,
- searchMode: SearchMode,
- listMode: ListMode
- ): SearchAction {
- return SearchAction(
- baseDirectory = baseDirectory,
- filter = filter,
- filterMode = filterMode,
- searchMode = searchMode,
- listMode = listMode,
- updateCounter = updateCounter
- )
- }
-
- private fun updateSearchAction(action: SearchAction) =
- action.copy(updateCounter = updateCounter)
-
- private val searchAction = MutableLiveData(
- makeSearchAction(
- baseDirectory = root,
- filter = "",
- filterMode = FilterMode.NoFilter,
- searchMode = SearchMode.InCurrentDirectoryOnly,
- listMode = ListMode.AllEntries
- )
+ }
+
+ private fun updateSearchAction(action: SearchAction) = action.copy(updateCounter = updateCounter)
+
+ private val searchAction =
+ MutableLiveData(
+ makeSearchAction(
+ baseDirectory = root,
+ filter = "",
+ filterMode = FilterMode.NoFilter,
+ searchMode = SearchMode.InCurrentDirectoryOnly,
+ listMode = ListMode.AllEntries
+ )
)
- private val searchActionFlow = searchAction.asFlow().distinctUntilChanged()
-
- data class SearchResult(val passwordItems: List<PasswordItem>, val isFiltered: Boolean)
-
- val searchResult = searchActionFlow
- .mapLatest { searchAction ->
- val listResultFlow = when (searchAction.searchMode) {
- SearchMode.RecursivelyInSubdirectories -> listFilesRecursively(searchAction.baseDirectory)
- SearchMode.InCurrentDirectoryOnly -> listFiles(searchAction.baseDirectory)
+ private val searchActionFlow = searchAction.asFlow().distinctUntilChanged()
+
+ data class SearchResult(val passwordItems: List<PasswordItem>, val isFiltered: Boolean)
+
+ val searchResult =
+ searchActionFlow
+ .mapLatest { searchAction ->
+ val listResultFlow =
+ when (searchAction.searchMode) {
+ SearchMode.RecursivelyInSubdirectories -> listFilesRecursively(searchAction.baseDirectory)
+ SearchMode.InCurrentDirectoryOnly -> listFiles(searchAction.baseDirectory)
+ }
+ val prefilteredResultFlow =
+ when (searchAction.listMode) {
+ ListMode.FilesOnly -> listResultFlow.filter { it.isFile }
+ ListMode.DirectoriesOnly -> listResultFlow.filter { it.isDirectory }
+ ListMode.AllEntries -> listResultFlow
+ }
+ val filterModeToUse = if (searchAction.filter == "") FilterMode.NoFilter else searchAction.filterMode
+ val passwordList =
+ when (filterModeToUse) {
+ FilterMode.NoFilter -> {
+ prefilteredResultFlow.map { it.toPasswordItem() }.toList().sortedWith(itemComparator)
}
- val prefilteredResultFlow = when (searchAction.listMode) {
- ListMode.FilesOnly -> listResultFlow.filter { it.isFile }
- ListMode.DirectoriesOnly -> listResultFlow.filter { it.isDirectory }
- ListMode.AllEntries -> listResultFlow
+ FilterMode.StrictDomain -> {
+ check(searchAction.listMode == ListMode.FilesOnly) {
+ "Searches with StrictDomain search mode can only list files"
+ }
+ val regex = generateStrictDomainRegex(searchAction.filter)
+ if (regex != null) {
+ prefilteredResultFlow
+ .filter { absoluteFile -> regex.containsMatchIn(absoluteFile.relativeTo(root).path) }
+ .map { it.toPasswordItem() }
+ .toList()
+ .sortedWith(itemComparator)
+ } else {
+ emptyList()
+ }
}
- val filterModeToUse =
- if (searchAction.filter == "") FilterMode.NoFilter else searchAction.filterMode
- val passwordList = when (filterModeToUse) {
- FilterMode.NoFilter -> {
- prefilteredResultFlow
- .map { it.toPasswordItem() }
- .toList()
- .sortedWith(itemComparator)
- }
- FilterMode.StrictDomain -> {
- check(searchAction.listMode == ListMode.FilesOnly) { "Searches with StrictDomain search mode can only list files" }
- val regex = generateStrictDomainRegex(searchAction.filter)
- if (regex != null) {
- prefilteredResultFlow
- .filter { absoluteFile ->
- regex.containsMatchIn(absoluteFile.relativeTo(root).path)
- }
- .map { it.toPasswordItem() }
- .toList()
- .sortedWith(itemComparator)
- } else {
- emptyList()
- }
- }
- FilterMode.Fuzzy -> {
- prefilteredResultFlow
- .map {
- val item = it.toPasswordItem()
- Pair(item.fuzzyMatch(searchAction.filter), item)
- }
- .filter { it.first > 0 }
- .toList()
- .sortedWith(
- compareByDescending<Pair<Int, PasswordItem>> { it.first }.thenBy(
- itemComparator
- ) { it.second })
- .map { it.second }
+ FilterMode.Fuzzy -> {
+ prefilteredResultFlow
+ .map {
+ val item = it.toPasswordItem()
+ Pair(item.fuzzyMatch(searchAction.filter), item)
}
+ .filter { it.first > 0 }
+ .toList()
+ .sortedWith(
+ compareByDescending<Pair<Int, PasswordItem>> { it.first }.thenBy(itemComparator) { it.second }
+ )
+ .map { it.second }
}
- SearchResult(passwordList, isFiltered = searchAction.filterMode != FilterMode.NoFilter)
- }.asLiveData(Dispatchers.IO)
-
- private fun shouldTake(file: File) = with(file) {
- if (showHiddenContents) return true
- if (isDirectory) {
- !isHidden
- } else {
- !isHidden && file.extension == "gpg"
- }
- }
-
- private fun listFiles(dir: File): Flow<File> {
- return dir.listFiles { file -> shouldTake(file) }?.asFlow() ?: emptyFlow()
+ }
+ SearchResult(passwordList, isFiltered = searchAction.filterMode != FilterMode.NoFilter)
+ }
+ .asLiveData(Dispatchers.IO)
+
+ private fun shouldTake(file: File) =
+ with(file) {
+ if (showHiddenContents) return true
+ if (isDirectory) {
+ !isHidden
+ } else {
+ !isHidden && file.extension == "gpg"
+ }
}
- private fun listFilesRecursively(dir: File): Flow<File> {
- return dir
- // Take top directory even if it is hidden.
- .walkTopDown().onEnter { file -> file == dir || shouldTake(file) }
- .asFlow()
- // Skip the root directory
- .drop(1)
- .map {
- yield()
- it
- }
- .filter { file -> shouldTake(file) }
+ private fun listFiles(dir: File): Flow<File> {
+ return dir.listFiles { file -> shouldTake(file) }?.asFlow() ?: emptyFlow()
+ }
+
+ private fun listFilesRecursively(dir: File): Flow<File> {
+ return dir
+ // Take top directory even if it is hidden.
+ .walkTopDown()
+ .onEnter { file -> file == dir || shouldTake(file) }
+ .asFlow()
+ // Skip the root directory
+ .drop(1)
+ .map {
+ yield()
+ it
+ }
+ .filter { file -> shouldTake(file) }
+ }
+
+ private val _currentDir = MutableLiveData(root)
+ val currentDir = _currentDir as LiveData<File>
+
+ data class NavigationStackEntry(val dir: File, val recyclerViewState: Parcelable?)
+
+ private val navigationStack = Stack<NavigationStackEntry>()
+
+ fun navigateTo(
+ newDirectory: File = root,
+ listMode: ListMode = ListMode.AllEntries,
+ recyclerViewState: Parcelable? = null,
+ pushPreviousLocation: Boolean = true
+ ) {
+ if (!newDirectory.exists()) return
+ require(newDirectory.isDirectory) { "Can only navigate to a directory" }
+ if (pushPreviousLocation) {
+ navigationStack.push(NavigationStackEntry(_currentDir.value!!, recyclerViewState))
}
-
- private val _currentDir = MutableLiveData(root)
- val currentDir = _currentDir as LiveData<File>
-
- data class NavigationStackEntry(val dir: File, val recyclerViewState: Parcelable?)
-
- private val navigationStack = Stack<NavigationStackEntry>()
-
- fun navigateTo(
- newDirectory: File = root,
- listMode: ListMode = ListMode.AllEntries,
- recyclerViewState: Parcelable? = null,
- pushPreviousLocation: Boolean = true
- ) {
- if (!newDirectory.exists()) return
- require(newDirectory.isDirectory) { "Can only navigate to a directory" }
- if (pushPreviousLocation) {
- navigationStack.push(NavigationStackEntry(_currentDir.value!!, recyclerViewState))
- }
- searchAction.postValue(
- makeSearchAction(
- filter = "",
- baseDirectory = newDirectory,
- filterMode = FilterMode.NoFilter,
- searchMode = SearchMode.InCurrentDirectoryOnly,
- listMode = listMode
- )
- )
- _currentDir.postValue(newDirectory)
- }
-
- val canNavigateBack
- get() = navigationStack.isNotEmpty()
-
- /**
- * Navigate back to the last location on the [navigationStack] and restore a cached scroll
- * position if possible.
- *
- * Returns the old RecyclerView's LinearLayoutManager state as a [Parcelable] if it was cached.
- */
- fun navigateBack(): Parcelable? {
- if (!canNavigateBack) return null
- val (oldDir, oldRecyclerViewState) = navigationStack.pop()
- navigateTo(oldDir, pushPreviousLocation = false)
- return oldRecyclerViewState
- }
-
- fun reset() {
- navigationStack.clear()
- forceUpdateOnNextSearchAction()
- navigateTo(pushPreviousLocation = false)
- }
-
- fun search(
- filter: String,
- baseDirectory: File? = null,
- filterMode: FilterMode = FilterMode.Fuzzy,
- searchMode: SearchMode? = null,
- listMode: ListMode = ListMode.AllEntries
- ) {
- require(baseDirectory?.isDirectory != false) { "Can only search in a directory" }
- searchAction.postValue(
- makeSearchAction(
- filter = filter,
- baseDirectory = baseDirectory ?: _currentDir.value!!,
- filterMode = filterMode,
- searchMode = searchMode ?: defaultSearchMode,
- listMode = listMode
- )
- )
- }
-
- fun forceRefresh() {
- forceUpdateOnNextSearchAction()
- searchAction.postValue(updateSearchAction(searchAction.value!!))
- }
-
- companion object {
-
- @VisibleForTesting
- fun generateStrictDomainRegex(domain: String): Regex? {
- // Valid domains do not contain path separators.
- if (domain.contains('/'))
- return null
- // Matches the start of a path component, which is either the start of the
- // string or a path separator.
- val prefix = """(?:^|/)"""
- val escapedFilter = Regex.escape(domain.replace("/", ""))
- // Matches either the filter literally or a strict subdomain of the filter term.
- // We allow a lot of freedom in what a subdomain is, as long as it is not an
- // email address.
- val subdomain = """(?:(?:[^/@]+\.)?$escapedFilter)"""
- // Matches the end of a path component, which is either the literal ".gpg" or a
- // path separator.
- val suffix = """(?:\.gpg|/)"""
- // Match any relative path with a component that is a subdomain of the filter.
- return Regex(prefix + subdomain + suffix)
- }
+ searchAction.postValue(
+ makeSearchAction(
+ filter = "",
+ baseDirectory = newDirectory,
+ filterMode = FilterMode.NoFilter,
+ searchMode = SearchMode.InCurrentDirectoryOnly,
+ listMode = listMode
+ )
+ )
+ _currentDir.postValue(newDirectory)
+ }
+
+ val canNavigateBack
+ get() = navigationStack.isNotEmpty()
+
+ /**
+ * Navigate back to the last location on the [navigationStack] and restore a cached scroll
+ * position if possible.
+ *
+ * Returns the old RecyclerView's LinearLayoutManager state as a [Parcelable] if it was cached.
+ */
+ fun navigateBack(): Parcelable? {
+ if (!canNavigateBack) return null
+ val (oldDir, oldRecyclerViewState) = navigationStack.pop()
+ navigateTo(oldDir, pushPreviousLocation = false)
+ return oldRecyclerViewState
+ }
+
+ fun reset() {
+ navigationStack.clear()
+ forceUpdateOnNextSearchAction()
+ navigateTo(pushPreviousLocation = false)
+ }
+
+ fun search(
+ filter: String,
+ baseDirectory: File? = null,
+ filterMode: FilterMode = FilterMode.Fuzzy,
+ searchMode: SearchMode? = null,
+ listMode: ListMode = ListMode.AllEntries
+ ) {
+ require(baseDirectory?.isDirectory != false) { "Can only search in a directory" }
+ searchAction.postValue(
+ makeSearchAction(
+ filter = filter,
+ baseDirectory = baseDirectory ?: _currentDir.value!!,
+ filterMode = filterMode,
+ searchMode = searchMode ?: defaultSearchMode,
+ listMode = listMode
+ )
+ )
+ }
+
+ fun forceRefresh() {
+ forceUpdateOnNextSearchAction()
+ searchAction.postValue(updateSearchAction(searchAction.value!!))
+ }
+
+ companion object {
+
+ @VisibleForTesting
+ fun generateStrictDomainRegex(domain: String): Regex? {
+ // Valid domains do not contain path separators.
+ if (domain.contains('/')) return null
+ // Matches the start of a path component, which is either the start of the
+ // string or a path separator.
+ val prefix = """(?:^|/)"""
+ val escapedFilter = Regex.escape(domain.replace("/", ""))
+ // Matches either the filter literally or a strict subdomain of the filter term.
+ // We allow a lot of freedom in what a subdomain is, as long as it is not an
+ // email address.
+ val subdomain = """(?:(?:[^/@]+\.)?$escapedFilter)"""
+ // Matches the end of a path component, which is either the literal ".gpg" or a
+ // path separator.
+ val suffix = """(?:\.gpg|/)"""
+ // Match any relative path with a component that is a subdomain of the filter.
+ return Regex(prefix + subdomain + suffix)
}
+ }
}
private object PasswordItemDiffCallback : DiffUtil.ItemCallback<PasswordItem>() {
- override fun areItemsTheSame(oldItem: PasswordItem, newItem: PasswordItem) =
- oldItem.file.absolutePath == newItem.file.absolutePath
+ override fun areItemsTheSame(oldItem: PasswordItem, newItem: PasswordItem) =
+ oldItem.file.absolutePath == newItem.file.absolutePath
- override fun areContentsTheSame(oldItem: PasswordItem, newItem: PasswordItem) =
- oldItem == newItem
+ override fun areContentsTheSame(oldItem: PasswordItem, newItem: PasswordItem) = oldItem == newItem
}
open class SearchableRepositoryAdapter<T : RecyclerView.ViewHolder>(
- private val layoutRes: Int,
- private val viewHolderCreator: (view: View) -> T,
- private val viewHolderBinder: T.(item: PasswordItem) -> Unit
+ private val layoutRes: Int,
+ private val viewHolderCreator: (view: View) -> T,
+ private val viewHolderBinder: T.(item: PasswordItem) -> Unit
) : ListAdapter<PasswordItem, T>(PasswordItemDiffCallback), PopupTextProvider {
- fun <T : ItemDetailsLookup<String>> makeSelectable(
- recyclerView: RecyclerView,
- itemDetailsLookupCreator: (recyclerView: RecyclerView) -> T
- ) {
- selectionTracker = SelectionTracker.Builder(
- "SearchableRepositoryAdapter",
- recyclerView,
- itemKeyProvider,
- itemDetailsLookupCreator(recyclerView),
- StorageStrategy.createStringStorage()
- ).withSelectionPredicate(SelectionPredicates.createSelectAnything()).build().apply {
- addObserver(object : SelectionTracker.SelectionObserver<String>() {
- override fun onSelectionChanged() {
- this@SearchableRepositoryAdapter.onSelectionChangedListener?.invoke(
- requireSelectionTracker().selection
- )
- }
- })
+ fun <T : ItemDetailsLookup<String>> makeSelectable(
+ recyclerView: RecyclerView,
+ itemDetailsLookupCreator: (recyclerView: RecyclerView) -> T
+ ) {
+ selectionTracker =
+ SelectionTracker.Builder(
+ "SearchableRepositoryAdapter",
+ recyclerView,
+ itemKeyProvider,
+ itemDetailsLookupCreator(recyclerView),
+ StorageStrategy.createStringStorage()
+ )
+ .withSelectionPredicate(SelectionPredicates.createSelectAnything())
+ .build()
+ .apply {
+ addObserver(
+ object : SelectionTracker.SelectionObserver<String>() {
+ override fun onSelectionChanged() {
+ this@SearchableRepositoryAdapter.onSelectionChangedListener?.invoke(requireSelectionTracker().selection)
+ }
+ }
+ )
}
+ }
+
+ private var onItemClickedListener: ((holder: T, item: PasswordItem) -> Unit)? = null
+ open fun onItemClicked(listener: (holder: T, item: PasswordItem) -> Unit): SearchableRepositoryAdapter<T> {
+ check(onItemClickedListener == null) { "Only a single listener can be registered for onItemClicked" }
+ onItemClickedListener = listener
+ return this
+ }
+
+ private var onSelectionChangedListener: ((selection: Selection<String>) -> Unit)? = null
+ open fun onSelectionChanged(listener: (selection: Selection<String>) -> Unit): SearchableRepositoryAdapter<T> {
+ check(onSelectionChangedListener == null) { "Only a single listener can be registered for onSelectionChanged" }
+ onSelectionChangedListener = listener
+ return this
+ }
+
+ private val itemKeyProvider =
+ object : ItemKeyProvider<String>(SCOPE_MAPPED) {
+ override fun getKey(position: Int) = getItem(position).stableId
+
+ override fun getPosition(key: String) =
+ (0 until itemCount).firstOrNull { getItem(it).stableId == key } ?: RecyclerView.NO_POSITION
}
- private var onItemClickedListener: ((holder: T, item: PasswordItem) -> Unit)? = null
- open fun onItemClicked(listener: (holder: T, item: PasswordItem) -> Unit): SearchableRepositoryAdapter<T> {
- check(onItemClickedListener == null) { "Only a single listener can be registered for onItemClicked" }
- onItemClickedListener = listener
- return this
- }
-
- private var onSelectionChangedListener: ((selection: Selection<String>) -> Unit)? = null
- open fun onSelectionChanged(listener: (selection: Selection<String>) -> Unit): SearchableRepositoryAdapter<T> {
- check(onSelectionChangedListener == null) { "Only a single listener can be registered for onSelectionChanged" }
- onSelectionChangedListener = listener
- return this
- }
-
- private val itemKeyProvider = object : ItemKeyProvider<String>(SCOPE_MAPPED) {
- override fun getKey(position: Int) = getItem(position).stableId
+ private var selectionTracker: SelectionTracker<String>? = null
+ fun requireSelectionTracker() = selectionTracker!!
- override fun getPosition(key: String) =
- (0 until itemCount).firstOrNull { getItem(it).stableId == key }
- ?: RecyclerView.NO_POSITION
- }
-
- private var selectionTracker: SelectionTracker<String>? = null
- fun requireSelectionTracker() = selectionTracker!!
+ private val selectedFiles
+ get() = requireSelectionTracker().selection.map { File(it) }
- private val selectedFiles
- get() = requireSelectionTracker().selection.map { File(it) }
+ fun getSelectedItems() = selectedFiles.map { it.toPasswordItem() }
- fun getSelectedItems() = selectedFiles.map { it.toPasswordItem() }
+ fun getPositionForFile(file: File) = itemKeyProvider.getPosition(file.absolutePath)
- fun getPositionForFile(file: File) = itemKeyProvider.getPosition(file.absolutePath)
+ final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): T {
+ val view = LayoutInflater.from(parent.context).inflate(layoutRes, parent, false)
+ return viewHolderCreator(view)
+ }
- final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): T {
- val view = LayoutInflater.from(parent.context)
- .inflate(layoutRes, parent, false)
- return viewHolderCreator(view)
+ final override fun onBindViewHolder(holder: T, position: Int) {
+ val item = getItem(position)
+ holder.apply {
+ viewHolderBinder.invoke(this, item)
+ selectionTracker?.let { itemView.isSelected = it.isSelected(item.stableId) }
+ itemView.setOnClickListener {
+ // Do not emit custom click events while the user is selecting items.
+ if (selectionTracker?.hasSelection() != true) onItemClickedListener?.invoke(holder, item)
+ }
}
+ }
- final override fun onBindViewHolder(holder: T, position: Int) {
- val item = getItem(position)
- holder.apply {
- viewHolderBinder.invoke(this, item)
- selectionTracker?.let { itemView.isSelected = it.isSelected(item.stableId) }
- itemView.setOnClickListener {
- // Do not emit custom click events while the user is selecting items.
- if (selectionTracker?.hasSelection() != true)
- onItemClickedListener?.invoke(holder, item)
- }
- }
- }
-
- final override fun getPopupText(position: Int): String {
- return getItem(position).name[0].toString().toUpperCase(Locale.getDefault())
- }
+ final override fun getPopupText(position: Int): String {
+ return getItem(position).name[0].toString().toUpperCase(Locale.getDefault())
+ }
}