aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-04 11:23:25 +0100
committerDanilo M. <danix@danix.xyz>2026-11-04 11:23:25 +0100
commitfa95b737778ab08203c679d716c1279f51cbd409 (patch)
treeb5de9fa41937d2007d8dc30576f6be59c5d74a94 /app/src/main/java
parentf5a736755a151be6753a2f939c493a7f9ad5bf06 (diff)
downloadHardenedPass-fa95b737778ab08203c679d716c1279f51cbd409.tar.gz
HardenedPass-fa95b737778ab08203c679d716c1279f51cbd409.zip
feat(app): move the autofill picker to Compose
AutofillFilterView draws the picker with the shared theme: origin header, search field, entry rows in a grouped surface and the three switches, over the same SearchableRepositoryViewModel search. Rows name the origin identifier and show the account and folders below it. The first tap wins, so a double tap no longer starts two decryptions. PasswordViewHolder, the RecyclerView adapter, the two layouts, their drawables, the multiselect color and their lint baseline entries go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterScreen.kt144
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt178
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/PasswordViewHolder.kt16
-rw-r--r--app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt127
4 files changed, 215 insertions, 250 deletions
diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterScreen.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterScreen.kt
new file mode 100644
index 00000000..49e19799
--- /dev/null
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterScreen.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package app.passwordstore.ui.autofill
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import app.passwordstore.R
+import app.passwordstore.ui.compose.EntryRow
+import app.passwordstore.ui.compose.GroupedSurface
+import app.passwordstore.ui.compose.theme.SpacingLarge
+import app.passwordstore.ui.list.ListRow
+import app.passwordstore.ui.settings.PreferenceSwitch
+import kotlinx.collections.immutable.ImmutableList
+
+/**
+ * The autofill picker: which site or app is being filled ([origin], as the user can check it), a
+ * search over the store's entries, the matching [rows] (null while the first search runs), and the
+ * switches for phishing-resistant search ([showStrict]: websites only), recording the pick as a
+ * match ([matchLabel]) and clearing earlier matches. [onPick] takes the entry's absolute path.
+ */
+@Composable
+fun AutofillFilterScreen(
+ origin: String,
+ query: String,
+ rows: ImmutableList<ListRow>?,
+ showStrict: Boolean,
+ strict: Boolean,
+ matchLabel: String,
+ match: Boolean,
+ clear: Boolean,
+ onQueryChange: (String) -> Unit,
+ onStrictChange: (Boolean) -> Unit,
+ onMatchChange: (Boolean) -> Unit,
+ onClearChange: (Boolean) -> Unit,
+ onPick: (path: String) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val fillInto = stringResource(R.string.oreo_autofill_select_and_fill_into)
+ Surface(modifier.fillMaxSize()) {
+ Column(Modifier.padding(vertical = SpacingLarge)) {
+ Image(
+ painterResource(R.drawable.ic_launcher_foreground),
+ contentDescription = stringResource(R.string.app_name),
+ modifier = Modifier.size(60.dp).align(Alignment.CenterHorizontally),
+ )
+ Text(
+ buildAnnotatedString {
+ append(fillInto)
+ append("\n")
+ withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append(origin) }
+ },
+ modifier = Modifier.fillMaxWidth().padding(horizontal = SpacingLarge),
+ textAlign = TextAlign.Center,
+ )
+ OutlinedTextField(
+ value = query,
+ onValueChange = onQueryChange,
+ modifier = Modifier.fillMaxWidth().padding(SpacingLarge),
+ label = { Text(stringResource(R.string.action_search)) },
+ leadingIcon = {
+ Icon(painterResource(R.drawable.ic_search_24dp), contentDescription = null)
+ },
+ trailingIcon =
+ if (query.isNotEmpty()) {
+ {
+ IconButton(onClick = { onQueryChange("") }) {
+ Icon(Icons.Filled.Close, stringResource(R.string.list_clear_search))
+ }
+ }
+ } else null,
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
+ )
+ Box(Modifier.weight(1f).fillMaxWidth()) {
+ when {
+ rows == null -> Unit
+ rows.isEmpty() ->
+ Text(
+ stringResource(R.string.oreo_autofill_filter_no_results),
+ modifier = Modifier.padding(horizontal = SpacingLarge),
+ style = MaterialTheme.typography.titleMedium,
+ )
+ else ->
+ // One lazy item per row: a cleared search lists the whole store. The surface wraps
+ // a short list and is capped by the box for a long one.
+ GroupedSurface(Modifier.fillMaxWidth()) {
+ LazyColumn {
+ items(rows, key = { it.path }) { row ->
+ EntryRow(
+ name = row.label,
+ subtitle = row.subtitle,
+ onClick = { onPick(row.path) },
+ )
+ }
+ }
+ }
+ }
+ }
+ if (showStrict)
+ PreferenceSwitch(
+ stringResource(R.string.oreo_autofill_strict_domain_search),
+ strict,
+ onStrictChange,
+ )
+ PreferenceSwitch(matchLabel, match, onMatchChange)
+ PreferenceSwitch(
+ stringResource(R.string.oreo_autofill_matches_clear_existing),
+ clear,
+ onClearChange,
+ )
+ }
+ }
+}
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 b027657e..44994646 100644
--- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillFilterView.kt
@@ -10,36 +10,35 @@ import android.content.Intent
import android.content.IntentSender
import android.os.Build
import android.os.Bundle
-import android.view.View
import android.view.autofill.AutofillManager
-import android.widget.TextView
+import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
-import androidx.core.text.bold
-import androidx.core.text.buildSpannedString
-import androidx.core.text.underline
-import androidx.core.widget.addTextChangedListener
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.passwordstore.R
-import app.passwordstore.data.password.PasswordItem
-import app.passwordstore.databinding.ActivityOreoAutofillFilterBinding
+import app.passwordstore.data.repo.PasswordRepository
+import app.passwordstore.ui.compose.theme.APSTheme
+import app.passwordstore.ui.list.ListRow
import app.passwordstore.util.autofill.AutofillMatcher
import app.passwordstore.util.autofill.AutofillPreferences
import app.passwordstore.util.coroutines.DispatcherProvider
-import app.passwordstore.util.extensions.viewBinding
-import app.passwordstore.util.settings.DirectoryStructure
import app.passwordstore.util.viewmodel.FilterMode
import app.passwordstore.util.viewmodel.ListMode
import app.passwordstore.util.viewmodel.SearchMode
-import app.passwordstore.util.viewmodel.SearchableRepositoryAdapter
import app.passwordstore.util.viewmodel.SearchableRepositoryViewModel
import com.github.androidpasswordstore.autofillparser.FormOrigin
import dagger.hilt.android.AndroidEntryPoint
+import java.io.File
import javax.inject.Inject
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.launch
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
import logcat.LogPriority.ERROR
import logcat.logcat
@@ -85,11 +84,12 @@ class AutofillFilterView : AppCompatActivity() {
}
private lateinit var formOrigin: FormOrigin
- private lateinit var directoryStructure: DirectoryStructure
- private val binding by viewBinding(ActivityOreoAutofillFilterBinding::inflate)
private val model: SearchableRepositoryViewModel by viewModels()
+ /** Set by the first tap on a row: a double tap must not start a second decryption. */
+ private var picked = false
+
private val decryptAction =
registerForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
@@ -100,7 +100,6 @@ class AutofillFilterView : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(binding.root)
setFinishOnTouchOutside(true)
val params = window.attributes
@@ -129,111 +128,76 @@ class AutofillFilterView : AppCompatActivity() {
return
}
}
- directoryStructure = AutofillPreferences.directoryStructure(this)
-
supportActionBar?.hide()
- bindUI()
- updateSearch()
setResult(RESULT_CANCELED)
- }
- private fun bindUI() {
- with(binding) {
- rvPassword.apply {
- adapter =
- SearchableRepositoryAdapter(
- R.layout.oreo_autofill_filter_row,
- ::PasswordViewHolder,
- lifecycleScope,
- dispatcherProvider,
- ) { item, _ ->
- val file = item.file.relativeTo(item.rootDir)
- val pathToIdentifier = directoryStructure.getPathToIdentifierFor(file)
- val identifier = directoryStructure.getIdentifierFor(file)
- val accountPart = directoryStructure.getAccountPartFor(file)
- check(identifier != null || accountPart != null) {
- "At least one of identifier and accountPart should always be non-null"
- }
- title.text =
- if (identifier != null) {
- buildSpannedString {
- if (pathToIdentifier != null) append("$pathToIdentifier/")
- bold { underline { append(identifier) } }
- }
- } else {
- accountPart
- }
- subtitle.apply {
- if (identifier != null && accountPart != null) {
- text = accountPart
- visibility = View.VISIBLE
- } else {
- visibility = View.GONE
- }
- }
- }
- .onItemClicked { _, item -> decryptAndFill(item) }
- layoutManager = LinearLayoutManager(context)
- }
- search.apply {
- val initialSearch = formOrigin.getPrettyIdentifier(applicationContext, untrusted = false)
- setText(initialSearch, TextView.BufferType.EDITABLE)
- addTextChangedListener { updateSearch() }
- }
- origin.text = buildSpannedString {
- append(getString(R.string.oreo_autofill_select_and_fill_into))
- append("\n")
- bold { append(formOrigin.getPrettyIdentifier(applicationContext, untrusted = true)) }
- }
- strictDomainSearch.apply {
- visibility = if (formOrigin is FormOrigin.Web) View.VISIBLE else View.GONE
- isChecked = formOrigin is FormOrigin.Web
- setOnCheckedChangeListener { _, _ -> updateSearch() }
- }
- shouldMatch.text =
- getString(
- R.string.oreo_autofill_match_with,
- formOrigin.getPrettyIdentifier(applicationContext),
+ val isWeb = formOrigin is FormOrigin.Web
+ val initialQuery = formOrigin.getPrettyIdentifier(applicationContext, untrusted = false)
+ // Before anything is composed, so the ViewModel's default root listing (folders included) is
+ // never shown. The LaunchedEffect below repeats it with equal arguments, which emits nothing.
+ search(initialQuery, isWeb)
+ val root = PasswordRepository.getRepositoryDirectory()
+ val directoryStructure = AutofillPreferences.directoryStructure(this)
+ // ponytail: cold flow, so each return to STARTED walks the store again; fine for a picker open
+ // a few seconds. Share it with stateIn in the ViewModel if a large store makes it slow.
+ val rows =
+ model.searchResult
+ .map { result ->
+ autofillRows(result.passwordItems.map { it.file }, root, directoryStructure)
+ }
+ .flowOn(dispatcherProvider.io())
+ val origin = formOrigin.getPrettyIdentifier(applicationContext, untrusted = true)
+ val matchLabel =
+ getString(
+ R.string.oreo_autofill_match_with,
+ formOrigin.getPrettyIdentifier(applicationContext),
+ )
+
+ setContent {
+ APSTheme {
+ var query by rememberSaveable { mutableStateOf(initialQuery) }
+ var strict by rememberSaveable { mutableStateOf(isWeb) }
+ var match by rememberSaveable { mutableStateOf(true) }
+ var clear by rememberSaveable { mutableStateOf(false) }
+ LaunchedEffect(query, strict) { search(query, strict) }
+ val shown by rows.collectAsStateWithLifecycle<ImmutableList<ListRow>?>(initialValue = null)
+ AutofillFilterScreen(
+ origin = origin,
+ query = query,
+ rows = shown,
+ showStrict = isWeb,
+ strict = strict,
+ matchLabel = matchLabel,
+ match = match,
+ clear = clear,
+ onQueryChange = { query = it },
+ onStrictChange = { strict = it },
+ onMatchChange = { match = it },
+ onClearChange = { clear = it },
+ onPick = { path -> decryptAndFill(File(path), match, clear) },
)
- lifecycleScope.launch { handleSearchResults() }
- }
- }
-
- private suspend fun handleSearchResults() {
- model.searchResult.collect { result ->
- val list = result.passwordItems
- (binding.rvPassword.adapter as SearchableRepositoryAdapter).submitList(list) {
- binding.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() && binding.rvPasswordSwitcher.nextView.id == binding.rvPasswordEmpty.id) ||
- (list.isNotEmpty() && binding.rvPasswordSwitcher.nextView.id == binding.rvPassword.id)
- ) {
- binding.rvPasswordSwitcher.showNext()
}
}
}
- private fun updateSearch() {
+ /** The old picker's search: fuzzy or strict domain, recursive, entries only. */
+ private fun search(query: String, strict: Boolean) {
model.search(
- binding.search.text.toString().trim(),
- filterMode =
- if (binding.strictDomainSearch.isChecked) FilterMode.StrictDomain else FilterMode.Fuzzy,
+ query.trim(),
+ filterMode = if (strict) FilterMode.StrictDomain else FilterMode.Fuzzy,
searchMode = SearchMode.RecursivelyInSubdirectories,
listMode = ListMode.FilesOnly,
)
}
- private fun decryptAndFill(item: PasswordItem) {
- if (binding.shouldClear.isChecked)
- AutofillMatcher.clearMatchesFor(applicationContext, formOrigin)
- if (binding.shouldMatch.isChecked)
- AutofillMatcher.addMatchFor(applicationContext, formOrigin, item.file)
+ private fun decryptAndFill(file: File, match: Boolean, clear: Boolean) {
+ if (picked) return
+ picked = true
+ if (clear) AutofillMatcher.clearMatchesFor(applicationContext, formOrigin)
+ if (match) AutofillMatcher.addMatchFor(applicationContext, formOrigin, file)
// intent?.extras? is checked to be non-null in onCreate
decryptAction.launch(
- AutofillDecryptActivity.makeDecryptFileIntent(item.file, intent!!.extras!!, this)
+ AutofillDecryptActivity.makeDecryptFileIntent(file, intent!!.extras!!, this)
)
}
}
diff --git a/app/src/main/java/app/passwordstore/ui/autofill/PasswordViewHolder.kt b/app/src/main/java/app/passwordstore/ui/autofill/PasswordViewHolder.kt
deleted file mode 100644
index 24a28564..00000000
--- a/app/src/main/java/app/passwordstore/ui/autofill/PasswordViewHolder.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package app.passwordstore.ui.autofill
-
-import android.view.View
-import android.widget.TextView
-import androidx.recyclerview.widget.RecyclerView
-import app.passwordstore.R
-
-class PasswordViewHolder(view: View) : RecyclerView.ViewHolder(view) {
-
- val title: TextView = itemView.findViewById(R.id.title)
- val subtitle: TextView = itemView.findViewById(R.id.subtitle)
-}
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 f8cbf1e4..25ea2cf7 100644
--- a/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt
+++ b/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt
@@ -7,20 +7,8 @@ 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.recyclerview.selection.ItemDetailsLookup
-import androidx.recyclerview.selection.ItemKeyProvider
-import androidx.recyclerview.selection.Selection
-import androidx.recyclerview.selection.SelectionPredicates
-import androidx.recyclerview.selection.SelectionTracker
-import androidx.recyclerview.selection.StorageStrategy
-import androidx.recyclerview.widget.DiffUtil
-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
@@ -35,9 +23,7 @@ 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 javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -51,9 +37,7 @@ 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
private fun File.toPasswordItem() =
if (isFile) PasswordItem.newPassword(name, this, PasswordRepository.getRepositoryDirectory())
@@ -399,114 +383,3 @@ constructor(
}
}
}
-
-private object PasswordItemDiffCallback : DiffUtil.ItemCallback<PasswordItem>() {
-
- override fun areItemsTheSame(oldItem: PasswordItem, newItem: PasswordItem) =
- oldItem.file.absolutePath == newItem.file.absolutePath
-
- 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 coroutineScope: CoroutineScope,
- private val dispatcherProvider: DispatcherProvider,
- private val viewHolderBinder:
- suspend T.(item: PasswordItem, dispatcherProvider: DispatcherProvider) -> 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
- )
- }
- }
- )
- }
- }
-
- 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 selectionTracker: SelectionTracker<String>? = null
-
- fun requireSelectionTracker() = selectionTracker!!
-
- private val selectedFiles
- get() = requireSelectionTracker().selection.map { File(it) }
-
- fun getSelectedItems() = selectedFiles.map { it.toPasswordItem() }
-
- 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 onBindViewHolder(holder: T, position: Int) {
- val item = getItem(position)
- holder.apply {
- coroutineScope.launch(dispatcherProvider.mainImmediate()) {
- viewHolderBinder.invoke(holder, item, dispatcherProvider)
- }
- 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(view: View, position: Int): String {
- return getItem(position).name[0].toString().uppercase(Locale.getDefault())
- }
-}