Browse Source

Add view holder for accounts

master
Chris Smith 5 years ago
parent
commit
36b0f34eca

+ 3
- 0
app/build.gradle View File

@@ -37,6 +37,9 @@ dependencies {
37 37
 
38 38
     testImplementation 'junit:junit:4.12'
39 39
     testImplementation 'com.natpryce:hamkrest:1.5.0.0'
40
+    testImplementation "androidx.arch.core:core-testing:$lifecycle_version"
41
+    testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.0.0-RC1"
42
+    testImplementation "org.mockito:mockito-inline:2.19.0"
40 43
 
41 44
     androidTestImplementation "androidx.test:runner:$support_version"
42 45
     androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha4'

+ 12
- 5
app/src/main/java/com/chameth/yaotp/accounts/Accounts.kt View File

@@ -1,8 +1,15 @@
1 1
 package com.chameth.yaotp.accounts
2 2
 
3
-import com.chameth.yaotp.algos.HotpParams
4
-import com.chameth.yaotp.algos.TotpParams
3
+import com.chameth.yaotp.algos.*
5 4
 
6
-sealed class Account(val label: String, val issuer: String? = null)
7
-class HotpAccount(label: String, issuer: String?, val counter: Long, val params: HotpParams) : Account(label, issuer)
8
-class TotpAccount(label: String, issuer: String?, val params: TotpParams) : Account(label, issuer)
5
+sealed class Account(val label: String, val issuer: String? = null, val timeBased: Boolean) {
6
+    abstract fun generateOtp(): Otp
7
+}
8
+
9
+class HotpAccount(label: String, issuer: String?, var counter: Long, val params: HotpParams) : Account(label, issuer, false) {
10
+    override fun generateOtp() = hotp(params, counter.toByteArray()).also { counter++ }
11
+}
12
+
13
+class TotpAccount(label: String, issuer: String?, val params: TotpParams) : Account(label, issuer, true) {
14
+    override fun generateOtp() = totp(params)
15
+}

+ 29
- 0
app/src/main/java/com/chameth/yaotp/viewmodel/OtpItemViewModel.kt View File

@@ -0,0 +1,29 @@
1
+package com.chameth.yaotp.viewmodel
2
+
3
+import androidx.lifecycle.MutableLiveData
4
+import androidx.lifecycle.ViewModel
5
+import com.chameth.yaotp.accounts.Account
6
+import com.chameth.yaotp.algos.Otp
7
+import com.chameth.yaotp.algos.currentTime
8
+import kotlin.math.max
9
+
10
+class OtpItemViewModel : ViewModel() {
11
+
12
+    var account: Account? = null
13
+    set(value) {
14
+        value?.let {
15
+            label.value = it.label
16
+            otp.value = it.generateOtp()
17
+            showTime.value = it.timeBased
18
+            timeLeft.value = if (it.timeBased) "${calculateTimeLeft(otp.value?.expiresAt ?: 0)}s" else ""
19
+        }
20
+    }
21
+
22
+    val label = MutableLiveData<String>()
23
+    val otp = MutableLiveData<Otp>()
24
+    val showTime = MutableLiveData<Boolean>()
25
+    val timeLeft = MutableLiveData<String>()
26
+
27
+    internal fun calculateTimeLeft(expiry: Long, currentTime: Long = currentTime())= max(0, expiry - currentTime)
28
+
29
+}

+ 50
- 0
app/src/main/res/layout/item_otp.xml View File

@@ -0,0 +1,50 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
3
+        xmlns:tools="http://schemas.android.com/tools">
4
+
5
+    <data>
6
+        <import type="android.view.View"/>
7
+        <variable
8
+                name="viewmodel"
9
+                type="com.chameth.yaotp.viewmodel.OtpItemViewModel"/>
10
+    </data>
11
+
12
+    <RelativeLayout
13
+            android:layout_width="wrap_content"
14
+            android:layout_height="wrap_content"
15
+            android:layout_margin="16dp">
16
+
17
+        <TextView
18
+                android:id="@+id/otpLabel"
19
+                android:layout_width="wrap_content"
20
+                android:layout_height="wrap_content"
21
+                android:layout_alignParentStart="true"
22
+                android:layout_toStartOf="@id/otpTimeLeft"
23
+                android:textSize="16sp"
24
+                android:text="@{viewmodel.label}"
25
+                tools:text="Label" />
26
+
27
+        <TextView
28
+                android:id="@+id/otpValue"
29
+                android:layout_width="wrap_content"
30
+                android:layout_height="wrap_content"
31
+                android:layout_below="@id/otpLabel"
32
+                android:layout_alignParentStart="true"
33
+                android:layout_toStartOf="@id/otpTimeLeft"
34
+                android:textSize="32sp"
35
+                android:text="@{viewmodel.otp.otp}"
36
+                tools:text="012345" />
37
+
38
+        <TextView
39
+                android:id="@+id/otpTimeLeft"
40
+                android:layout_width="wrap_content"
41
+                android:layout_height="wrap_content"
42
+                android:layout_alignParentEnd="true"
43
+                android:textSize="24sp"
44
+                android:layout_margin="16dp"
45
+                android:visibility="@{viewmodel.showTime ? View.VISIBLE : View.GONE}"
46
+                android:text="@{viewmodel.timeLeft}"
47
+                tools:text="20s"/>
48
+
49
+    </RelativeLayout>
50
+</layout>

+ 86
- 0
app/src/test/java/com/chameth/yaotp/viewmodel/OtpItemViewModelTest.kt View File

@@ -0,0 +1,86 @@
1
+package com.chameth.yaotp.viewmodel
2
+
3
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4
+import androidx.lifecycle.Observer
5
+import com.chameth.yaotp.algos.Otp
6
+import com.nhaarman.mockitokotlin2.argThat
7
+import com.nhaarman.mockitokotlin2.doReturn
8
+import com.nhaarman.mockitokotlin2.mock
9
+import com.nhaarman.mockitokotlin2.verify
10
+import org.junit.Assert.assertEquals
11
+import org.junit.Rule
12
+import org.junit.Test
13
+
14
+class OtpItemViewModelTest {
15
+
16
+    @get:Rule
17
+    val instantTaskExecutorRule = InstantTaskExecutorRule()
18
+
19
+    private val viewModel = OtpItemViewModel()
20
+
21
+    @Test
22
+    fun testCalculateTimeLeft_withFutureTimes() {
23
+        assertEquals(1, viewModel.calculateTimeLeft(100L, 99L))
24
+        assertEquals(1000, viewModel.calculateTimeLeft(1000L, 0L))
25
+        assertEquals(1000, viewModel.calculateTimeLeft(99999991000L, 99999990000L))
26
+    }
27
+
28
+    @Test
29
+    fun testCalculateTimeLeft_withPastTimes() {
30
+        assertEquals(0, viewModel.calculateTimeLeft(100L, 100L))
31
+        assertEquals(0, viewModel.calculateTimeLeft(100L, 1000L))
32
+        assertEquals(0, viewModel.calculateTimeLeft(100L, 100000000000L))
33
+    }
34
+
35
+    @Test
36
+    fun testSetAccount_updatesLabel() {
37
+        val observer = mock<Observer<String>>()
38
+        viewModel.label.observeForever(observer)
39
+        viewModel.account = mock { on { label } doReturn "My label" }
40
+        verify(observer).onChanged("My label")
41
+    }
42
+
43
+    @Test
44
+    fun testSetAccount_updatesOtp() {
45
+        val otp = mock<Otp>()
46
+        val observer = mock<Observer<Otp>>()
47
+        viewModel.otp.observeForever(observer)
48
+        viewModel.account = mock { on { generateOtp() } doReturn otp }
49
+        verify(observer).onChanged(otp)
50
+    }
51
+
52
+    @Test
53
+    fun testSetAccount_updatesShowTime_withTimeBasedAccount() {
54
+        val observer = mock<Observer<Boolean>>()
55
+        viewModel.showTime.observeForever(observer)
56
+        viewModel.account = mock { on { timeBased } doReturn true }
57
+        verify(observer).onChanged(true)
58
+    }
59
+
60
+    @Test
61
+    fun testSetAccount_updatesShowTime_withNonTimeBasedAccount() {
62
+        val observer = mock<Observer<Boolean>>()
63
+        viewModel.showTime.observeForever(observer)
64
+        viewModel.account = mock { on { timeBased } doReturn false }
65
+        verify(observer).onChanged(false)
66
+    }
67
+
68
+    @Test
69
+    fun testSetAccount_updatesTimeLeft_withTimeBasedAccount() {
70
+        val otp = mock<Otp> { on { expiresAt } doReturn (System.currentTimeMillis() / 1000) + 10 }
71
+        val observer = mock<Observer<String>>()
72
+        viewModel.timeLeft.observeForever(observer)
73
+        viewModel.account = mock { on { generateOtp() } doReturn otp; on { timeBased } doReturn true }
74
+        verify(observer).onChanged(argThat { matches(Regex("(9|10)s")) })
75
+    }
76
+
77
+    @Test
78
+    fun testSetAccount_updatesTimeLeft_withNonTimeBasedAccount() {
79
+        val otp = mock<Otp>()
80
+        val observer = mock<Observer<String>>()
81
+        viewModel.timeLeft.observeForever(observer)
82
+        viewModel.account = mock { on { generateOtp() } doReturn otp; on { timeBased } doReturn false }
83
+        verify(observer).onChanged("")
84
+    }
85
+
86
+}

Loading…
Cancel
Save