Skip to content
This repository has been archived by the owner on Dec 14, 2021. It is now read-only.

Commit

Permalink
autofill search ui (#487)
Browse files Browse the repository at this point in the history
* started on display implementation

* logic + tests in place, ui still needs tweaking

* mostly working, last entry still cut off when scrolling

* searching working

* no entries in lockbox case

* addressed review comments
  • Loading branch information
sashei authored Mar 8, 2019
1 parent daa5b82 commit c34a3ba
Show file tree
Hide file tree
Showing 13 changed files with 477 additions and 93 deletions.
45 changes: 38 additions & 7 deletions app/src/main/java/mozilla/lockbox/adapter/ItemListAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ package mozilla.lockbox.adapter

import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.jakewharton.rxbinding2.view.clicks
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.list_cell_item.view.*
import kotlinx.android.synthetic.main.list_cell_no_entries.view.*
import kotlinx.android.synthetic.main.list_cell_no_matching.view.*
import mozilla.lockbox.R
Expand All @@ -25,10 +27,18 @@ import mozilla.lockbox.view.ItemViewHolder

open class ItemListCell(override val containerView: View) : RecyclerView.ViewHolder(containerView), LayoutContainer

class ItemListAdapter : RecyclerView.Adapter<ItemListCell>() {
sealed class ItemListAdapterType {
object ItemList : ItemListAdapterType()
object Filter : ItemListAdapterType()
object AutofillFilter : ItemListAdapterType()
}

class ItemListAdapter(
val type: ItemListAdapterType
) : RecyclerView.Adapter<ItemListCell>() {

private var itemList: List<ItemViewModel>? = null
private var isFiltering: Boolean = false
private var displayNoEntries: Boolean = true
val itemClicks: Observable<ItemViewModel> = PublishSubject.create()
val noEntriesClicks: Observable<Unit> = PublishSubject.create()
val noMatchingEntriesClicks: Observable<Unit> = PublishSubject.create()
Expand All @@ -37,6 +47,7 @@ class ItemListAdapter : RecyclerView.Adapter<ItemListCell>() {
private const val ITEM_DISPLAY_CELL_TYPE = 0
private const val NO_MATCHING_ENTRIES_CELL_TYPE = 1
private const val NO_ENTRIES_CELL_TYPE = 2
private const val SIMPLE_NO_ENTRIES_CELL_TYPE = 3
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemListCell {
Expand All @@ -60,6 +71,9 @@ class ItemListAdapter : RecyclerView.Adapter<ItemListCell>() {

return ItemListCell(view)
}
SIMPLE_NO_ENTRIES_CELL_TYPE -> {
return ItemListCell(inflater.inflate(R.layout.list_cell_no_entries_found, parent, false))
}
else -> {
val view = inflater.inflate(R.layout.list_cell_item, parent, false)

Expand All @@ -70,6 +84,10 @@ class ItemListAdapter : RecyclerView.Adapter<ItemListCell>() {
.filterNotNull()
.subscribe(this.itemClicks as Subject)

if (type is ItemListAdapterType.AutofillFilter) {
view.disclosureIndicator.visibility = GONE
}

return viewHolder
}
}
Expand All @@ -78,6 +96,11 @@ class ItemListAdapter : RecyclerView.Adapter<ItemListCell>() {
override fun getItemCount(): Int {
val list = itemList ?: return 0
val count = list.count()

if (count == 0 && !displayNoEntries) {
return 0
}

return if (count == 0) 1 else count
}

Expand All @@ -92,17 +115,25 @@ class ItemListAdapter : RecyclerView.Adapter<ItemListCell>() {

override fun getItemViewType(position: Int): Int {
val count = itemList?.count() ?: 0
return when (count) {
0 -> if (isFiltering) NO_MATCHING_ENTRIES_CELL_TYPE else NO_ENTRIES_CELL_TYPE
else -> ITEM_DISPLAY_CELL_TYPE
if (count > 0) {
return ITEM_DISPLAY_CELL_TYPE
}

return when (type) {
is ItemListAdapterType.ItemList -> NO_ENTRIES_CELL_TYPE
is ItemListAdapterType.Filter -> NO_MATCHING_ENTRIES_CELL_TYPE
is ItemListAdapterType.AutofillFilter -> SIMPLE_NO_ENTRIES_CELL_TYPE
}
}

fun updateItems(newItems: List<ItemViewModel>, isFiltering: Boolean = false) {
this.isFiltering = isFiltering
fun updateItems(newItems: List<ItemViewModel>) {
itemList = newItems
// note: this is not a performant way to do updates; we should think about using
// diffutil here when implementing filtering / sorting
notifyDataSetChanged()
}

fun displayNoEntries(enabled: Boolean) {
displayNoEntries = enabled
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,80 @@
package mozilla.lockbox.presenter

import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.Consumer
import io.reactivex.rxkotlin.Observables
import io.reactivex.rxkotlin.addTo
import mozilla.appservices.logins.ServerPassword
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.lockbox.action.AutofillAction
import mozilla.lockbox.extensions.mapToItemViewModelList
import mozilla.lockbox.flux.Dispatcher
import mozilla.lockbox.flux.Presenter
import mozilla.lockbox.model.ItemViewModel
import mozilla.lockbox.store.DataStore

interface AutofillFilterView {
val fillMeButtonClicks: Observable<Unit>
val onDismiss: Observable<Unit>
// val filterTextEntered: Observable<CharSequence>
// val filterText: Consumer<in CharSequence>
// val cancelButtonClicks: Observable<Unit>
// val cancelButtonVisibility: Consumer<in Boolean>
// val itemSelection: Observable<ItemViewModel>
// fun updateItems(items: List<ItemViewModel>)
val filterTextEntered: Observable<CharSequence>
val filterText: Consumer<in CharSequence>
val cancelButtonClicks: Observable<Unit>
val cancelButtonVisibility: Consumer<in Boolean>
val itemSelection: Observable<ItemViewModel>
fun updateItems(items: List<ItemViewModel>)
fun displayNoEntries(enabled: Boolean)
}

@ExperimentalCoroutinesApi
class AutofillFilterPresenter(
val view: AutofillFilterView,
val dispatcher: Dispatcher = Dispatcher.shared,
val dataStore: DataStore = DataStore.shared
) : Presenter() {
override fun onViewReady() {
view.fillMeButtonClicks
.map {
AutofillAction.Complete(
ServerPassword("bllah",
"www.blah.com",
"cats@cats.com",
"dawgzone")
)
}
view.onDismiss
.map { AutofillAction.Cancel }
.subscribe(dispatcher::dispatch)
.addTo(compositeDisposable)

view.onDismiss
val itemViewModelList = dataStore.list.mapToItemViewModelList()

val filteredItems = Observables.combineLatest(view.filterTextEntered, itemViewModelList)
.map { pair ->
Pair(pair.first, pair.second.filter {
it.title.contains(pair.first, true) ||
it.subtitle.contains(pair.first, true)
})
}

filteredItems
.map { if (it.first.isEmpty()) emptyList() else it.second }
.observeOn(AndroidSchedulers.mainThread())
.subscribe(view::updateItems)
.addTo(compositeDisposable)

filteredItems
.map { !it.first.isEmpty() || it.second.isEmpty() }
.subscribe(view::displayNoEntries)
.addTo(compositeDisposable)

view.filterTextEntered
.map { it.isNotEmpty() }
.observeOn(AndroidSchedulers.mainThread())
.subscribe(view.cancelButtonVisibility)
.addTo(compositeDisposable)

view.cancelButtonClicks
.map { "" }
.observeOn(AndroidSchedulers.mainThread())
.subscribe(view.filterText)
.addTo(compositeDisposable)

view.itemSelection
.switchMap { dataStore.get(it.guid) }
.map {
AutofillAction.Cancel
it.value?.let { serverPassword ->
AutofillAction.Complete(serverPassword)
} ?: AutofillAction.Cancel
}
.subscribe(dispatcher::dispatch)
.addTo(compositeDisposable)
Expand Down
77 changes: 54 additions & 23 deletions app/src/main/java/mozilla/lockbox/view/AutofillFilterFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,55 +6,86 @@

package mozilla.lockbox.view

import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.jakewharton.rxbinding2.view.clicks
import com.jakewharton.rxbinding2.view.visibility
import com.jakewharton.rxbinding2.widget.text
import com.jakewharton.rxbinding2.widget.textChanges
import io.reactivex.Observable
import io.reactivex.functions.Consumer
import io.reactivex.subjects.PublishSubject
import kotlinx.android.synthetic.main.fragment_autofill_filter.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.lockbox.R
import mozilla.lockbox.adapter.ItemListAdapter
import mozilla.lockbox.adapter.ItemListAdapterType
import mozilla.lockbox.model.ItemViewModel
import mozilla.lockbox.presenter.AutofillFilterPresenter
import mozilla.lockbox.presenter.AutofillFilterView

@ExperimentalCoroutinesApi
class AutofillFilterFragment : DialogFragment(), AutofillFilterView {
override val onDismiss: Observable<Unit> = PublishSubject.create<Unit>()
private var isEnablingDismissed: Boolean = true
val adapter = ItemListAdapter(ItemListAdapterType.AutofillFilter)

override val onDismiss: Observable<Unit> = PublishSubject.create<Unit>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
presenter = AutofillFilterPresenter(this)
val view = inflater.inflate(R.layout.fragment_autofill_filter, container, false)

val layoutManager = LinearLayoutManager(context)
view.entriesView.layoutManager = layoutManager
view.entriesView.adapter = adapter
retainInstance = true
return inflater.inflate(R.layout.fragment_autofill_filter, container, false)
return view
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(androidx.fragment.app.DialogFragment.STYLE_NORMAL, R.style.NoTitleDialog)
}

override val fillMeButtonClicks: Observable<Unit>
get() = view!!.fillMePlaceholder.clicks()

// override val filterTextEntered: Observable<CharSequence>
// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
// override val filterText: Consumer<in CharSequence>
// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
// override val cancelButtonClicks: Observable<Unit>
// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
// override val cancelButtonVisibility: Consumer<in Boolean>
// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
// override val itemSelection: Observable<ItemViewModel>
// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
//
// override fun updateItems(items: List<ItemViewModel>) {
// TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
// }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
super.onViewCreated(view, savedInstanceState)
}

override fun onResume() {
super.onResume()
view!!.filterField.requestFocus()

val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(view!!.filterField, InputMethodManager.SHOW_IMPLICIT)
}

override val filterTextEntered: Observable<CharSequence>
get() = view!!.filterField.textChanges()

override val filterText: Consumer<in CharSequence>
get() = view!!.filterField.text()
override val cancelButtonClicks: Observable<Unit>
get() = view!!.cancelButton.clicks()
override val cancelButtonVisibility: Consumer<in Boolean>
get() = view!!.cancelButton.visibility()
override val itemSelection: Observable<ItemViewModel>
get() = adapter.itemClicks

override fun updateItems(items: List<ItemViewModel>) {
adapter.updateItems(items)
}

override fun displayNoEntries(enabled: Boolean) {
adapter.displayNoEntries(enabled)
}

override fun onDestroyView() {
if (isEnablingDismissed) {
(onDismiss as PublishSubject).onNext(Unit)
}
(onDismiss as PublishSubject).onNext(Unit)

val dialog = dialog
// handles https://code.google.com/p/android/issues/detail?id=17423
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/mozilla/lockbox/view/FilterFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ import kotlinx.android.synthetic.main.include_backable_filter.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.lockbox.R
import mozilla.lockbox.adapter.ItemListAdapter
import mozilla.lockbox.adapter.ItemListAdapterType
import mozilla.lockbox.model.ItemViewModel
import mozilla.lockbox.presenter.FilterPresenter
import mozilla.lockbox.presenter.FilterView

@ExperimentalCoroutinesApi
class FilterFragment : BackableFragment(), FilterView {
val adapter = ItemListAdapter()
val adapter = ItemListAdapter(ItemListAdapterType.Filter)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
Expand Down Expand Up @@ -82,6 +83,6 @@ class FilterFragment : BackableFragment(), FilterView {
get() = adapter.noMatchingEntriesClicks

override fun updateItems(items: List<ItemViewModel>) {
adapter.updateItems(items, true)
adapter.updateItems(items)
}
}
1 change: 0 additions & 1 deletion app/src/main/java/mozilla/lockbox/view/FxALoginFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import com.jakewharton.rxbinding2.view.clicks
import io.reactivex.Observable
import kotlinx.android.synthetic.main.fragment_fxa_login.*
import kotlinx.android.synthetic.main.fragment_fxa_login.view.*
import kotlinx.android.synthetic.main.fragment_warning.view.*
import kotlinx.android.synthetic.main.include_backable.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.lockbox.R
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/mozilla/lockbox/view/ItemListFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ import io.reactivex.subjects.PublishSubject
import jp.wasabeef.picasso.transformations.CropCircleTransformation
import kotlinx.android.synthetic.main.fragment_item_list.*
import kotlinx.android.synthetic.main.fragment_item_list.view.*
import kotlinx.android.synthetic.main.fragment_warning.view.*
import kotlinx.android.synthetic.main.nav_header.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.lockbox.R
import mozilla.lockbox.action.Setting
import mozilla.lockbox.adapter.ItemListAdapter
import mozilla.lockbox.adapter.ItemListAdapterType
import mozilla.lockbox.adapter.SortItemAdapter
import mozilla.lockbox.model.AccountViewModel
import mozilla.lockbox.model.ItemViewModel
Expand All @@ -50,7 +50,7 @@ import mozilla.lockbox.support.showAndRemove
@ExperimentalCoroutinesApi
class ItemListFragment : Fragment(), ItemListView {
private val compositeDisposable = CompositeDisposable()
private val adapter = ItemListAdapter()
private val adapter = ItemListAdapter(ItemListAdapterType.ItemList)
private val errorHelper = NetworkErrorHelper()

private lateinit var spinner: Spinner
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/res/drawable/ic_close_black.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!--
~ This Source Code Form is subject to the terms of the Mozilla Public
~ License, v. 2.0. If a copy of the MPL was not distributed with this
~ file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->

<vector android:alpha="0.54" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>
Loading

0 comments on commit c34a3ba

Please sign in to comment.