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

Copy and reveal credentials #124

Merged
merged 12 commits into from
Oct 8, 2018
6 changes: 6 additions & 0 deletions app/src/main/java/mozilla/lockbox/LockboxApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@
package mozilla.lockbox

import android.app.Application
import android.content.ClipboardManager
import android.content.Context
import io.sentry.Sentry
import io.sentry.android.AndroidSentryClientFactory
import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.base.log.sink.AndroidLogSink
import mozilla.lockbox.store.ClipboardStore

val log: Logger = Logger("Lockbox")
class LockboxApplication : Application() {
override fun onCreate() {
super.onCreate()
Log.addSink(AndroidLogSink())

// use context for system service
ClipboardStore.shared.apply(getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)

// Set up Sentry using DSN (client key) from the Project Settings page on Sentry
val ctx = this.applicationContext
// Retrieved from environment's local (or bitrise's "Secrets") environment variable
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/mozilla/lockbox/action/ClipboardAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package mozilla.lockbox.action

import mozilla.lockbox.flux.Action

sealed class ClipboardAction : Action {
data class Clip(val label: String, val str: String) : ClipboardAction()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider CopyUsername, CopyPassword.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

package mozilla.lockbox.presenter

import android.support.annotation.StringRes
import io.reactivex.Observable
import io.reactivex.rxkotlin.addTo
import mozilla.lockbox.R
import mozilla.lockbox.action.ClipboardAction
import mozilla.lockbox.flux.Dispatcher
import mozilla.lockbox.flux.Presenter
import mozilla.lockbox.model.ItemDetailViewModel
Expand All @@ -16,13 +20,56 @@ import mozilla.lockbox.store.DataStore
interface ItemDetailView {
var itemId: String?
fun updateItem(item: ItemDetailViewModel)
fun showToastNotification(@StringRes strId: Int)

val usernameCopyClicks: Observable<Unit>
val passwordCopyClicks: Observable<Unit>
val togglePasswordClicks: Observable<Unit>

var isPasswordVisible: Boolean
}

class ItemDetailPresenter(
private val view: ItemDetailView,
private val dispatcher: Dispatcher = Dispatcher.shared,
private val dataStore: DataStore = DataStore.shared

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: whitespace.

) : Presenter() {

override fun onViewReady() {
this.view.usernameCopyClicks
.subscribe {
view.itemId?.let {
dataStore.get(it)
.subscribe {
dispatcher.dispatch(ClipboardAction.Clip("username", it!!.username!!))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when there is no username?

view.showToastNotification(R.string.toast_username_copied)
}
.addTo(compositeDisposable)
}
}
.addTo(compositeDisposable)

this.view.passwordCopyClicks
.subscribe {
view.itemId?.let {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract this statement into a method, parameterized for username and password.

At some point, it would be good to not hit the database each time, and stash the itemViewModel, but I don't think we need to worry about that now.

dataStore.get(it)
.subscribe {
dispatcher.dispatch(ClipboardAction.Clip("password", it!!.password!!))
view.showToastNotification(R.string.toast_password_copied)
}
.addTo(compositeDisposable)
}
}
.addTo(compositeDisposable)

this.view.togglePasswordClicks
.subscribe {
view.isPasswordVisible = view.isPasswordVisible.not()
}
.addTo(compositeDisposable)
}

override fun onResume() {
super.onResume()
val itemId = view?.itemId ?: return
Expand All @@ -32,5 +79,7 @@ class ItemDetailPresenter(
}
.subscribe(view::updateItem)
.addTo(compositeDisposable)

view.isPasswordVisible = false
}
}
44 changes: 44 additions & 0 deletions app/src/main/java/mozilla/lockbox/store/ClipboardStore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package mozilla.lockbox.store

import android.content.ClipData
import android.content.ClipboardManager
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.addTo
import mozilla.lockbox.action.ClipboardAction
import mozilla.lockbox.extensions.filterByType
import mozilla.lockbox.flux.Dispatcher

open class ClipboardStore(
val dispatcher: Dispatcher = Dispatcher.shared
) {
internal val compositeDisposable = CompositeDisposable()
companion object {
val shared = ClipboardStore()
}

private lateinit var clipboardManager: ClipboardManager

init {
dispatcher.register
.filterByType(ClipboardAction::class.java)
.subscribe {
// unpack the action, including adding new Clips to the Clipboard.
when (it) {
is ClipboardAction.Clip -> {
addToClipboard(it.label, it.str)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good. Consider making a CopyUsername / CopyPassword objects, but this shouldn't stop you landing this at this point.

}
}
.addTo(compositeDisposable)
}
mihainisipeanusv marked this conversation as resolved.
Show resolved Hide resolved

fun apply(manager: ClipboardManager) {
clipboardManager = manager
}

fun addToClipboard(label: String, str: String) {

mihainisipeanusv marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace.

val clip = ClipData.newPlainText(label, str)
clipboardManager.primaryClip = clip
}
}
32 changes: 31 additions & 1 deletion app/src/main/java/mozilla/lockbox/view/ItemDetailFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@
package mozilla.lockbox.view

import android.os.Bundle
import android.support.annotation.StringRes
import android.text.method.PasswordTransformationMethod
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import com.jakewharton.rxbinding2.view.clicks
import io.reactivex.Observable
import kotlinx.android.synthetic.main.fragment_item_detail.*
import kotlinx.android.synthetic.main.fragment_item_detail.view.*
import kotlinx.android.synthetic.main.include_backable.*
import kotlinx.android.synthetic.main.include_backable.view.*
import mozilla.lockbox.R
import mozilla.lockbox.model.ItemDetailViewModel
import mozilla.lockbox.presenter.ItemDetailPresenter
Expand All @@ -35,6 +40,27 @@ class ItemDetailFragment : BackableFragment(), ItemDetailView {
return view
}

override val usernameCopyClicks: Observable<Unit>
get() = view!!.btnUsernameCopy.clicks()

override val passwordCopyClicks: Observable<Unit>
get() = view!!.btnPasswordCopy.clicks()

override val togglePasswordClicks: Observable<Unit>
get() = view!!.btnPasswordToggle.clicks()

override var isPasswordVisible: Boolean = false
set(value) {
field = value
if (value) {
inputPassword.transformationMethod = null
btnPasswordToggle.setImageResource(R.drawable.ic_icon_hide)
} else {
inputPassword.transformationMethod = PasswordTransformationMethod.getInstance()
btnPasswordToggle.setImageResource(R.drawable.ic_icon_show)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.


override fun updateItem(item: ItemDetailViewModel) {
toolbar.title = item.title

Expand All @@ -50,6 +76,10 @@ class ItemDetailFragment : BackableFragment(), ItemDetailView {
inputUsername.setText(item.username, TextView.BufferType.NORMAL)
inputPassword.setText(item.password, TextView.BufferType.NORMAL)
}

override fun showToastNotification(@StringRes strId: Int) {
Toast.makeText(activity, getString(strId), Toast.LENGTH_SHORT).show()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

var EditText.readOnly: Boolean
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/res/drawable/ic_icon_copy.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">

<path
android:pathData="M16.545375,9.329625 L13.170375,5.954625 C12.9594457,5.74363195 12.6733442,5.62506372 12.375,5.625 L11.25,5.625 L11.25,4.5 C11.2499363,4.20165583 11.131368,3.91555432 10.920375,3.704625 L7.545375,0.329625 C7.33444568,0.118631953 7.04834417,6.37170812e-05 6.75,0 L3.375,0 C2.13235931,-7.6089797e-17 1.125,1.00735931 1.125,2.25 L1.125,10.125 C1.125,11.3676407 2.13235931,12.375 3.375,12.375 L6.75,12.375 L6.75,15.75 C6.75,16.9926407 7.75735931,18 9,18 L14.625,18 C15.8676407,18 16.875,16.9926407 16.875,15.75 L16.875,10.125 C16.8749363,9.82665583 16.756368,9.54055432 16.545375,9.329625 Z M14.15925,10.125 L12.375,10.125 L12.375,8.34075 L14.15925,10.125 Z M8.53425,4.5 L6.75,4.5 L6.75,2.71575 L8.53425,4.5 Z M6.75,7.875 L6.75,10.125 L3.375,10.125 L3.375,2.25 L5.625,2.25 L5.625,5.0625 C5.625,5.37316017 5.87683983,5.625 6.1875,5.625 L9,5.625 C7.75735931,5.625 6.75,6.63235931 6.75,7.875 Z M9,15.75 L9,7.875 L11.25,7.875 L11.25,10.6875 C11.25,10.9981602 11.5018398,11.25 11.8125,11.25 L14.625,11.25 L14.625,15.75 L9,15.75 Z"
android:strokeWidth="1"
android:fillColor="#4A4A4F"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>
12 changes: 12 additions & 0 deletions app/src/main/res/drawable/ic_icon_hide.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M13.5001378,7.875 L9.00013784,12.375 C11.4854192,12.375 13.5001378,10.3602814 13.5001378,7.875 Z M17.9495128,8.6625 C18.018604,8.88219601 18.018604,9.11780399 17.9495128,9.3375 C16.6702783,13.1878327 13.0573283,15.7766216 9.00013784,15.75 C8.01052596,15.7487738 7.02713331,15.5935412 6.08526284,15.289875 L7.95388784,13.42125 C8.30030917,13.4724565 8.64995453,13.4987739 9.00013784,13.5 C11.9447269,13.5263468 14.6019241,11.7378916 15.6860128,9 C15.3985795,8.24211122 14.9720031,7.54463215 14.4282628,6.9435 L16.0145128,5.35725 C16.8945093,6.30345955 17.5551757,7.43196981 17.9495128,8.6625 Z M16.5455128,1.454625 C16.9846925,1.89393733 16.9846925,2.60606267 16.5455128,3.045375 L3.04551284,16.545375 C2.76305406,16.8378262 2.34477521,16.9551142 1.9514421,16.8521593 C1.55810899,16.7492044 1.25093341,16.4420289 1.14797853,16.0486957 C1.04502366,15.6553626 1.16231168,15.2370838 1.45476284,14.954625 L2.89926284,13.50675 C1.56431174,12.4209648 0.574503528,10.9698716 0.0507628361,9.33075 C-0.0169209454,9.11314152 -0.0169209454,8.88010848 0.0507628361,8.6625 C0.915942709,6.08251245 2.85677943,4.00409597 5.37156688,2.96452103 C7.88635433,1.92494608 10.728255,2.02625156 13.1626378,3.24225 L14.9547628,1.454625 C15.3940752,1.01544532 16.1062005,1.01544532 16.5455128,1.454625 Z M9.56263784,5.625 C8.63065732,5.625 7.87513784,6.38051948 7.87513784,7.3125 C7.87809346,7.64390747 7.98123585,7.96666865 8.17101284,8.238375 L10.4885128,5.920875 C10.2168065,5.73109801 9.89404531,5.62795562 9.56263784,5.625 Z M2.31426284,9 C2.75531187,10.1563686 3.50944026,11.1671966 4.49226284,11.919375 L5.60151284,10.810125 C4.33541627,9.36765229 4.14366735,7.27340871 5.12676284,5.625 C3.84259111,6.40890234 2.85374236,7.59552083 2.31426284,9 Z"
android:strokeWidth="1"
android:fillColor="#4A4A4F"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>
12 changes: 12 additions & 0 deletions app/src/main/res/drawable/ic_icon_show.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M17.9495128,6.6625 C16.6702783,2.81216726 13.0573283,0.223378405 9.00013784,0.25 C4.94294738,0.223378405 1.32999732,2.81216726 0.0507628361,6.6625 C-0.0169209454,6.88010848 -0.0169209454,7.11314152 0.0507628361,7.33075 C1.32764302,11.1837517 4.94114947,13.7756647 9.00013784,13.75 C13.0573283,13.7766216 16.6702783,11.1878327 17.9495128,7.3375 C18.018604,7.11780399 18.018604,6.88219601 17.9495128,6.6625 Z M9.56263784,3.625 C10.4946184,3.625 11.2501378,4.38051948 11.2501378,5.3125 C11.2501378,6.24448052 10.4946184,7 9.56263784,7 C8.63065732,7 7.87513784,6.24448052 7.87513784,5.3125 C7.87513784,4.38051948 8.63065732,3.625 9.56263784,3.625 Z M9.00013784,11.5 C6.05554876,11.5263468 3.39835155,9.73789164 2.31426284,7 C2.85374236,5.59552083 3.84259111,4.40890234 5.12676284,3.625 C4.72125214,4.30581733 4.50492353,5.08258075 4.50013784,5.875 C4.50013787,8.36028135 6.51485649,10.3749999 9.00013784,10.3749999 C11.4854192,10.3749999 13.5001378,8.36028135 13.5001378,5.875 C13.4960332,5.08278883 13.2804853,4.30603972 12.8757628,3.625 C14.1599346,4.40890234 15.1487833,5.59552083 15.6882628,7 C14.603878,9.73866412 11.9455457,11.5272813 9.00013784,11.5 Z"
android:strokeWidth="1"
android:fillColor="#4A4A4F"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>
41 changes: 37 additions & 4 deletions app/src/main/res/layout/fragment_item_detail.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

Expand All @@ -19,6 +18,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="?attr/actionBarSize"
app:layout_constraintTop_toTopOf="parent">

<EditText
android:id="@+id/inputHostname"
android:layout_width="match_parent"
Expand All @@ -32,22 +32,35 @@
android:id="@+id/inputLayoutUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/inputLayoutHostname"
>
app:layout_constraintTop_toBottomOf="@id/inputLayoutHostname">

<EditText
android:id="@+id/inputUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_username"
android:inputType="none"
android:singleLine="true" />
android:singleLine="true"
app:layout_constraintRight_toRightOf="@+id/inputLayoutUsername"
app:layout_constraintTop_toTopOf="@+id/inputLayoutUsername" />
</android.support.design.widget.TextInputLayout>

<ImageView
android:id="@+id/btnUsernameCopy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="@+id/inputLayoutUsername"
app:layout_constraintRight_toRightOf="@+id/inputLayoutUsername"
app:layout_constraintTop_toTopOf="@+id/inputLayoutUsername"
app:srcCompat="@drawable/ic_icon_copy" />

<android.support.design.widget.TextInputLayout
android:id="@+id/inputLayoutPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/inputLayoutUsername">

<EditText
android:id="@+id/inputPassword"
android:layout_width="match_parent"
Expand All @@ -56,4 +69,24 @@
android:inputType="none|textPassword"
android:singleLine="true" />
</android.support.design.widget.TextInputLayout>

<ImageView
android:id="@+id/btnPasswordCopy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="@+id/inputLayoutPassword"
app:layout_constraintRight_toRightOf="@+id/inputLayoutPassword"
app:layout_constraintTop_toTopOf="@+id/inputLayoutPassword"
app:srcCompat="@drawable/ic_icon_copy" />

<ImageView
android:id="@+id/btnPasswordToggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="@+id/inputLayoutPassword"
app:layout_constraintRight_toLeftOf="@+id/btnPasswordCopy"
app:layout_constraintTop_toTopOf="@+id/inputLayoutPassword"
app:srcCompat="@drawable/ic_icon_show" />
</android.support.constraint.ConstraintLayout>
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@
<string name="hint_hostname">Web address</string>
<string name="hint_username">Username</string>
<string name="hint_password">Password</string>
<string name="toast_username_copied">Username copied</string>
<string name="toast_password_copied">Password copied</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

package mozilla.lockbox.presenter

import android.support.annotation.StringRes
import io.reactivex.Observable
import io.reactivex.observers.TestObserver
import io.reactivex.subjects.PublishSubject
Expand All @@ -21,12 +22,29 @@ import org.mozilla.sync15.logins.ServerPassword

class ItemDetailPresenterTest {
class FakeView : ItemDetailView {

override var itemId: String? = null
var item: ItemDetailViewModel? = null
val tapStub: PublishSubject<Unit> = PublishSubject.create<Unit>()

override val usernameCopyClicks: Observable<Unit>
get() = tapStub

override val passwordCopyClicks: Observable<Unit>
get() = tapStub

override val togglePasswordClicks: Observable<Unit>
get() = tapStub

override fun updateItem(item: ItemDetailViewModel) {
this.item = item
}

override fun showToastNotification(@StringRes strId: Int) {
// notification Test
}

override var isPasswordVisible: Boolean = false
}

class FakeDataStore : DataStore() {
Expand All @@ -38,6 +56,7 @@ class ItemDetailPresenterTest {

val view = FakeView()
val dataStore = FakeDataStore()

val subject = ItemDetailPresenter(view, dataStore = dataStore)

val dispatcherObserver = TestObserver.create<Action>()
Expand Down
Loading