[#1096] Change brightness adjusting feature

* [#1096] Change brightness adjusting feature

- Closes #1096
- As reported by testers, the automatic brightness adjustment could be too invasive
- Switched to the on-demand feature after a new button click
- Tests aligned

* Align Screenshot tests

* Changelog update
This commit is contained in:
Honza Rychnovský 2023-12-07 16:35:27 +01:00 committed by GitHub
parent 201cdbbd07
commit 06ca665fca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 174 additions and 118 deletions

View File

@ -12,6 +12,10 @@ directly impact users rather than highlighting other key architectural updates.*
### Added ### Added
- Unfinished features show a "Not implemented yet" message after accessing in the app UI - Unfinished features show a "Not implemented yet" message after accessing in the app UI
### Changed
- Home and Receive screens have their Top app bar UI changed
- Automatic brightness adjustment switched to an on-demand feature after a new button is clicked on the Receive screen
### Removed ### Removed
- Home screen side menu navigation was removed in favor of the Settings screen - Home screen side menu navigation was removed in favor of the Settings screen

View File

@ -1,55 +1,40 @@
package co.electriccoin.zcash.ui.screen.receive.view package co.electriccoin.zcash.ui.screen.receive.view
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.WalletAddress import cash.z.ecc.android.sdk.model.WalletAddress
import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.LocalScreenBrightness
import co.electriccoin.zcash.ui.common.ScreenBrightness
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class)
class ReceiveViewScreenBrightnessTest : UiTestPrerequisites() { class ReceiveViewScreenBrightnessTest : UiTestPrerequisites() {
@get:Rule @get:Rule
val composeTestRule = createComposeRule() val composeTestRule = createComposeRule()
@Test @Test
@MediumTest @MediumTest
fun testFullBrightness() = runTest { fun testBrightnessDefaultState() = runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified()) val testSetup = newTestSetup(WalletAddressFixture.unified())
assertEquals(1, testSetup.getSecureBrightnessCount()) assertEquals(0, testSetup.getScreenBrightnessCount())
} }
private fun newTestSetup(walletAddress: WalletAddress) = TestSetup(composeTestRule, walletAddress) @Test
@MediumTest
fun testBrightnessOnState() = runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified())
private class TestSetup(private val composeTestRule: ComposeContentTestRule, walletAddress: WalletAddress) { assertEquals(false, testSetup.getOnAdjustBrightness())
private val screenBrightness = ScreenBrightness() assertEquals(0, testSetup.getScreenBrightnessCount())
fun getSecureBrightnessCount() = screenBrightness.referenceCount.value composeTestRule.clickAdjustBrightness()
init { assertEquals(true, testSetup.getOnAdjustBrightness())
composeTestRule.setContent { assertEquals(1, testSetup.getScreenBrightnessCount())
CompositionLocalProvider(LocalScreenBrightness provides screenBrightness) {
ZcashTheme {
ZcashTheme {
Receive(
walletAddress,
onBack = { },
onAddressDetails = { },
)
}
}
}
}
}
} }
private fun newTestSetup(walletAddress: WalletAddress) = ReceiveViewTestSetup(composeTestRule, walletAddress)
} }

View File

@ -1,56 +1,40 @@
package co.electriccoin.zcash.ui.screen.receive.view package co.electriccoin.zcash.ui.screen.receive.view
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.WalletAddress import cash.z.ecc.android.sdk.model.WalletAddress
import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.LocalScreenTimeout
import co.electriccoin.zcash.ui.common.ScreenTimeout
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class)
class ReceiveViewScreenTimeoutTest : UiTestPrerequisites() { class ReceiveViewScreenTimeoutTest : UiTestPrerequisites() {
@get:Rule @get:Rule
val composeTestRule = createComposeRule() val composeTestRule = createComposeRule()
@Test @Test
@MediumTest @MediumTest
fun testFullBrightness() = runTest { fun testTimeoutDefaultState() = runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified()) val testSetup = newTestSetup(WalletAddressFixture.unified())
assertEquals(0, testSetup.getScreenTimeoutCount())
}
@Test
@MediumTest
fun testTimeoutOnState() = runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified())
assertEquals(false, testSetup.getOnAdjustBrightness())
assertEquals(0, testSetup.getScreenTimeoutCount())
composeTestRule.clickAdjustBrightness()
assertEquals(true, testSetup.getOnAdjustBrightness())
assertEquals(1, testSetup.getScreenTimeoutCount()) assertEquals(1, testSetup.getScreenTimeoutCount())
} }
private fun newTestSetup(walletAddress: WalletAddress) = TestSetup(composeTestRule, walletAddress) private fun newTestSetup(walletAddress: WalletAddress) = ReceiveViewTestSetup(composeTestRule, walletAddress)
private class TestSetup(private val composeTestRule: ComposeContentTestRule, walletAddress: WalletAddress) {
private val screenTimeout = ScreenTimeout()
fun getScreenTimeoutCount() = screenTimeout.referenceCount.value
init {
composeTestRule.setContent {
CompositionLocalProvider(LocalScreenTimeout provides screenTimeout) {
ZcashTheme {
ZcashTheme {
Receive(
walletAddress,
onBack = { },
onAddressDetails = { },
)
}
}
}
}
}
}
} }

View File

@ -1,6 +1,5 @@
package co.electriccoin.zcash.ui.screen.receive.view package co.electriccoin.zcash.ui.screen.receive.view
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
@ -9,13 +8,11 @@ import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.WalletAddress import cash.z.ecc.android.sdk.model.WalletAddress
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.test.getStringResource import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
/* /*
* Note: It is difficult to test the QR code from automated tests. There is a manual test case * Note: It is difficult to test the QR code from automated tests. There is a manual test case
@ -71,37 +68,5 @@ class ReceiveViewTest {
assertEquals(1, testSetup.getOnAddressDetailsCount()) assertEquals(1, testSetup.getOnAddressDetailsCount())
} }
private fun newTestSetup(walletAddress: WalletAddress) = TestSetup(composeTestRule, walletAddress) private fun newTestSetup(walletAddress: WalletAddress) = ReceiveViewTestSetup(composeTestRule, walletAddress)
private class TestSetup(private val composeTestRule: ComposeContentTestRule, walletAddress: WalletAddress) {
private val onBackCount = AtomicInteger(0)
private val onAddressDetailsCount = AtomicInteger(0)
fun getOnBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
}
fun getOnAddressDetailsCount(): Int {
composeTestRule.waitForIdle()
return onAddressDetailsCount.get()
}
init {
composeTestRule.setContent {
ZcashTheme {
Receive(
walletAddress,
onBack = {
onBackCount.getAndIncrement()
},
onAddressDetails = {
onAddressDetailsCount.getAndIncrement()
}
)
}
}
}
}
} }

View File

@ -0,0 +1,78 @@
package co.electriccoin.zcash.ui.screen.receive.view
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.performClick
import cash.z.ecc.android.sdk.model.WalletAddress
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.LocalScreenBrightness
import co.electriccoin.zcash.ui.common.LocalScreenTimeout
import co.electriccoin.zcash.ui.common.ScreenBrightness
import co.electriccoin.zcash.ui.common.ScreenTimeout
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.test.getStringResource
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
class ReceiveViewTestSetup(
private val composeTestRule: ComposeContentTestRule,
walletAddress: WalletAddress
) {
private val onBackCount = AtomicInteger(0)
private val onAddressDetailsCount = AtomicInteger(0)
private val screenBrightness = ScreenBrightness()
private val screenTimeout = ScreenTimeout()
private val onAdjustBrightness = AtomicBoolean(false)
fun getScreenBrightnessCount() = screenBrightness.referenceCount.value
fun getScreenTimeoutCount() = screenTimeout.referenceCount.value
fun getOnAdjustBrightness(): Boolean {
composeTestRule.waitForIdle()
return onAdjustBrightness.get()
}
fun getOnBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
}
fun getOnAddressDetailsCount(): Int {
composeTestRule.waitForIdle()
return onAddressDetailsCount.get()
}
init {
composeTestRule.setContent {
CompositionLocalProvider(
LocalScreenBrightness provides screenBrightness,
LocalScreenTimeout provides screenTimeout
) {
ZcashTheme {
ZcashTheme {
Receive(
walletAddress,
onBack = {
onBackCount.getAndIncrement()
},
onAddressDetails = {
onAddressDetailsCount.getAndIncrement()
},
onAdjustBrightness = {
onAdjustBrightness.getAndSet(it)
},
)
}
}
}
}
}
}
fun ComposeContentTestRule.clickAdjustBrightness() {
onNodeWithContentDescription(getStringResource(R.string.receive_brightness_content_description)).also {
it.performClick()
}
}

View File

@ -17,7 +17,7 @@ class ScreenBrightness {
mutableReferenceCount.update { it + 1 } mutableReferenceCount.update { it + 1 }
} }
fun restore() { fun restoreBrightness() {
val after = mutableReferenceCount.updateAndGet { it - 1 } val after = mutableReferenceCount.updateAndGet { it - 1 }
if (after < 0) { if (after < 0) {
@ -34,6 +34,6 @@ fun BrightenScreen() {
val screenBrightness = LocalScreenBrightness.current val screenBrightness = LocalScreenBrightness.current
DisposableEffect(screenBrightness) { DisposableEffect(screenBrightness) {
screenBrightness.fullBrightness() screenBrightness.fullBrightness()
onDispose { screenBrightness.restore() } onDispose { screenBrightness.restoreBrightness() }
} }
} }

View File

@ -55,6 +55,7 @@ internal fun WrapReceive(
walletAddresses.unified, walletAddresses.unified,
onBack = onBack, onBack = onBack,
onAddressDetails = onAddressDetails, onAddressDetails = onAddressDetails,
onAdjustBrightness = { /* Just for testing */ }
) )
} }
} }

View File

@ -10,14 +10,15 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.BrightnessHigh
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material.icons.filled.BrightnessLow
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
@ -35,6 +36,7 @@ import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.Body import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.receive.util.AndroidQrCodeImageGenerator import co.electriccoin.zcash.ui.screen.receive.util.AndroidQrCodeImageGenerator
import co.electriccoin.zcash.ui.screen.receive.util.JvmQrCodeGenerator import co.electriccoin.zcash.ui.screen.receive.util.JvmQrCodeGenerator
@ -50,24 +52,35 @@ private fun ComposablePreview() {
walletAddress = runBlocking { WalletAddressFixture.unified() }, walletAddress = runBlocking { WalletAddressFixture.unified() },
onBack = {}, onBack = {},
onAddressDetails = {}, onAddressDetails = {},
onAdjustBrightness = {},
) )
} }
} }
} }
@Composable @Composable
@Suppress("LongParameterList")
fun Receive( fun Receive(
walletAddress: WalletAddress, walletAddress: WalletAddress,
onBack: () -> Unit, onBack: () -> Unit,
onAddressDetails: () -> Unit, onAddressDetails: () -> Unit,
onAdjustBrightness: (Boolean) -> Unit,
) { ) {
val (brightness, setBrightness) = rememberSaveable { mutableStateOf(false) }
// Rework this into Scaffold // Rework this into Scaffold
Column { Column {
ReceiveTopAppBar(onBack = onBack) ReceiveTopAppBar(
adjustBrightness = brightness,
onBack = onBack,
onBrightness = {
onAdjustBrightness(!brightness)
setBrightness(!brightness)
}
)
ReceiveContents( ReceiveContents(
walletAddress = walletAddress, walletAddress = walletAddress,
onAddressDetails = onAddressDetails, onAddressDetails = onAddressDetails,
adjustBrightness = brightness,
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
@ -81,17 +94,27 @@ fun Receive(
} }
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) private fun ReceiveTopAppBar(
private fun ReceiveTopAppBar(onBack: () -> Unit) { adjustBrightness: Boolean,
TopAppBar( onBack: () -> Unit,
title = { Text(text = stringResource(id = R.string.receive_title)) }, onBrightness: () -> Unit
navigationIcon = { ) {
SmallTopAppBar(
titleText = stringResource(id = R.string.receive_title),
backText = stringResource(id = R.string.receive_back),
backContentDescriptionText = stringResource(id = R.string.receive_back_content_description),
onBack = onBack,
regularActions = {
IconButton( IconButton(
onClick = onBack onClick = onBrightness
) { ) {
Icon( Icon(
imageVector = Icons.Filled.ArrowBack, imageVector = if (adjustBrightness) {
contentDescription = stringResource(R.string.receive_back_content_description) Icons.Default.BrightnessLow
} else {
Icons.Default.BrightnessHigh
},
contentDescription = stringResource(R.string.receive_brightness_content_description)
) )
} }
} }
@ -101,17 +124,22 @@ private fun ReceiveTopAppBar(onBack: () -> Unit) {
private val DEFAULT_QR_CODE_SIZE = 320.dp private val DEFAULT_QR_CODE_SIZE = 320.dp
@Composable @Composable
@Suppress("LongParameterList")
private fun ReceiveContents( private fun ReceiveContents(
walletAddress: WalletAddress, walletAddress: WalletAddress,
onAddressDetails: () -> Unit, onAddressDetails: () -> Unit,
modifier: Modifier = Modifier adjustBrightness: Boolean,
modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
QrCode(data = walletAddress.address, DEFAULT_QR_CODE_SIZE, Modifier.align(Alignment.CenterHorizontally)) QrCode(
data = walletAddress.address,
size = DEFAULT_QR_CODE_SIZE,
adjustBrightness = adjustBrightness,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
@ -151,10 +179,18 @@ private fun ReceiveContents(
} }
@Composable @Composable
private fun QrCode(data: String, size: Dp, modifier: Modifier = Modifier) { private fun QrCode(
data: String,
size: Dp,
modifier: Modifier = Modifier,
adjustBrightness: Boolean = false,
) {
Column(modifier = modifier) { Column(modifier = modifier) {
BrightenScreen() if (adjustBrightness) {
DisableScreenTimeout() BrightenScreen()
DisableScreenTimeout()
}
val sizePixels = with(LocalDensity.current) { size.toPx() }.roundToInt() val sizePixels = with(LocalDensity.current) { size.toPx() }.roundToInt()
// In the future, use actual/expect to switch QR code generator implementations for multiplatform // In the future, use actual/expect to switch QR code generator implementations for multiplatform

View File

@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="receive_title">Receive</string> <string name="receive_title">Receive</string>
<string name="receive_back">Back</string>
<string name="receive_back_content_description">Back</string> <string name="receive_back_content_description">Back</string>
<string name="receive_brightness_content_description">Adjust brightness</string>
<string name="receive_qr_code_content_description">QR code for address</string> <string name="receive_qr_code_content_description">QR code for address</string>
<string name="receive_caption">Your Address</string> <string name="receive_caption">Your Address</string>
<string name="receive_see_address_details">See Address Details</string> <string name="receive_see_address_details">See Address Details</string>

View File

@ -155,6 +155,7 @@ class ScreenshotTest : UiTestPrerequisites() {
), ),
ignoreCase = true ignoreCase = true
).also { ).also {
it.performScrollTo()
it.assertExists() it.assertExists()
it.performClick() it.performClick()
} }
@ -408,7 +409,7 @@ private fun receiveZecScreenshots(
composeTestRule.activity.walletViewModel.addresses.value != null composeTestRule.activity.walletViewModel.addresses.value != null
} }
composeTestRule.onNode(hasText(resContext.getString(R.string.receive_title))).also { composeTestRule.onNode(hasText(resContext.getString(R.string.receive_title), ignoreCase = true)).also {
it.assertExists() it.assertExists()
} }