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

Commit

Permalink
628: edit view and routing (#843)
Browse files Browse the repository at this point in the history
* Edit xml layout

* Add route, presenter, and fragment

Button clicks and dialog

Update dependencies

Edit presenter tests

Routing stuff

Usable state with spinner

Buttons working

Password visibility in edit mode

Lint

View

Create popup item and insert menu into detail view's kebab buttton

* Popup menu and click listener

Dropdown menu and formatting

Update list - in progress

DataStoreTest - mocked up test, stuck on init

Datastore update item detail test

* Save edited changes

Lint

Update dependencies

Edit unit tests

Add sync to datastore editing. Update var name in robot and screenshot tests

Reformatting dropdown menu and edit view

Adding inclusive popup to edit->detail view nav definition

Address nullable server passwords for delete and edit. Remove unused string.

Remove learn more clicks from test

Refactoring pushError into a helper method.
  • Loading branch information
Elise Richards authored Sep 3, 2019
1 parent 5a83079 commit c435688
Show file tree
Hide file tree
Showing 54 changed files with 1,368 additions and 305 deletions.
8 changes: 4 additions & 4 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@ configurations.all { config ->
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.google.android.material:material:1.1.0-alpha07'
implementation 'com.google.android.material:material:1.1.0-alpha09'
implementation "androidx.recyclerview:recyclerview:$recyclerview_version"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.exifinterface:exifinterface:1.0.0'
Expand Down Expand Up @@ -119,8 +119,8 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.2'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.2'
testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.2'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-alpha01'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.2.0-alpha01'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-alpha03'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.2.0-alpha03'
implementation "android.arch.navigation:navigation-fragment:$navigation_version"
implementation "android.arch.navigation:navigation-ui-ktx:$navigation_version"
implementation 'com.adjust.sdk:adjust-android:4.17.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class ItemDetailRobot : BaseTestRobot {
onView(withText(id)).inRoot(withDecorView(not(`is`(activityRule.activity.getWindow().decorView))))
.check(matches(isDisplayed()))

fun tapKebabMenu() = ClickActions.click { id(R.id.kebabMenu) }
fun tapKebabMenu() = ClickActions.click { id(R.id.kebabMenuButton) }
}

fun itemDetail(f: ItemDetailRobot.() -> Unit) = ItemDetailRobot().apply(f)
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ open class ScreenshotsTest {
.check(matches(isDisplayed()))
Screengrab.screenshot("item-detail-screen")

onView(withId(R.id.kebabMenu)).perform(click())
onView(withId(R.id.kebabMenuButton)).perform(click())
Screengrab.screenshot("item-menu")

onView(withText(R.string.delete)).perform(click())
Expand Down
18 changes: 14 additions & 4 deletions app/src/main/java/mozilla/lockbox/action/DataStoreAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,26 @@ sealed class DataStoreAction(
"ID: $id"
)

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

data class Delete(val item: ServerPassword?) : DataStoreAction(
/**
* Emitted when an entry is deleted from the entry's detail view or the edit view.
*/
data class Delete(val item: ServerPassword) : DataStoreAction(
TelemetryEventMethod.delete,
TelemetryEventObject.delete_credential,
item?.id
item.id
)

data class Edit(val itemId: Int) : DataStoreAction(TelemetryEventMethod.edit, TelemetryEventObject.edit_credential)
/**
* Emitted when an entry is edited and saved.
*/
data class UpdateItemDetail(val item: ServerPassword)
: DataStoreAction(
TelemetryEventMethod.edit,
TelemetryEventObject.update_credential
)
}
17 changes: 16 additions & 1 deletion app/src/main/java/mozilla/lockbox/action/DialogAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ sealed class DialogAction(
)

data class DeleteConfirmationDialog(
val item: ServerPassword?
val item: ServerPassword
) : DialogAction(
DialogViewModel(
R.string.delete_this_login,
Expand Down Expand Up @@ -91,4 +91,19 @@ sealed class DialogAction(
),
listOf(Login)
)

data class DiscardChangesDialog(
val itemId: String
) : DialogAction(
DialogViewModel(
R.string.discard_changes,
R.string.discard_changes_description,
R.string.discard,
R.string.cancel,
R.color.red
),
listOf(
ItemDetail(itemId)
)
)
}
12 changes: 2 additions & 10 deletions app/src/main/java/mozilla/lockbox/action/ItemDetailAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,10 @@

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)
}
data class TogglePassword(val displayed: Boolean) :
ItemDetailAction(TelemetryEventMethod.tap, TelemetryEventObject.reveal_password)
}
1 change: 1 addition & 0 deletions app/src/main/java/mozilla/lockbox/action/RouteAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ open class RouteAction(
object LockScreen : RouteAction(TelemetryEventMethod.show, TelemetryEventObject.lock_screen)
object Filter : RouteAction(TelemetryEventMethod.tap, TelemetryEventObject.filter)
data class ItemDetail(val id: String) : RouteAction(TelemetryEventMethod.show, TelemetryEventObject.entry_detail)
data class EditItemDetail(val id: String) : RouteAction(TelemetryEventMethod.show, TelemetryEventObject.edit_entry_detail)

// This should _only_ be triggered by pressing the back button.
object InternalBack : RouteAction(TelemetryEventMethod.tap, TelemetryEventObject.back)
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/mozilla/lockbox/action/TelemetryAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,7 @@ enum class TelemetryEventObject {
datastore,
delete_credential,
edit_credential,
entry_kebab
entry_kebab,
edit_entry_detail,
update_credential
}
45 changes: 0 additions & 45 deletions app/src/main/java/mozilla/lockbox/adapter/DeleteItemAdapter.kt

This file was deleted.

12 changes: 12 additions & 0 deletions app/src/main/java/mozilla/lockbox/presenter/AppRoutePresenter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import mozilla.lockbox.flux.Dispatcher
import mozilla.lockbox.store.RouteStore
import mozilla.lockbox.store.SettingStore
import mozilla.lockbox.view.AppWebPageFragmentArgs
import mozilla.lockbox.view.EditItemFragmentArgs
import mozilla.lockbox.view.FingerprintAuthDialogFragment
import mozilla.lockbox.view.ItemDetailFragmentArgs

Expand Down Expand Up @@ -67,6 +68,13 @@ class AppRoutePresenter(
.toBundle()
}

fun bundle(action: RouteAction.EditItemDetail): Bundle {
return EditItemFragmentArgs.Builder()
.setItemId(action.id)
.build()
.toBundle()
}

override fun route(action: RouteAction) {
activity.setTheme(R.style.AppTheme)
when (action) {
Expand All @@ -82,6 +90,7 @@ class AppRoutePresenter(
is RouteAction.LockScreen -> navigateToFragment(R.id.fragment_locked)
is RouteAction.Filter -> navigateToFragment(R.id.fragment_filter)
is RouteAction.ItemDetail -> navigateToFragment(R.id.fragment_item_detail, bundle(action))
is RouteAction.EditItemDetail -> navigateToFragment(R.id.fragment_item_edit, bundle(action))
is RouteAction.OpenWebsite -> openWebsite(action.url)
is RouteAction.SystemSetting -> openSetting(action)
is RouteAction.UnlockFallbackDialog -> showUnlockFallback(action)
Expand Down Expand Up @@ -134,6 +143,9 @@ class AppRoutePresenter(

R.id.fragment_item_detail to R.id.fragment_webview -> R.id.action_to_webview
R.id.fragment_item_detail to R.id.fragment_item_list -> R.id.action_to_itemList
R.id.fragment_item_detail to R.id.fragment_item_edit -> R.id.action_itemDetail_to_edit

R.id.fragment_item_edit to R.id.fragment_item_detail -> R.id.action_itemEdit_to_itemDetail

R.id.fragment_setting to R.id.fragment_webview -> R.id.action_to_webview

Expand Down
154 changes: 154 additions & 0 deletions app/src/main/java/mozilla/lockbox/presenter/EditItemPresenter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* 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/.
*/

package mozilla.lockbox.presenter

import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers.mainThread
import io.reactivex.rxkotlin.addTo
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.appservices.logins.ServerPassword
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
import mozilla.lockbox.extensions.toDetailViewModel
import mozilla.lockbox.flux.Dispatcher
import mozilla.lockbox.flux.Presenter
import mozilla.lockbox.model.ItemDetailViewModel
import mozilla.lockbox.store.DataStore
import mozilla.lockbox.store.ItemDetailStore
import mozilla.lockbox.support.Constant
import mozilla.lockbox.support.pushError

interface EditItemDetailView {
var isPasswordVisible: Boolean
val togglePasswordClicks: Observable<Unit>
val deleteClicks: Observable<Unit>
val learnMoreClicks: Observable<Unit>
val closeEntryClicks: Observable<Unit>
val saveEntryClicks: Observable<Unit>
val hostnameChanged: Observable<CharSequence>
val usernameChanged: Observable<CharSequence>
val passwordChanged: Observable<CharSequence>
fun updateItem(item: ItemDetailViewModel)
fun closeKeyboard()
}

@ExperimentalCoroutinesApi
class EditItemPresenter(
private val view: EditItemDetailView,
val itemId: String?,
private val dispatcher: Dispatcher = Dispatcher.shared,
private val dataStore: DataStore = DataStore.shared,
private val itemDetailStore: ItemDetailStore = ItemDetailStore.shared
) : Presenter() {

private var credentials: ServerPassword? = null

override fun onViewReady() {
val itemId = this.itemId ?: return

dataStore.get(itemId)
.observeOn(mainThread())
.filterNotNull()
.doOnNext { credentials = it }
.map { it.toDetailViewModel() }
.subscribe(view::updateItem)
.addTo(compositeDisposable)

view.isPasswordVisible = false

itemDetailStore.isPasswordVisible
.subscribe { view.isPasswordVisible = it }
.addTo(compositeDisposable)

view.togglePasswordClicks
.subscribe {
dispatcher.dispatch(ItemDetailAction.TogglePassword(view.isPasswordVisible.not()))
}
.addTo(compositeDisposable)

view.learnMoreClicks
.subscribe {
dispatcher.dispatch(RouteAction.OpenWebsite(Constant.Faq.uri))
}
.addTo(compositeDisposable)

view.deleteClicks
.subscribe {
if (credentials != null) {
dispatcher.dispatch(DataStoreAction.Delete(credentials!!))
dispatcher.dispatch(RouteAction.ItemList)
} else {
pushError(
NullPointerException("Credentials are null"),
"Error editing credential with id ${credentials?.id}"
)
}
}
.addTo(compositeDisposable)

view.closeEntryClicks
.subscribe {
dispatcher.dispatch(DialogAction.DiscardChangesDialog(credentials!!.id))
}
.addTo(compositeDisposable)

view.hostnameChanged
.subscribe {
updateCredentials(newHostname = it)
}
.addTo(compositeDisposable)

view.usernameChanged
.subscribe {
updateCredentials(newUsername = it)
}
.addTo(compositeDisposable)

view.passwordChanged
.subscribe {
updateCredentials(newPassword = it)
}
.addTo(compositeDisposable)

view.saveEntryClicks
.subscribe {
if (credentials != null) {
dispatcher.dispatch(DataStoreAction.UpdateItemDetail(credentials!!))
view.closeKeyboard()
dispatcher.dispatch(RouteAction.ItemList)
} else {
pushError(
NullPointerException("Credentials are null"),
"Error editing credential with id ${credentials?.id}"
)
}
}
.addTo(compositeDisposable)
}

private fun updateCredentials(
newHostname: CharSequence? = null,
newUsername: CharSequence? = null,
newPassword: CharSequence? = null
) {
try {
credentials = ServerPassword(
id = credentials?.id.orEmpty(),
hostname = newHostname?.toString() ?: credentials?.hostname.orEmpty(),
username = newUsername?.toString() ?: credentials?.username,
password = newPassword?.toString() ?: credentials?.password.orEmpty(),
httpRealm = credentials?.httpRealm,
formSubmitURL = credentials?.formSubmitURL
)
} catch (exception: NullPointerException) {
pushError(exception, "Error editing credential with id ${credentials?.id}")
}
}
}
Loading

0 comments on commit c435688

Please sign in to comment.