diff options
| author | Fabian Henneke <FabianHenneke@users.noreply.github.com> | 2020-04-06 22:56:52 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-04-07 02:26:52 +0530 |
| commit | e3a49e2632cfda9c548728ae306ceb06374855ea (patch) | |
| tree | 2f822eb194f6b4e0e74ab68f5009571d35c2a8e0 /app/src/main/java/com/zeapo/pwdstore/autofill | |
| parent | 034babcbf40517e6abfce5aa7ae5ca647033f971 (diff) | |
| download | HardenedPass-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')
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) } |
