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

Commit

Permalink
626, 627: Add kebab menu for delete (#764)
Browse files Browse the repository at this point in the history
* Add kebab menu icon and strings. Set up fragment for addition to toolbar.

* Create fragment and dialog.

* Add confirmation dialog, actions, and presenters

* Routing

* Ensure item is actually deleted and item list is refreshed before navigating back.

* Refactor and remove edit for now

* Keeping edit button, just not doing any functionality with it

* Test for item detail presenter

* Ellipsize long item titles in detail view

* Delete confirmation toast

* Delete toast and tests

* Consumable toast notification for delete

* Specify return type on Optional extension

* ItemListPresenterTest needed a consumable for the delete toast test

* Cleanup

* Fix toast notifications
  • Loading branch information
Elise Richards authored Jul 3, 2019
1 parent 297f829 commit 17605c3
Show file tree
Hide file tree
Showing 29 changed files with 478 additions and 81 deletions.
11 changes: 10 additions & 1 deletion app/src/main/java/mozilla/lockbox/action/DataStoreAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

package mozilla.lockbox.action

import mozilla.appservices.logins.ServerPassword
import mozilla.lockbox.model.SyncCredentials

sealed class DataStoreAction(
Expand All @@ -16,6 +17,14 @@ sealed class DataStoreAction(
object Unlock : DataStoreAction(TelemetryEventMethod.unlock, TelemetryEventObject.datastore)
object Reset : DataStoreAction(TelemetryEventMethod.reset, TelemetryEventObject.datastore)
object Sync : DataStoreAction(TelemetryEventMethod.sync, TelemetryEventObject.datastore)

data class Touch(val id: String) : DataStoreAction(TelemetryEventMethod.touch, TelemetryEventObject.datastore)
data class UpdateCredentials(val syncCredentials: SyncCredentials) : DataStoreAction(TelemetryEventMethod.update_credentials, TelemetryEventObject.datastore)

data class UpdateCredentials(val syncCredentials: SyncCredentials) :
DataStoreAction(TelemetryEventMethod.update_credentials, TelemetryEventObject.datastore)

data class Delete(val item: ServerPassword?) :
DataStoreAction(TelemetryEventMethod.delete, TelemetryEventObject.delete_credential)

data class Edit(val itemId: Int) : DataStoreAction(TelemetryEventMethod.edit, TelemetryEventObject.edit_credential)
}
44 changes: 32 additions & 12 deletions app/src/main/java/mozilla/lockbox/action/DialogAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

package mozilla.lockbox.action

import mozilla.appservices.logins.ServerPassword
import mozilla.lockbox.R
import mozilla.lockbox.flux.Action
import mozilla.lockbox.model.DialogViewModel
Expand All @@ -15,15 +16,17 @@ sealed class DialogAction(
val positiveButtonActionList: List<Action> = emptyList(),
val negativeButtonActionList: List<Action> = emptyList()
) : RouteAction(TelemetryEventMethod.show, TelemetryEventObject.dialog) {

object SecurityDisclaimer : DialogAction(
DialogViewModel(
R.string.no_device_security_title,
R.string.no_device_security_message,
R.string.set_up_security_button,
R.string.cancel
),
listOf(RouteAction.SystemSetting(SettingIntent.Security))
listOf(SystemSetting(SettingIntent.Security))
)

object UnlinkDisclaimer : DialogAction(
DialogViewModel(
R.string.disconnect_disclaimer_title,
Expand All @@ -34,17 +37,34 @@ sealed class DialogAction(
),
listOf(LifecycleAction.UserReset)
)

object OnboardingSecurityDialog : DialogAction(
DialogViewModel(
R.string.secure_your_device,
R.string.device_security_description,
R.string.set_up_now,
R.string.skip_button
),
listOf(
RouteAction.SystemSetting(SettingIntent.Security),
RouteAction.Login
),
listOf(RouteAction.Login)
DialogViewModel(
R.string.secure_your_device,
R.string.device_security_description,
R.string.set_up_now,
R.string.skip_button
),
listOf(
SystemSetting(SettingIntent.Security),
Login
),
listOf(Login)
)

data class DeleteConfirmationDialog(
val item: ServerPassword?
) : DialogAction(
DialogViewModel(
R.string.delete_this_login,
R.string.delete_description,
R.string.delete,
R.string.cancel,
R.color.red
),
listOf(
DataStoreAction.Delete(item),
ItemList
)
)
}
8 changes: 8 additions & 0 deletions app/src/main/java/mozilla/lockbox/action/ItemDetailAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@

package mozilla.lockbox.action

import androidx.annotation.StringRes
import mozilla.lockbox.R

sealed class ItemDetailAction(
override val eventMethod: TelemetryEventMethod,
override val eventObject: TelemetryEventObject
) : TelemetryAction {
data class TogglePassword(val displayed: Boolean)
: ItemDetailAction(TelemetryEventMethod.tap, TelemetryEventObject.reveal_password)

enum class EditItemMenu(@StringRes val titleId: Int) {
EDIT(R.string.edit),
DELETE(R.string.delete)
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/mozilla/lockbox/action/SettingAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class Setting {
val ms: Long = this.seconds * 1000
}

enum class ItemListSort(val titleId: Int, val valueId: Int) {
enum class ItemListSort(@StringRes val titleId: Int, @StringRes val valueId: Int) {
ALPHABETICALLY(R.string.all_logins_a_z, R.string.sort_menu_az),
RECENTLY_USED(R.string.all_logins_recent, R.string.sort_menu_recent)
}
Expand Down
11 changes: 8 additions & 3 deletions app/src/main/java/mozilla/lockbox/action/TelemetryAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface TelemetryAction : Action {
val extras: Map<String, Any>?
get() = null

open fun createEvent(category: String = "action"): TelemetryEvent {
fun createEvent(category: String = "action"): TelemetryEvent {
val evt = TelemetryEvent.create(
category,
eventMethod.name,
Expand Down Expand Up @@ -53,7 +53,9 @@ enum class TelemetryEventMethod {
autofill_multiple,
autofill_cancel,
autofill_error,
autofill_filter
autofill_filter,
delete,
edit
}

enum class TelemetryEventObject {
Expand Down Expand Up @@ -93,5 +95,8 @@ enum class TelemetryEventObject {
filter,
back,
dialog,
datastore
datastore,
delete_credential,
edit_credential,
entry_kebab
}
45 changes: 45 additions & 0 deletions app/src/main/java/mozilla/lockbox/adapter/DeleteItemAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package mozilla.lockbox.adapter

import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import mozilla.lockbox.R
import mozilla.lockbox.action.ItemDetailAction

class DeleteItemAdapter(
context: Context,
textViewResourceId: Int,
val values: ArrayList<ItemDetailAction.EditItemMenu>
) : ArrayAdapter<ItemDetailAction.EditItemMenu>(context, textViewResourceId, values) {

private var selectedIndex = -1

fun setSelection(position: Int) {
selectedIndex = position
notifyDataSetChanged()
}

override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val label = super.getView(position, convertView, parent) as TextView
label.setTextAppearance(R.style.TextAppearanceWidgetEventToolbarTitle)
label.setTextColor(label.resources.getColor(R.color.text_white, null))
label.setBackgroundColor(label.resources.getColor(R.color.color_primary, null))
label.text = context.resources.getString(values[position].titleId)
label.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_menu_kebab, 0)

return label
}

override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val label = super.getDropDownView(position, convertView, parent) as TextView
label.setTextAppearance(R.style.TextAppearanceSortMenuItem)
label.text = context.resources.getString(values[position].titleId)
label.background = context.resources.getDrawable(R.drawable.button_pressed_white, null)
val padding = label.resources.getDimensionPixelSize(R.dimen.sort_item_padding)
label.setPadding(padding, padding, padding, padding)

return label
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ class SectionedAdapter(
else
baseAdapter.getItemViewType(sectionedPositionToPosition(position)) + 1
}

class Section(internal var firstPosition: Int, @StringRes title: Int) {
internal var sectionedPosition: Int = 0
@StringRes var title: Int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import io.reactivex.Observable
import io.reactivex.ObservableEmitter
import kotlinx.android.synthetic.main.list_cell_setting_toggle.*
import mozilla.lockbox.R
import mozilla.lockbox.model.DialogViewModel

Expand All @@ -25,7 +26,7 @@ object AlertDialogHelper {
viewModel: DialogViewModel
): Observable<AlertState> {
return Observable.create { emitter ->
val builder = AlertDialog.Builder(context, R.style.AlertDialogStyle)
val builder = AlertDialog.Builder(context, R.style.DeleteDialogStyle)

viewModel.title?.let {
val titleString = context.getString(it)
Expand Down Expand Up @@ -62,7 +63,6 @@ object AlertDialogHelper {
setUpDismissal(builder, emitter)

val dialog = builder.create()

dialog.show()

val defaultColor = context.getColor(R.color.violet_70)
Expand Down
14 changes: 12 additions & 2 deletions app/src/main/java/mozilla/lockbox/presenter/ItemDetailPresenter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import mozilla.lockbox.R
import mozilla.lockbox.action.AppWebPageAction
import mozilla.lockbox.action.ClipboardAction
import mozilla.lockbox.action.DataStoreAction
import mozilla.lockbox.action.DialogAction
import mozilla.lockbox.action.ItemDetailAction
import mozilla.lockbox.action.RouteAction
import mozilla.lockbox.extensions.filterNotNull
Expand All @@ -33,10 +34,12 @@ interface ItemDetailView {
val togglePasswordClicks: Observable<Unit>
val hostnameClicks: Observable<Unit>
val learnMoreClicks: Observable<Unit>
val kebabMenuClicks: Observable<Unit>
var isPasswordVisible: Boolean
fun updateItem(item: ItemDetailViewModel)
fun showToastNotification(@StringRes strId: Int)
fun handleNetworkError(networkErrorVisibility: Boolean)
val menuItemSelection: Observable<ItemDetailAction.EditItemMenu>
// val retryNetworkConnectionClicks: Observable<Unit>
}

Expand Down Expand Up @@ -80,6 +83,13 @@ class ItemDetailPresenter(
}
}

view.menuItemSelection
.map {
DialogAction.DeleteConfirmationDialog(credentials)
}
.subscribe(dispatcher::dispatch)
.addTo(compositeDisposable)

this.view.learnMoreClicks
.map { AppWebPageAction.FaqEdit }
.subscribe(dispatcher::dispatch)
Expand Down Expand Up @@ -117,8 +127,8 @@ class ItemDetailPresenter(

private fun handleClicks(clicks: Observable<Unit>, withServerPassword: (ServerPassword) -> Unit) {
clicks.subscribe {
this.credentials?.let { password -> withServerPassword(password) }
}
this.credentials?.let { password -> withServerPassword(password) }
}
.addTo(compositeDisposable)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ interface ItemListView {
val isRefreshing: Boolean
fun stopRefreshing()
fun showToastNotification(@StringRes strId: Int)
fun showDeleteToastNotification(text: String)
}

@ExperimentalCoroutinesApi
Expand Down Expand Up @@ -166,6 +167,13 @@ class ItemListPresenter(
.subscribe(view::handleNetworkError)
.addTo(compositeDisposable)

dataStore.deletedItem
.subscribe {
val event = it.get() ?: return@subscribe
view.showDeleteToastNotification(event.formSubmitURL ?: event.hostname)
}
.addTo(compositeDisposable)

// TODO: make this more robust to retry loading the correct page again (loadUrl)
// view.retryNetworkConnectionClicks.subscribe {
// dispatcher.dispatch(NetworkAction.CheckConnectivity)
Expand Down
31 changes: 28 additions & 3 deletions app/src/main/java/mozilla/lockbox/store/DataStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ import mozilla.lockbox.extensions.filterByType
import mozilla.lockbox.flux.Dispatcher
import mozilla.lockbox.log
import mozilla.lockbox.model.SyncCredentials
import mozilla.lockbox.support.Consumable
import mozilla.lockbox.support.Constant
import mozilla.lockbox.support.DataStoreSupport
import mozilla.lockbox.support.FxASyncDataStoreSupport
import mozilla.lockbox.support.Optional
import mozilla.lockbox.support.TimingSupport
import mozilla.lockbox.support.asOptional
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import kotlin.coroutines.CoroutineContext
Expand Down Expand Up @@ -62,10 +62,12 @@ open class DataStore(
private val stateSubject = ReplayRelay.createWithSize<State>(1)
private val syncStateSubject = BehaviorRelay.createDefault<SyncState>(SyncState.NotSyncing)
private val listSubject: BehaviorRelay<List<ServerPassword>> = BehaviorRelay.createDefault(emptyList())
private val deletedItemSubject = ReplayRelay.create<Consumable<ServerPassword>>()

open val state: Observable<State> = stateSubject
open val syncState: Observable<SyncState> = syncStateSubject
open val list: Observable<List<ServerPassword>> get() = listSubject
open val deletedItem: Observable<Consumable<ServerPassword>> get() = deletedItemSubject

private val exceptionHandler: CoroutineExceptionHandler
get() = CoroutineExceptionHandler { _, e ->
Expand Down Expand Up @@ -107,6 +109,7 @@ open class DataStore(
is DataStoreAction.Touch -> touch(action.id)
is DataStoreAction.Reset -> reset()
is DataStoreAction.UpdateCredentials -> updateCredentials(action.syncCredentials)
is DataStoreAction.Delete -> deleteCredentials(action.item)
}
}
.addTo(compositeDisposable)
Expand All @@ -119,6 +122,26 @@ open class DataStore(
setupAutoLock()
}

private fun deleteCredentials(item: ServerPassword?) {
try {
if (item != null) {
backend.delete(item.id)
.asSingle(coroutineContext)
.subscribe()
.addTo(compositeDisposable)
sync()
deletedItemSubject.accept(Consumable(item))
}
} catch (loginsStorageException: LoginsStorageException) {
log.error("Exception: ", loginsStorageException)
}
}

private fun editEntry() {
// TODO
// dispatcher.dispatch(RouteAction.ItemList)
}

private fun shutdown() {
// rather than calling `close`, which will make the `AsyncLoginsStorage` instance unusable,
// we use the `ensureLocked` method to close the database connection.
Expand All @@ -144,8 +167,10 @@ open class DataStore(

open fun get(id: String): Observable<Optional<ServerPassword>> {
return list.map { items ->
items.findLast { item -> item.id == id }.asOptional()
}
Optional(
items.findLast { item -> item.id == id }
)
}
}

private fun touch(id: String) {
Expand Down
Loading

0 comments on commit 17605c3

Please sign in to comment.