aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/com/zeapo/pwdstore/autofill
diff options
context:
space:
mode:
authorFabian Henneke <FabianHenneke@users.noreply.github.com>2020-04-06 22:56:52 +0200
committerGitHub <noreply@github.com>2020-04-07 02:26:52 +0530
commite3a49e2632cfda9c548728ae306ceb06374855ea (patch)
tree2f822eb194f6b4e0e74ab68f5009571d35c2a8e0 /app/src/main/java/com/zeapo/pwdstore/autofill
parent034babcbf40517e6abfce5aa7ae5ca647033f971 (diff)
downloadHardenedPass-e3a49e2632cfda9c548728ae306ceb06374855ea.tar.gz
HardenedPass-e3a49e2632cfda9c548728ae306ceb06374855ea.zip
Modernize file listing and search in AutofillFilterActivity (#683)
* WIP: Modernize file listing and search * Refactor * Implement fuzzy search * Improve ViewModel API and introduce Adapter * Integrate new search into AutofillFilterActivity and dedebounce * Improve no results layout * Reformat * Highlight origin in FileBased directory structure * Extract highlighting logic into DirectoryStructure * Trim whitespace before searching * Remove debug logging * Remove more debug logging * Organize imports * Remove imports * Update app/src/main/java/com/zeapo/pwdstore/SearchableRepositoryViewModel.kt Co-Authored-By: Harsh Shandilya <me@msfjarvis.dev> * Address review comments Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/autofill')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillPreferences.kt22
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt143
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/PasswordViewHolder.kt2
3 files changed, 85 insertions, 82 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillPreferences.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillPreferences.kt
index c1ce51e9..bedb551d 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillPreferences.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillPreferences.kt
@@ -25,8 +25,26 @@ enum class DirectoryStructure(val value: String) {
}
fun getIdentifierFor(file: File) = when (this) {
- FileBased -> file.parentFile?.name
- DirectoryBased -> file.parentFile?.parentFile?.name
+ FileBased -> file.parentFile.name
+ DirectoryBased -> file.parentFile.parentFile?.name
+ }
+
+ /**
+ * Returns the path components of [file] until right before the component that contains the
+ * origin identifier according to the current [DirectoryStructure].
+ *
+ * Examples:
+ * - /work/example.org/john@doe.org --> /work (FileBased)
+ * - /work/example.org/john@doe.org/password --> /work (DirectoryBased)
+ */
+ fun getPathToIdentifierFor(file: File) = when (this) {
+ FileBased -> file.parentFile.parent
+ DirectoryBased -> file.parentFile.parentFile?.parent
+ }
+
+ fun getAccountPartFor(file: File) = when (this) {
+ FileBased -> file.nameWithoutExtension
+ DirectoryBased -> "${file.parentFile.name}/${file.nameWithoutExtension}"
}
@RequiresApi(Build.VERSION_CODES.O)
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt
index e4517197..2bfe0ad3 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt
@@ -13,26 +13,33 @@ import android.os.Build
import android.os.Bundle
import android.view.autofill.AutofillManager
import android.widget.TextView
+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.preference.PreferenceManager
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DividerItemDecoration
-import com.afollestad.recyclical.datasource.dataSourceOf
-import com.afollestad.recyclical.setup
-import com.afollestad.recyclical.withItem
+import androidx.recyclerview.widget.LinearLayoutManager
import com.github.ajalt.timberkt.e
+import com.zeapo.pwdstore.DelegatedSearchableRepositoryAdapter
+import com.zeapo.pwdstore.FilterMode
import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.SearchMode
+import com.zeapo.pwdstore.SearchableRepositoryViewModel
import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
import com.zeapo.pwdstore.autofill.oreo.FormOrigin
import com.zeapo.pwdstore.utils.PasswordItem
-import com.zeapo.pwdstore.utils.PasswordRepository
-import java.io.File
-import java.nio.file.Paths
-import java.util.Locale
import kotlinx.android.synthetic.main.activity_oreo_autofill_filter.*
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+@FlowPreview
+@ExperimentalCoroutinesApi
@TargetApi(Build.VERSION_CODES.O)
class AutofillFilterView : AppCompatActivity() {
@@ -66,15 +73,13 @@ class AutofillFilterView : AppCompatActivity() {
}
}
- private val dataSource = dataSourceOf()
- private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
- private val sortOrder
- get() = PasswordRepository.PasswordSortOrder.getSortOrder(preferences)
-
private lateinit var formOrigin: FormOrigin
- private lateinit var repositoryRoot: File
private lateinit var directoryStructure: DirectoryStructure
+ private val model: SearchableRepositoryViewModel by viewModels {
+ ViewModelProvider.AndroidViewModelFactory(application)
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_oreo_autofill_filter)
@@ -103,7 +108,6 @@ class AutofillFilterView : AppCompatActivity() {
return
}
}
- repositoryRoot = PasswordRepository.getRepositoryDirectory(this)
directoryStructure = AutofillPreferences.directoryStructure(this)
supportActionBar?.hide()
@@ -112,35 +116,55 @@ class AutofillFilterView : AppCompatActivity() {
}
private fun bindUI() {
- // setup is an extension method provided by recyclical
- rvPassword.setup {
- withDataSource(dataSource)
- withItem<PasswordItem, PasswordViewHolder>(R.layout.oreo_autofill_filter_row) {
- onBind(::PasswordViewHolder) { _, item ->
- when (directoryStructure) {
- DirectoryStructure.FileBased -> {
- title.text = item.file.relativeTo(item.rootDir).parent
- subtitle.text = item.file.nameWithoutExtension
- }
- DirectoryStructure.DirectoryBased -> {
- title.text =
- item.file.relativeTo(item.rootDir).parentFile?.parent ?: "/INVALID"
- subtitle.text =
- Paths.get(item.file.parentFile.name, item.file.nameWithoutExtension)
- .toString()
- }
- }
- }
- onClick { decryptAndFill(item) }
+ val searchableAdapter = DelegatedSearchableRepositoryAdapter(
+ R.layout.oreo_autofill_filter_row,
+ ::PasswordViewHolder
+ ) { item ->
+ val file = item.file.relativeTo(item.rootDir)
+ val pathToIdentifier = directoryStructure.getPathToIdentifierFor(file)
+ val identifier = directoryStructure.getIdentifierFor(file) ?: "INVALID"
+ val accountPart = directoryStructure.getAccountPartFor(file)
+ title.text = buildSpannedString {
+ pathToIdentifier?.let { append("$it/") }
+ bold { underline { append(identifier) } }
}
+ subtitle.text = accountPart
+ itemView.setOnClickListener { decryptAndFill(item) }
+ }
+ rvPassword.apply {
+ adapter = searchableAdapter
+ layoutManager = LinearLayoutManager(context)
+ addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
- rvPassword.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
- search.addTextChangedListener { recursiveFilter(it.toString(), strict = false) }
- val initialFilter =
- formOrigin.getPrettyIdentifier(applicationContext, untrusted = false)
+ val initialFilter = formOrigin.getPrettyIdentifier(applicationContext, untrusted = false)
search.setText(initialFilter, TextView.BufferType.EDITABLE)
- recursiveFilter(initialFilter, strict = formOrigin is FormOrigin.Web)
+ val filterMode =
+ if (formOrigin is FormOrigin.Web) FilterMode.StrictDomain else FilterMode.Fuzzy
+ model.search(
+ initialFilter,
+ filterMode = filterMode,
+ searchMode = SearchMode.RecursivelyInSubdirectories,
+ listFilesOnly = true
+ )
+ search.addTextChangedListener {
+ model.search(
+ it.toString(),
+ filterMode = FilterMode.Fuzzy,
+ searchMode = SearchMode.RecursivelyInSubdirectories,
+ listFilesOnly = true
+ )
+ }
+ model.passwordItemsList.observe(
+ this,
+ Observer { list ->
+ searchableAdapter.submitList(list)
+ // 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()
+ })
shouldMatch.text = getString(
R.string.oreo_autofill_match_with,
@@ -172,43 +196,4 @@ class AutofillFilterView : AppCompatActivity() {
finish()
}
}
-
- private fun File.matches(filter: String, strict: Boolean): Boolean {
- return if (strict) {
- val toMatch = directoryStructure.getIdentifierFor(this) ?: return false
- // In strict mode, we match
- // * the search term exactly,
- // * subdomains of the search term,
- // * or the search term plus an arbitrary protocol.
- toMatch == filter || toMatch.endsWith(".$filter") || toMatch.endsWith("://$filter")
- } else {
- val toMatch =
- "${relativeTo(repositoryRoot).path}/$nameWithoutExtension".toLowerCase(Locale.getDefault())
- toMatch.contains(filter.toLowerCase(Locale.getDefault()))
- }
- }
-
- private fun recursiveFilter(filter: String, dir: File? = null, strict: Boolean = true) {
- // on the root the pathStack is empty
- val passwordItems = if (dir == null) {
- PasswordRepository.getPasswords(repositoryRoot, sortOrder)
- } else {
- PasswordRepository.getPasswords(dir, repositoryRoot, sortOrder)
- }
-
- for (item in passwordItems) {
- if (item.type == PasswordItem.TYPE_CATEGORY) {
- recursiveFilter(filter, item.file, strict = strict)
- } else {
- // TODO: Implement fuzzy search if strict == false?
- val matches = item.file.matches(filter, strict = strict)
- val inAdapter = dataSource.contains(item)
- if (matches && !inAdapter) {
- dataSource.add(item)
- } else if (!matches && inAdapter) {
- dataSource.remove(item)
- }
- }
- }
- }
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/PasswordViewHolder.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/PasswordViewHolder.kt
index f6ad7a4d..71c7cf46 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/PasswordViewHolder.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/PasswordViewHolder.kt
@@ -9,7 +9,7 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.zeapo.pwdstore.R
-class PasswordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+class PasswordViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val title: TextView = itemView.findViewById(R.id.title)
val subtitle: TextView = itemView.findViewById(R.id.subtitle)
}