[#1145] Receive screen

- Closes #1145
- Closes #1057
- Closes #1088
- Closes #1154
- Closes #1155
- Closes #1185
This commit is contained in:
Honza Rychnovský 2024-01-15 17:32:15 +01:00 committed by GitHub
parent a276cb41e0
commit 6519df7539
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 584 additions and 827 deletions

View File

@ -29,7 +29,7 @@
tools:targetApi="29" />
<provider
android:name="co.electriccoin.zcash.ui.screen.exportdata.util.ShareFileProvider"
android:name="co.electriccoin.zcash.global.ShareFileProvider"
android:authorities="co.electriccoin.zcash.provider"
android:exported="false"
android:grantUriPermissions="true">

View File

@ -10,7 +10,7 @@
android:label="@string/app_name">
<provider
android:name="co.electriccoin.zcash.ui.screen.exportdata.util.ShareFileProvider"
android:name="co.electriccoin.zcash.global.ShareFileProvider"
android:authorities="co.electriccoin.zcash.debug.provider"
android:exported="false"
android:grantUriPermissions="true"

View File

@ -10,7 +10,7 @@
android:label="@string/app_name">
<provider
android:name="co.electriccoin.zcash.ui.screen.exportdata.util.ShareFileProvider"
android:name="co.electriccoin.zcash.global.ShareFileProvider"
android:authorities="co.electriccoin.zcash.provider.testnet"
android:exported="false"
android:grantUriPermissions="true"

View File

@ -10,7 +10,7 @@
android:label="@string/app_name">
<provider
android:name="co.electriccoin.zcash.ui.screen.exportdata.util.ShareFileProvider"
android:name="co.electriccoin.zcash.global.ShareFileProvider"
android:authorities="co.electriccoin.zcash.debug.provider.testnet"
android:exported="false"
android:grantUriPermissions="true"

View File

@ -188,7 +188,7 @@ ZCASH_BIP39_VERSION=1.0.7
ZXING_VERSION=3.5.2
# WARNING: Ensure a non-snapshot version is used before releasing to production.
ZCASH_SDK_VERSION=2.0.4-SNAPSHOT
ZCASH_SDK_VERSION=2.0.4
# Toolchain is the Java version used to build the application, which is separate from the
# Java version used to run the application.

View File

@ -3,10 +3,20 @@
package co.electriccoin.zcash.spackle
import android.content.Context
import co.electriccoin.zcash.spackle.io.mkdirsSuspend
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
// TODO [#1182]: Cover ContextExt with tests
// TODO [#1182]: https://github.com/Electric-Coin-Company/zashi-android/issues/1182
suspend fun Context.getExternalFilesDirSuspend(type: String?) =
withContext(Dispatchers.IO) {
getExternalFilesDir(type)
}
suspend fun Context.getInternalCacheDirSuspend(subDirectory: String?): File =
withContext(Dispatchers.IO) {
(subDirectory?.let { File(cacheDir, subDirectory) } ?: cacheDir).apply { mkdirsSuspend() }
}

View File

@ -43,7 +43,7 @@ suspend fun File.renameToSuspend(destination: File) =
renameTo(destination)
}
suspend fun File.listFilesSuspend() =
suspend fun File.listFilesSuspend(): Array<out File>? =
withContext(Dispatchers.IO) {
listFiles()
}

View File

@ -5,25 +5,47 @@ package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AccountBox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@Preview
@Composable
private fun TextComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
Column {
Reference(text = "Test reference text", onClick = {})
Reference(text = "User account", imageVector = Icons.Outlined.AccountBox, onClick = {})
// Preview the rest of the composable
}
}
}
}
@Composable
fun Header(
text: String,
@ -36,7 +58,23 @@ fun Header(
color = color,
textAlign = textAlign,
modifier = modifier,
style = MaterialTheme.typography.headlineLarge,
style = ZcashTheme.typography.secondary.headlineLarge,
)
}
@Composable
fun SubHeader(
text: String,
modifier: Modifier = Modifier,
textAlign: TextAlign = TextAlign.Start,
color: Color = ZcashTheme.colors.onBackgroundHeader,
) {
Text(
text = text,
color = color,
textAlign = textAlign,
modifier = modifier,
style = ZcashTheme.typography.secondary.headlineSmall,
)
}
@ -171,33 +209,46 @@ fun ListHeader(
)
}
@Suppress("LongParameterList")
@Composable
fun Reference(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
textAlign: TextAlign = TextAlign.Start,
onClick: () -> Unit
textAlign: TextAlign = TextAlign.Center,
imageVector: ImageVector? = null,
imageContentDescription: String? = null
) {
Box(
Row(
modifier =
Modifier
.wrapContentSize()
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
.clickable { onClick() }
.then(modifier)
.padding(all = ZcashTheme.dimens.spacingDefault)
.then(modifier),
verticalAlignment = Alignment.CenterVertically
) {
imageVector?.let {
Icon(
imageVector = imageVector,
contentDescription = imageContentDescription
)
}
Spacer(modifier = Modifier.padding(ZcashTheme.dimens.spacingTiny))
Text(
text = text,
textAlign = TextAlign.Center,
style =
MaterialTheme.typography.bodyLarge
ZcashTheme.typography.primary.bodyLarge
.merge(
TextStyle(
color = ZcashTheme.colors.reference,
textAlign = textAlign,
textDecoration = TextDecoration.Underline
textDecoration = TextDecoration.Underline,
fontWeight = FontWeight.SemiBold
)
),
modifier = Modifier.padding(all = ZcashTheme.dimens.spacingDefault)
)
)
}
}
@ -220,7 +271,10 @@ fun HeaderWithZecIcon(
style = ZcashTheme.extendedTypography.zecBalance,
color = MaterialTheme.colorScheme.onBackground,
maxLines = 1,
modifier = Modifier.basicMarquee().then(modifier)
modifier =
Modifier
.basicMarquee()
.then(modifier)
)
}

View File

@ -19,13 +19,10 @@ data class ExtendedColors(
val progressBackground: Color,
val chipIndex: Color,
val textFieldHint: Color,
val textDescription: Color,
val layoutStroke: Color,
val overlay: Color,
val highlight: Color,
val addressHighlightBorder: Color,
val addressHighlightUnified: Color,
val addressHighlightSapling: Color,
val addressHighlightTransparent: Color,
val dangerous: Color,
val onDangerous: Color,
val reference: Color,

View File

@ -27,6 +27,8 @@ internal object Dark {
val textCaption = Color(0xFFFFFFFF)
val textChipIndex = Color(0xFFFFB900)
val textFieldHint = Color(0xFFB7B7B7)
val textDescription = Color(0xFF777777)
val layoutStroke = Color(0xFFFFFFFF)
val primaryButton = Color(0xFFFFFFFF)
@ -90,10 +92,10 @@ internal object Light {
val textCaption = Color(0xFF000000)
val textChipIndex = Color(0xFFEE8592)
val textFieldHint = Color(0xFFB7B7B7)
val textDescription = Color(0xFF777777)
val layoutStroke = Color(0xFF000000)
// TODO [#159]: The button colors are wrong for light
// TODO [#159]: https://github.com/Electric-Coin-Company/zashi-android/issues/159
val primaryButton = Color(0xFF000000)
val primaryButtonPressed = Color(0xFF000000)
val primaryButtonDisabled = Color(0xFF000000)
@ -118,13 +120,6 @@ internal object Light {
val overlay = Color(0x22000000)
val highlight = Color(0xFFFFD800)
// TODO [#159]: The colors are wrong for light theme
// TODO [#159]: https://github.com/Electric-Coin-Company/zashi-android/issues/159
val addressHighlightBorder = Color(0xFF525252)
val addressHighlightUnified = Color(0xFFFFD800)
val addressHighlightSapling = Color(0xFF1BBFF6)
val addressHighlightTransparent = Color(0xFF97999A)
val dangerous = Color(0xFFEC0008)
val onDangerous = Color(0xFFFFFFFF)
@ -179,13 +174,10 @@ internal val DarkExtendedColorPalette =
progressBackground = Dark.progressBackground,
chipIndex = Dark.textChipIndex,
textFieldHint = Dark.textFieldHint,
textDescription = Dark.textDescription,
layoutStroke = Dark.layoutStroke,
overlay = Dark.overlay,
highlight = Dark.highlight,
addressHighlightBorder = Dark.addressHighlightBorder,
addressHighlightUnified = Dark.addressHighlightUnified,
addressHighlightSapling = Dark.addressHighlightSapling,
addressHighlightTransparent = Dark.addressHighlightTransparent,
dangerous = Dark.dangerous,
onDangerous = Dark.onDangerous,
disabledButtonTextColor = Dark.disabledButtonTextColor,
@ -213,13 +205,10 @@ internal val LightExtendedColorPalette =
progressBackground = Light.progressBackground,
chipIndex = Light.textChipIndex,
textFieldHint = Light.textFieldHint,
textDescription = Light.textDescription,
layoutStroke = Light.layoutStroke,
overlay = Light.overlay,
highlight = Light.highlight,
addressHighlightBorder = Light.addressHighlightBorder,
addressHighlightUnified = Light.addressHighlightUnified,
addressHighlightSapling = Light.addressHighlightSapling,
addressHighlightTransparent = Light.addressHighlightTransparent,
dangerous = Light.dangerous,
onDangerous = Light.onDangerous,
disabledButtonTextColor = Light.disabledButtonTextColor,
@ -249,13 +238,10 @@ internal val LocalExtendedColors =
progressBackground = Color.Unspecified,
chipIndex = Color.Unspecified,
textFieldHint = Color.Unspecified,
textDescription = Color.Unspecified,
layoutStroke = Color.Unspecified,
overlay = Color.Unspecified,
highlight = Color.Unspecified,
addressHighlightBorder = Color.Unspecified,
addressHighlightUnified = Color.Unspecified,
addressHighlightSapling = Color.Unspecified,
addressHighlightTransparent = Color.Unspecified,
dangerous = Color.Unspecified,
onDangerous = Color.Unspecified,
disabledButtonTextColor = Color.Unspecified,

View File

@ -107,7 +107,7 @@ internal val SecondaryTypography =
headlineSmall =
TextStyle(
fontFamily = ArchivoFontFamily,
fontWeight = FontWeight.Bold,
fontWeight = FontWeight.SemiBold,
fontSize = 20.sp,
textAlign = TextAlign.Center
),

View File

@ -1,185 +0,0 @@
package co.electriccoin.zcash.ui.screen.address.view
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.fixture.WalletAddressesFixture
import cash.z.ecc.android.sdk.model.WalletAddresses
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.address.WalletAddressesTag
import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
class WalletAddressViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
@Test
@MediumTest
fun initial_screen_setup() =
runTest {
val walletAddresses = WalletAddressesFixture.new()
newTestSetup(walletAddresses)
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_unified)).also {
it.assertExists()
}
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_sapling)).also {
it.assertExists()
}
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_transparent)).also {
it.assertExists()
}
composeTestRule.onNodeWithText(walletAddresses.unified.address).also {
it.assertExists()
}
composeTestRule.onNodeWithText(walletAddresses.sapling.address).also {
it.assertDoesNotExist()
}
composeTestRule.onNodeWithText(walletAddresses.transparent.address).also {
it.assertDoesNotExist()
}
}
@Test
@MediumTest
fun unified_collapses() =
runTest {
val walletAddresses = WalletAddressesFixture.new()
newTestSetup(walletAddresses)
composeTestRule.onNodeWithText(walletAddresses.unified.address).also {
it.assertExists()
}
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_unified)).also {
it.assertExists()
it.performClick()
}
composeTestRule.onNodeWithText(walletAddresses.unified.address).also {
it.assertDoesNotExist()
}
}
@Test
@MediumTest
fun sapling_expands() =
runTest {
val walletAddresses = WalletAddressesFixture.new()
newTestSetup(walletAddresses)
composeTestRule.onNodeWithText(walletAddresses.sapling.address).also {
it.assertDoesNotExist()
}
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_sapling)).also {
it.assertExists()
it.performClick()
}
composeTestRule.onNodeWithText(walletAddresses.sapling.address).also {
it.assertExists()
}
}
@Test
@MediumTest
fun transparent_expands() =
runTest {
val walletAddresses = WalletAddressesFixture.new()
newTestSetup(walletAddresses)
composeTestRule.onNodeWithText(walletAddresses.transparent.address).also {
it.assertDoesNotExist()
}
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_transparent)).also {
it.assertExists()
it.performClick()
}
composeTestRule.onNodeWithText(walletAddresses.transparent.address).also {
it.assertExists()
}
}
@Test
@MediumTest
fun back_clicked() =
runTest {
val testSetup = newTestSetup(WalletAddressesFixture.new())
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.onNodeWithContentDescription(
getStringResource(R.string.wallet_address_back_content_description)
).also {
it.performClick()
}
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun copy_to_clipboard_clicked() =
runTest {
val testSetup = newTestSetup(WalletAddressesFixture.new())
assertEquals(0, testSetup.getOnCopyToClipboardCount())
composeTestRule.onNodeWithTag(
WalletAddressesTag.WALLET_ADDRESS
).also {
it.performClick()
}
assertEquals(1, testSetup.getOnCopyToClipboardCount())
}
private fun newTestSetup(initialState: WalletAddresses) = TestSetup(composeTestRule, initialState)
private class TestSetup(private val composeTestRule: ComposeContentTestRule, initialState: WalletAddresses) {
private val onBackCount = AtomicInteger(0)
private val onCopyToClipboardCount = AtomicInteger(0)
fun getOnBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
}
fun getOnCopyToClipboardCount(): Int {
composeTestRule.waitForIdle()
return onCopyToClipboardCount.get()
}
init {
composeTestRule.setContent {
ZcashTheme {
WalletAddresses(
walletAddresses = initialState,
onCopyToClipboard = {
onCopyToClipboardCount.incrementAndGet()
},
onBack = {
onBackCount.incrementAndGet()
}
)
}
}
}
}
}

View File

@ -4,6 +4,7 @@ import android.content.Intent
import androidx.test.filters.SmallTest
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.test.getAppContext
import co.electriccoin.zcash.ui.util.FileShareUtil
import org.junit.Assert.assertEquals
import org.junit.Test
import kotlin.io.path.pathString
@ -26,6 +27,7 @@ class FileShareUtilTest {
FileShareUtil.newShareContentIntent(
context = getAppContext(),
dataFilePath = tempFilePath.pathString,
fileType = FileShareUtil.ZASHI_INTERNAL_DATA_MIME_TYPE,
versionInfo = VersionInfoFixture.new()
)
assertEquals(intent.action, Intent.ACTION_VIEW)

View File

@ -2,9 +2,11 @@ package co.electriccoin.zcash.ui.screen.receive.view
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.android.sdk.fixture.WalletAddressesFixture
import cash.z.ecc.android.sdk.model.WalletAddresses
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ -18,7 +20,12 @@ class ReceiveViewScreenBrightnessTest : UiTestPrerequisites() {
@MediumTest
fun testBrightnessDefaultState() =
runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified())
// Using isDebuggable flag to have brightness toggle in the UI
val testSetup =
newTestSetup(
WalletAddressesFixture.new(),
VersionInfoFixture.new(isDebuggable = true)
)
assertEquals(0, testSetup.getScreenBrightnessCount())
}
@ -27,7 +34,12 @@ class ReceiveViewScreenBrightnessTest : UiTestPrerequisites() {
@MediumTest
fun testBrightnessOnState() =
runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified())
// Using isDebuggable flag to have brightness toggle in the UI
val testSetup =
newTestSetup(
WalletAddressesFixture.new(),
VersionInfoFixture.new(isDebuggable = true)
)
assertEquals(false, testSetup.getOnAdjustBrightness())
assertEquals(0, testSetup.getScreenBrightnessCount())
@ -38,5 +50,8 @@ class ReceiveViewScreenBrightnessTest : UiTestPrerequisites() {
assertEquals(1, testSetup.getScreenBrightnessCount())
}
private fun newTestSetup(walletAddress: WalletAddress) = ReceiveViewTestSetup(composeTestRule, walletAddress)
private fun newTestSetup(
walletAddresses: WalletAddresses,
versionInfo: VersionInfo
) = ReceiveViewTestSetup(composeTestRule, walletAddresses, versionInfo)
}

View File

@ -2,9 +2,11 @@ package co.electriccoin.zcash.ui.screen.receive.view
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.android.sdk.fixture.WalletAddressesFixture
import cash.z.ecc.android.sdk.model.WalletAddresses
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ -18,7 +20,11 @@ class ReceiveViewScreenTimeoutTest : UiTestPrerequisites() {
@MediumTest
fun testTimeoutDefaultState() =
runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified())
val testSetup =
newTestSetup(
WalletAddressesFixture.new(),
VersionInfoFixture.new()
)
assertEquals(0, testSetup.getScreenTimeoutCount())
}
@ -27,7 +33,12 @@ class ReceiveViewScreenTimeoutTest : UiTestPrerequisites() {
@MediumTest
fun testTimeoutOnState() =
runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified())
// Using isDebuggable flag to have brightness toggle in the UI
val testSetup =
newTestSetup(
WalletAddressesFixture.new(),
VersionInfoFixture.new(isDebuggable = true)
)
assertEquals(false, testSetup.getOnAdjustBrightness())
assertEquals(0, testSetup.getScreenTimeoutCount())
@ -38,5 +49,8 @@ class ReceiveViewScreenTimeoutTest : UiTestPrerequisites() {
assertEquals(1, testSetup.getScreenTimeoutCount())
}
private fun newTestSetup(walletAddress: WalletAddress) = ReceiveViewTestSetup(composeTestRule, walletAddress)
private fun newTestSetup(
walletAddresses: WalletAddresses,
versionInfo: VersionInfo
) = ReceiveViewTestSetup(composeTestRule, walletAddresses, versionInfo)
}

View File

@ -5,8 +5,8 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.android.sdk.fixture.WalletAddressesFixture
import cash.z.ecc.android.sdk.model.WalletAddresses
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.coroutines.test.runTest
@ -14,6 +14,9 @@ import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
// TODO [#1184]: Improve ReceiveScreen UI tests
// TODO [#1184]: https://github.com/Electric-Coin-Company/zashi-android/issues/1184
/*
* Note: It is difficult to test the QR code from automated tests. There is a manual test case
* for that currently. A future enhancement could take a screenshot and try to analyze the
@ -27,11 +30,11 @@ class ReceiveViewTest {
@MediumTest
fun setup() =
runTest {
val walletAddress = WalletAddressFixture.unified()
newTestSetup(walletAddress)
val walletAddresses = WalletAddressesFixture.new()
newTestSetup(walletAddresses)
// Enable substring for ellipsizing
composeTestRule.onNodeWithText(walletAddress.address, substring = true).also {
composeTestRule.onNodeWithText(walletAddresses.unified.address, substring = true).also {
it.assertExists()
}
}
@ -40,7 +43,7 @@ class ReceiveViewTest {
@MediumTest
fun click_settings_test() =
runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified())
val testSetup = newTestSetup(WalletAddressesFixture.new())
assertEquals(0, testSetup.getOnSettingsCount())
@ -53,23 +56,5 @@ class ReceiveViewTest {
assertEquals(1, testSetup.getOnSettingsCount())
}
@Test
@MediumTest
fun address_details() =
runTest {
val testSetup = newTestSetup(WalletAddressFixture.unified())
assertEquals(0, testSetup.getOnAddressDetailsCount())
composeTestRule.onNodeWithText(
text = getStringResource(R.string.receive_see_address_details),
ignoreCase = true
).also {
it.performClick()
}
assertEquals(1, testSetup.getOnAddressDetailsCount())
}
private fun newTestSetup(walletAddress: WalletAddress) = ReceiveViewTestSetup(composeTestRule, walletAddress)
private fun newTestSetup(walletAddresses: WalletAddresses) = ReceiveViewTestSetup(composeTestRule, walletAddresses)
}

View File

@ -1,23 +1,27 @@
package co.electriccoin.zcash.ui.screen.receive.view
import androidx.compose.material3.SnackbarHostState
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 cash.z.ecc.android.sdk.model.WalletAddresses
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.common.model.VersionInfo
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
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
walletAddresses: WalletAddresses,
versionInfo: VersionInfo = VersionInfoFixture.new()
) {
private val onSettingsCount = AtomicInteger(0)
private val onAddressDetailsCount = AtomicInteger(0)
@ -53,16 +57,17 @@ class ReceiveViewTestSetup(
ZcashTheme {
ZcashTheme {
Receive(
walletAddress,
walletAddress = walletAddresses,
snackbarHostState = SnackbarHostState(),
onSettings = {
onSettingsCount.getAndIncrement()
},
onAddressDetails = {
onAddressDetailsCount.getAndIncrement()
},
onAdjustBrightness = {
onAdjustBrightness.getAndSet(it)
},
onAddrCopyToClipboard = {},
onQrImageShare = {},
versionInfo = versionInfo
)
}
}

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.exportdata.util
package co.electriccoin.zcash.global
import androidx.core.content.FileProvider
import co.electriccoin.zcash.ui.R

View File

@ -20,11 +20,9 @@ import co.electriccoin.zcash.ui.NavigationTargets.SCAN
import co.electriccoin.zcash.ui.NavigationTargets.SEED_RECOVERY
import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
import co.electriccoin.zcash.ui.NavigationTargets.WALLET_ADDRESS_DETAILS
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.screen.about.WrapAbout
import co.electriccoin.zcash.ui.screen.address.WrapWalletAddresses
import co.electriccoin.zcash.ui.screen.exportdata.WrapExportPrivateData
import co.electriccoin.zcash.ui.screen.history.WrapHistory
import co.electriccoin.zcash.ui.screen.home.WrapHome
@ -51,7 +49,6 @@ internal fun MainActivity.Navigation() {
onPageChange = {
homeViewModel.screenIndex.value = it
},
goAddressDetails = { navController.navigateJustOnce(WALLET_ADDRESS_DETAILS) },
goBack = { finish() },
goHistory = { navController.navigateJustOnce(HISTORY) },
goSettings = { navController.navigateJustOnce(SETTINGS) },
@ -72,13 +69,6 @@ internal fun MainActivity.Navigation() {
WrapCheckForUpdate()
}
}
composable(WALLET_ADDRESS_DETAILS) {
WrapWalletAddresses(
goBack = {
navController.popBackStackJustOnce(WALLET_ADDRESS_DETAILS)
}
)
}
composable(SETTINGS) {
WrapSettings(
goAbout = {
@ -201,5 +191,4 @@ object NavigationTargets {
const val SEND = "send"
const val SETTINGS = "settings"
const val SUPPORT = "support"
const val WALLET_ADDRESS_DETAILS = "wallet_address_details"
}

View File

@ -1,48 +0,0 @@
@file:Suppress("ktlint:standard:filename")
package co.electriccoin.zcash.ui.screen.address
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.spackle.ClipboardManagerUtil
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.address.view.WalletAddresses
@Composable
internal fun MainActivity.WrapWalletAddresses(goBack: () -> Unit) {
WrapWalletAddresses(this, goBack)
}
@Composable
private fun WrapWalletAddresses(
activity: ComponentActivity,
goBack: () -> Unit
) {
val walletViewModel by activity.viewModels<WalletViewModel>()
val walletAddresses = walletViewModel.addresses.collectAsStateWithLifecycle().value
if (null == walletAddresses) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
// TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available
// TODO [#1146]: https://github.com/Electric-Coin-Company/zashi-android/issues/1146
CircularScreenProgressIndicator()
} else {
WalletAddresses(
walletAddresses,
goBack,
onCopyToClipboard = { address ->
ClipboardManagerUtil.copyToClipboard(
activity.applicationContext,
activity.getString(R.string.wallet_address_clipboard_tag),
address
)
},
)
}
}

View File

@ -1,8 +0,0 @@
package co.electriccoin.zcash.ui.screen.address
/**
* These are only used for automated testing.
*/
object WalletAddressesTag {
const val WALLET_ADDRESS = "wallet_address_tag"
}

View File

@ -1,295 +0,0 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui.screen.address.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowDropDownCircle
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.fixture.WalletAddressesFixture
import cash.z.ecc.android.sdk.model.WalletAddresses
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.ListHeader
import co.electriccoin.zcash.ui.design.component.ListItem
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.address.WalletAddressesTag
import kotlinx.coroutines.runBlocking
@Preview("WalletAddresses")
@Composable
private fun ComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
WalletAddresses(
runBlocking { WalletAddressesFixture.new() },
onBack = {},
onCopyToClipboard = {}
)
}
}
}
@Composable
fun WalletAddresses(
walletAddresses: WalletAddresses,
onBack: () -> Unit,
onCopyToClipboard: (String) -> Unit
) {
Column {
WalletDetailTopAppBar(onBack)
WalletDetailAddresses(
walletAddresses = walletAddresses,
onCopyToClipboard = onCopyToClipboard,
modifier =
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
)
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun WalletDetailTopAppBar(onBack: () -> Unit) {
TopAppBar(
title = {
Text(
text = stringResource(id = R.string.wallet_address_title)
)
},
navigationIcon = {
IconButton(
onClick = onBack
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.wallet_address_back_content_description)
)
}
}
)
}
private val BIG_INDICATOR_WIDTH = 24.dp
private val SMALL_INDICATOR_WIDTH = 16.dp
@Composable
private fun WalletDetailAddresses(
walletAddresses: WalletAddresses,
onCopyToClipboard: (String) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier) {
Row(
Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
) {
Image(
painter = ColorPainter(ZcashTheme.colors.highlight),
contentDescription = "",
modifier =
Modifier
.fillMaxHeight()
.width(BIG_INDICATOR_WIDTH)
)
Column(Modifier.fillMaxWidth()) {
ExpandableRow(
title = stringResource(R.string.wallet_address_unified),
content = walletAddresses.unified.address,
isInitiallyExpanded = true,
onCopyToClipboard = onCopyToClipboard
)
Box(Modifier.height(IntrinsicSize.Min)) {
Divider(modifier = Modifier.fillMaxHeight())
ListHeader(
text = stringResource(R.string.wallet_address_header_includes),
modifier = Modifier.padding(all = ZcashTheme.dimens.spacingSmall)
)
}
SaplingAddress(
saplingAddress = walletAddresses.sapling.address,
onCopyToClipboard = onCopyToClipboard,
modifier =
Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
)
TransparentAddress(
transparentAddress = walletAddresses.transparent.address,
onCopyToClipboard = onCopyToClipboard,
modifier =
Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
)
}
}
}
}
// Note: The addresses code below has opportunities to be made more DRY.
// Refactoring that is being held off until issue #160 is fixed, since knowledge
// of row position will be needed.
@Composable
private fun SaplingAddress(
saplingAddress: String,
onCopyToClipboard: (String) -> Unit,
modifier: Modifier = Modifier
) {
Row(modifier) {
SmallIndicator(ZcashTheme.colors.addressHighlightSapling)
ExpandableRow(
title = stringResource(R.string.wallet_address_sapling),
content = saplingAddress,
isInitiallyExpanded = false,
onCopyToClipboard = onCopyToClipboard
)
}
}
@Composable
private fun TransparentAddress(
transparentAddress: String,
onCopyToClipboard: (String) -> Unit,
modifier: Modifier = Modifier
) {
Row(modifier) {
SmallIndicator(ZcashTheme.colors.addressHighlightTransparent)
ExpandableRow(
title = stringResource(R.string.wallet_address_transparent),
content = transparentAddress,
isInitiallyExpanded = false,
onCopyToClipboard = onCopyToClipboard
)
}
}
@Composable
private fun ExpandableRow(
title: String,
content: String,
isInitiallyExpanded: Boolean,
onCopyToClipboard: (String) -> Unit
) {
var expandedState by rememberSaveable { mutableStateOf(isInitiallyExpanded) }
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.defaultMinSize(minHeight = 48.dp)
.clickable { expandedState = !expandedState }
.padding(
horizontal = ZcashTheme.dimens.spacingDefault,
vertical = ZcashTheme.dimens.spacingTiny
)
) {
ListItem(text = title)
Spacer(
modifier =
Modifier
.fillMaxWidth()
.weight(MINIMAL_WEIGHT)
)
ExpandableArrow(expandedState)
}
if (expandedState) {
Body(
content,
modifier =
Modifier
.clickable { onCopyToClipboard(content) }
.padding(
horizontal = ZcashTheme.dimens.spacingDefault,
vertical = ZcashTheme.dimens.spacingTiny
)
.testTag(WalletAddressesTag.WALLET_ADDRESS)
)
}
}
}
@Composable
private fun SmallIndicator(color: Color) {
// TODO [#160]: Border is not the right implementation here, as it causes double thickness for the middle item
// TODO [#160]: https://github.com/Electric-Coin-Company/zashi-android/issues/160
Image(
modifier =
Modifier
.fillMaxHeight()
.width(SMALL_INDICATOR_WIDTH)
.border(1.dp, ZcashTheme.colors.addressHighlightBorder),
painter = ColorPainter(color),
contentDescription = ""
)
}
private const val NINETY_DEGREES = 90f
@Composable
private fun ExpandableArrow(isExpanded: Boolean) {
Icon(
imageVector = Icons.Filled.ArrowDropDownCircle,
contentDescription =
if (isExpanded) {
stringResource(id = R.string.wallet_address_hide)
} else {
stringResource(id = R.string.wallet_address_show)
},
modifier =
if (isExpanded) {
Modifier
} else {
Modifier.rotate(NINETY_DEGREES)
},
tint = MaterialTheme.colorScheme.onBackground
)
}

View File

@ -17,8 +17,8 @@ import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.exportdata.util.FileShareUtil
import co.electriccoin.zcash.ui.screen.exportdata.view.ExportPrivateData
import co.electriccoin.zcash.ui.util.FileShareUtil
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
@ -93,6 +93,7 @@ fun shareData(
context = context,
network = ZcashNetwork.fromResources(context)
),
fileType = FileShareUtil.ZASHI_INTERNAL_DATA_MIME_TYPE,
versionInfo = VersionInfo.new(context.applicationContext)
)
runCatching {

View File

@ -26,7 +26,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
@Composable
internal fun MainActivity.WrapHome(
onPageChange: (HomeScreenIndex) -> Unit,
goAddressDetails: () -> Unit,
goBack: () -> Unit,
goHistory: () -> Unit,
goSettings: () -> Unit,
@ -36,7 +35,6 @@ internal fun MainActivity.WrapHome(
WrapHome(
this,
onPageChange = onPageChange,
goAddressDetails = goAddressDetails,
goBack = goBack,
goHistory = goHistory,
goScan = goScan,
@ -49,7 +47,6 @@ internal fun MainActivity.WrapHome(
@Composable
internal fun WrapHome(
activity: ComponentActivity,
goAddressDetails: () -> Unit,
goBack: () -> Unit,
goHistory: () -> Unit,
goSettings: () -> Unit,
@ -117,7 +114,6 @@ internal fun WrapHome(
WrapReceive(
activity = activity,
onSettings = goSettings,
onAddressDetails = goAddressDetails,
)
}
),

View File

@ -2,48 +2,134 @@
package co.electriccoin.zcash.ui.screen.receive
import android.content.Context
import android.graphics.Bitmap
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.model.WalletAddresses
import co.electriccoin.zcash.spackle.ClipboardManagerUtil
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.spackle.getInternalCacheDirSuspend
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.receive.view.Receive
import co.electriccoin.zcash.ui.util.FileShareUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
@Composable
internal fun WrapReceive(
activity: ComponentActivity,
onSettings: () -> Unit,
onAddressDetails: () -> Unit,
) {
val viewModel by activity.viewModels<WalletViewModel>()
val walletAddresses = viewModel.addresses.collectAsStateWithLifecycle().value
WrapReceive(
walletAddresses,
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val versionInfo = VersionInfo.new(activity.applicationContext)
Receive(
walletAddress = walletAddresses,
snackbarHostState = snackbarHostState,
onAdjustBrightness = { /* Just for testing purposes */ },
onAddrCopyToClipboard = { address ->
ClipboardManagerUtil.copyToClipboard(
activity.applicationContext,
activity.getString(R.string.receive_clipboard_tag),
address
)
},
onQrImageShare = { imageBitmap ->
scope.launch {
shareData(
context = activity.applicationContext,
snackbarHostState = snackbarHostState,
qrImageBitmap = imageBitmap.asAndroidBitmap(),
versionInfo = versionInfo
).collect { shareResult ->
Twig.info {
if (shareResult) {
"Sharing the address QR code was successful"
} else {
"Sharing the address QR code failed"
}
}
// No other action for now
}
}
},
onSettings = onSettings,
onAddressDetails = onAddressDetails,
versionInfo = versionInfo
)
}
@Composable
internal fun WrapReceive(
walletAddresses: WalletAddresses?,
onSettings: () -> Unit,
onAddressDetails: () -> Unit,
) {
if (null == walletAddresses) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
// TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available
// TODO [#1146]: https://github.com/Electric-Coin-Company/zashi-android/issues/1146
CircularScreenProgressIndicator()
} else {
Receive(
walletAddresses.unified,
onSettings = onSettings,
onAddressDetails = onAddressDetails,
onAdjustBrightness = { /* Just for testing */ }
)
private const val CACHE_SUBDIR = "zcash_address_qr_images" // NON-NLS
private const val TEMP_FILE_NAME_PREFIX = "zcash_address_qr_" // NON-NLS
private const val TEMP_FILE_NAME_SUFFIX = ".png" // NON-NLS
fun shareData(
context: Context,
snackbarHostState: SnackbarHostState,
qrImageBitmap: Bitmap,
versionInfo: VersionInfo
): Flow<Boolean> =
callbackFlow {
// Initialize cache directory
val cacheDir = context.getInternalCacheDirSuspend(CACHE_SUBDIR)
// Save the bitmap to a temporary file in the cache directory
val bitmapFile =
withContext(Dispatchers.IO) {
File.createTempFile(
TEMP_FILE_NAME_PREFIX,
TEMP_FILE_NAME_SUFFIX,
cacheDir,
).also {
it.storeBitmap(qrImageBitmap)
}
}
// Example of the expected temporary file path:
// /data/user/0/co.electriccoin.zcash.debug/cache/zcash_address_qr_images/
// zcash_address_qr_6455164324646067652.png
val shareIntent =
FileShareUtil.newShareContentIntent(
context = context,
dataFilePath = bitmapFile.absolutePath,
versionInfo = versionInfo,
fileType = FileShareUtil.ZASHI_QR_CODE_MIME_TYPE
)
runCatching {
context.startActivity(shareIntent)
trySend(true)
}.onFailure {
snackbarHostState.showSnackbar(message = context.getString(R.string.receive_data_unable_to_share))
trySend(false)
}
awaitClose {
// No resources to release
}
}
suspend fun File.storeBitmap(bitmap: Bitmap) =
withContext(Dispatchers.IO) {
outputStream().use { fOut ->
@Suppress("MagicNumber")
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut)
fOut.flush()
}
}
}

View File

@ -8,10 +8,7 @@ object JvmQrCodeGenerator : QrCodeGenerator {
data: String,
sizePixels: Int
): BooleanArray {
val bitMatrix =
QRCodeWriter().let {
it.encode(data, BarcodeFormat.QR_CODE, sizePixels, sizePixels)
}
val bitMatrix = QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, sizePixels, sizePixels)
return BooleanArray(sizePixels * sizePixels).apply {
var booleanArrayPosition = 0

View File

@ -1,11 +1,16 @@
package co.electriccoin.zcash.ui.screen.receive.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@ -13,34 +18,41 @@ import androidx.compose.material.icons.filled.BrightnessHigh
import androidx.compose.material.icons.filled.BrightnessLow
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.fixture.WalletAddressesFixture
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.android.sdk.model.WalletAddresses
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.BrightenScreen
import co.electriccoin.zcash.ui.common.DisableScreenTimeout
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.test.CommonTag
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.Reference
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.component.SubHeader
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.screen.receive.util.AndroidQrCodeImageGenerator
import co.electriccoin.zcash.ui.screen.receive.util.JvmQrCodeGenerator
import kotlinx.coroutines.runBlocking
@ -52,46 +64,63 @@ private fun ComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
Receive(
walletAddress = runBlocking { WalletAddressFixture.unified() },
walletAddress = runBlocking { WalletAddressesFixture.new() },
snackbarHostState = SnackbarHostState(),
onSettings = {},
onAddressDetails = {},
onAdjustBrightness = {},
onAddrCopyToClipboard = {},
onQrImageShare = {},
versionInfo = VersionInfoFixture.new()
)
}
}
}
@Suppress("LongParameterList")
@Composable
fun Receive(
walletAddress: WalletAddress,
walletAddress: WalletAddresses?,
snackbarHostState: SnackbarHostState,
onSettings: () -> Unit,
onAddressDetails: () -> Unit,
onAdjustBrightness: (Boolean) -> Unit,
onAddrCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit,
versionInfo: VersionInfo,
) {
val (brightness, setBrightness) = rememberSaveable { mutableStateOf(false) }
Scaffold(topBar = {
ReceiveTopAppBar(
adjustBrightness = brightness,
onSettings = onSettings,
onBrightness = {
onAdjustBrightness(!brightness)
setBrightness(!brightness)
}
)
}) { paddingValues ->
ReceiveContents(
walletAddress = walletAddress,
onAddressDetails = onAddressDetails,
adjustBrightness = brightness,
modifier =
Modifier.padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingHuge,
start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
end = ZcashTheme.dimens.screenHorizontalSpacingRegular
)
)
Scaffold(
topBar = {
ReceiveTopAppBar(
adjustBrightness = brightness,
onSettings = onSettings,
onBrightness = {
onAdjustBrightness(!brightness)
setBrightness(!brightness)
},
versionInfo = versionInfo,
)
},
snackbarHost = { SnackbarHost(snackbarHostState) },
) { paddingValues ->
if (null == walletAddress) {
CircularScreenProgressIndicator()
} else {
ReceiveContents(
walletAddress = walletAddress,
onAddressCopyToClipboard = onAddrCopyToClipboard,
onQrImageShare = onQrImageShare,
adjustBrightness = brightness,
versionInfo = versionInfo,
modifier =
Modifier.padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingDefault,
start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
end = ZcashTheme.dimens.screenHorizontalSpacingRegular
)
)
}
}
}
@ -99,23 +128,26 @@ fun Receive(
private fun ReceiveTopAppBar(
adjustBrightness: Boolean,
onSettings: () -> Unit,
onBrightness: () -> Unit
onBrightness: () -> Unit,
versionInfo: VersionInfo
) {
SmallTopAppBar(
titleText = stringResource(id = R.string.receive_title),
regularActions = {
IconButton(
onClick = onBrightness
) {
Icon(
imageVector =
if (adjustBrightness) {
Icons.Default.BrightnessLow
} else {
Icons.Default.BrightnessHigh
},
contentDescription = stringResource(R.string.receive_brightness_content_description)
)
if (versionInfo.isDebuggable) {
IconButton(
onClick = onBrightness
) {
Icon(
imageVector =
if (adjustBrightness) {
Icons.Default.BrightnessLow
} else {
Icons.Default.BrightnessHigh
},
contentDescription = stringResource(R.string.receive_brightness_content_description)
)
}
}
},
hamburgerMenuActions = {
@ -132,13 +164,14 @@ private fun ReceiveTopAppBar(
)
}
private val DEFAULT_QR_CODE_SIZE = 320.dp
@Suppress("LongParameterList")
@Composable
private fun ReceiveContents(
walletAddress: WalletAddress,
onAddressDetails: () -> Unit,
walletAddress: WalletAddresses,
onAddressCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit,
adjustBrightness: Boolean,
versionInfo: VersionInfo,
modifier: Modifier = Modifier,
) {
Column(
@ -149,76 +182,160 @@ private fun ReceiveContents(
.then(modifier),
horizontalAlignment = 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))
Body(
text = stringResource(id = R.string.wallet_address_unified),
Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
// TODO [#163]: Ellipsize center of the string
// TODO [#163]: https://github.com/Electric-Coin-Company/zashi-android/issues/163
Text(
text = walletAddress.address,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.align(Alignment.CenterHorizontally),
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
Spacer(
modifier =
Modifier
.fillMaxHeight()
.weight(MINIMAL_WEIGHT)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
PrimaryButton(
onClick = onAddressDetails,
text = stringResource(id = R.string.receive_see_address_details)
)
}
}
@Composable
private fun QrCode(
data: String,
size: Dp,
modifier: Modifier = Modifier,
adjustBrightness: Boolean = false,
) {
Column(modifier = modifier) {
if (adjustBrightness) {
BrightenScreen()
DisableScreenTimeout()
}
val sizePixels = with(LocalDensity.current) { size.toPx() }.roundToInt()
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
// In the future, use actual/expect to switch QR code generator implementations for multiplatform
Address(
walletAddress = walletAddress.unified,
onAddressCopyToClipboard = onAddressCopyToClipboard,
onQrImageShare = onQrImageShare,
)
// Note that our implementation has an extra array copy to BooleanArray, which is a cross-platform
// representation. This should have minimal performance impact since the QR code is relatively
// small and we only generate QR codes infrequently.
if (versionInfo.isTestnet) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
val qrCodePixelArray = JvmQrCodeGenerator.generate(data, sizePixels)
val qrCodeImage = AndroidQrCodeImageGenerator.generate(qrCodePixelArray, sizePixels)
Address(
walletAddress = walletAddress.sapling,
onAddressCopyToClipboard = onAddressCopyToClipboard,
onQrImageShare = onQrImageShare,
)
}
Image(
bitmap = qrCodeImage,
contentDescription = stringResource(R.string.receive_qr_code_content_description)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
Address(
walletAddress = walletAddress.transparent,
onAddressCopyToClipboard = onAddressCopyToClipboard,
onQrImageShare = onQrImageShare,
)
}
}
private val DEFAULT_QR_CODE_SIZE = 320.dp
@Suppress("LongMethod")
@Composable
private fun Address(
walletAddress: WalletAddress,
onAddressCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
SubHeader(
text =
stringResource(
id =
when (walletAddress) {
is WalletAddress.Unified -> R.string.receive_wallet_address_unified
is WalletAddress.Sapling -> R.string.receive_wallet_address_sapling
is WalletAddress.Transparent -> R.string.receive_wallet_address_transparent
}
),
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
val sizePixels = with(LocalDensity.current) { DEFAULT_QR_CODE_SIZE.toPx() }.roundToInt()
val qrCodeImage =
remember {
qrCodeForAddress(
address = walletAddress.address,
size = sizePixels
)
}
QrCode(
qrCodeImage = qrCodeImage,
onQrImageBitmapShare = onQrImageShare,
contentDescription =
stringResource(
id =
when (walletAddress) {
is WalletAddress.Unified -> R.string.receive_unified_content_description
is WalletAddress.Sapling -> R.string.receive_sapling_content_description
is WalletAddress.Transparent -> R.string.receive_transparent_content_description
}
),
modifier = Modifier.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
// TODO [#163]: Ellipsize center of the string
// TODO [#163]: https://github.com/Electric-Coin-Company/zashi-android/issues/163
Text(
text = walletAddress.address,
style = ZcashTheme.typography.primary.bodyLarge,
color = ZcashTheme.colors.textDescription,
textAlign = TextAlign.Center,
modifier =
Modifier
.align(Alignment.CenterHorizontally)
.clickable { onAddressCopyToClipboard(walletAddress.address) }
.padding(horizontal = ZcashTheme.dimens.spacingLarge)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Reference(
text = stringResource(id = R.string.receive_copy),
onClick = { onAddressCopyToClipboard(walletAddress.address) },
textAlign = TextAlign.Center,
imageVector = ImageVector.vectorResource(R.drawable.copy),
imageContentDescription = null,
modifier = Modifier.wrapContentSize(),
)
Reference(
text = stringResource(id = R.string.receive_share),
onClick = { onQrImageShare(qrCodeImage) },
textAlign = TextAlign.Center,
imageVector = ImageVector.vectorResource(R.drawable.share),
imageContentDescription = null,
modifier = Modifier.wrapContentSize(),
)
}
}
}
private fun qrCodeForAddress(
address: String,
size: Int,
): ImageBitmap {
// In the future, use actual/expect to switch QR code generator implementations for multiplatform
// Note that our implementation has an extra array copy to BooleanArray, which is a cross-platform
// representation. This should have minimal performance impact since the QR code is relatively
// small and we only generate QR codes infrequently.
val qrCodePixelArray = JvmQrCodeGenerator.generate(address, size)
return AndroidQrCodeImageGenerator.generate(qrCodePixelArray, size)
}
@Composable
private fun QrCode(
contentDescription: String,
qrCodeImage: ImageBitmap,
onQrImageBitmapShare: (ImageBitmap) -> Unit,
modifier: Modifier = Modifier,
) {
Image(
bitmap = qrCodeImage,
contentDescription = contentDescription,
modifier =
Modifier
.clickable { onQrImageBitmapShare(qrCodeImage) }
.then(modifier)
)
}

View File

@ -37,12 +37,13 @@ private const val MAX_EXCEPTIONS_TO_REPORT = 5
suspend fun CrashInfo.Companion.all(context: Context): List<CrashInfo> {
val exceptionDirectory = ExceptionPath.getExceptionDirectory(context) ?: return emptyList()
val filesList: List<File> = exceptionDirectory.listFilesSuspend().toList()
return filesList
.mapNotNull {
val filesList: List<File>? = exceptionDirectory.listFilesSuspend()?.toList()
return filesList?.run {
mapNotNull {
ReportedException.new(it)
}.sortedBy { it.time }
.reversed()
.take(MAX_EXCEPTIONS_TO_REPORT)
.map { CrashInfo(it.exceptionClassName, it.isUncaught, it.time) }
.reversed()
.take(MAX_EXCEPTIONS_TO_REPORT)
.map { CrashInfo(it.exceptionClassName, it.isUncaught, it.time) }
} ?: emptyList()
}

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.exportdata.util
package co.electriccoin.zcash.ui.util
import android.content.Context
import android.content.Intent
@ -13,6 +13,7 @@ object FileShareUtil {
const val SHARE_CONTENT_PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
const val ZASHI_INTERNAL_DATA_MIME_TYPE = "application/octet-stream" // NON-NLS
const val ZASHI_QR_CODE_MIME_TYPE = "image/png" // NON-NLS
const val ZASHI_INTERNAL_DATA_AUTHORITY = "co.electriccoin.zcash.provider" // NON-NLS
const val ZASHI_INTERNAL_DATA_AUTHORITY_DEBUG = "co.electriccoin.zcash.debug.provider" // NON-NLS
@ -30,7 +31,8 @@ object FileShareUtil {
internal fun newShareContentIntent(
context: Context,
dataFilePath: String,
versionInfo: VersionInfo
fileType: String,
versionInfo: VersionInfo,
): Intent {
val fileUri =
FileProvider.getUriForFile(
@ -43,7 +45,7 @@ object FileShareUtil {
Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, fileUri)
type = ZASHI_INTERNAL_DATA_MIME_TYPE
type = fileType
}
val shareDataIntent =

View File

@ -1,21 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<!-- TODO [#1183]: Rework the way we grant access to file provider -->
<!-- TODO [#1183]: https://github.com/Electric-Coin-Company/zashi-android/issues/1183 -->
<!-- Android Studio complains about root-path. Search for an alternative way of approaching no_backup folder -->
<!-- Another way is to split paths into packages depending on current build type -->
<root-path
name="root_mainnet_release"
path="/data/data/co.electriccoin.zcash/no_backup/co.electricoin.zcash/."
path="/data/data/co.electriccoin.zcash/."
/>
<root-path
name="root_mainnet_debug"
path="/data/data/co.electriccoin.zcash.debug/no_backup/co.electricoin.zcash/."
path="/data/data/co.electriccoin.zcash.debug/."
/>
<root-path
name="root_testnet_release"
path="/data/data/co.electriccoin.zcash.testnet/no_backup/co.electricoin.zcash/."
path="/data/data/co.electriccoin.zcash.testnet/."
/>
<root-path
name="root_testnet_debug"
path="/data/data/co.electriccoin.zcash.testnet.debug/no_backup/co.electricoin.zcash/."
path="/data/data/co.electriccoin.zcash.testnet.debug/."
/>
</paths>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<group>
<clip-path
android:pathData="M0,0h16v16h-16z"/>
<path
android:pathData="M0,16V2.823H13.206V16H0ZM11.832,14.605V4.218H1.374V14.622H11.832V14.605Z"
android:fillColor="#000000"/>
<path
android:pathData="M15.313,11.816C14.939,11.816 14.627,11.504 14.627,11.126V1.395H4.839C4.465,1.395 4.152,1.083 4.152,0.706C4.152,0.328 4.465,0.017 4.839,0.017H16V11.143C16,11.52 15.688,11.832 15.313,11.832V11.816Z"
android:fillColor="#000000"/>
</group>
</vector>

View File

@ -0,0 +1,40 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="14dp"
android:viewportWidth="16"
android:viewportHeight="14">
<group>
<clip-path
android:pathData="M0,0.155h16v13.58h-16z"/>
<path
android:pathData="M16,12.005H0V13.735H16V12.005Z"
android:fillColor="#231F20"/>
<path
android:pathData="M15.5,12.505H0.5V13.235H15.5V12.505Z"
android:fillColor="#231F20"/>
<path
android:pathData="M1.7,8.465H0V13.735H1.7V8.465Z"
android:fillColor="#231F20"/>
<path
android:pathData="M1.2,8.965H0.5V13.235H1.2V8.965Z"
android:fillColor="#231F20"/>
<path
android:pathData="M16,8.465H14.3V13.735H16V8.465Z"
android:fillColor="#231F20"/>
<path
android:pathData="M15.5,8.965H14.8V13.235H15.5V8.965Z"
android:fillColor="#231F20"/>
<path
android:pathData="M8.85,1.175H7.15V8.335H8.85V1.175Z"
android:fillColor="#231F20"/>
<path
android:pathData="M8.35,1.675H7.65V7.835H8.35V1.675Z"
android:fillColor="#231F20"/>
<path
android:pathData="M8.5,1.395L8,0.875L7.5,1.395L5.42,3.555L5.92,4.075L7.99,1.915L10.07,4.075L10.57,3.555L8.5,1.395Z"
android:fillColor="#231F20"/>
<path
android:pathData="M8,2.625L5.93,4.785L4.73,3.555L8,0.155L11.26,3.555L10.08,4.785L8,2.625Z"
android:fillColor="#231F20"/>
</group>
</vector>

View File

@ -2,8 +2,14 @@
<resources>
<string name="receive_title">Receive</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_caption">Your Address</string>
<string name="receive_see_address_details">See Address Details</string>
<string name="receive_unified_content_description">Unified Address QR code</string>
<string name="receive_sapling_content_description">Sapling Address QR code</string>
<string name="receive_transparent_content_description">Transparent Address QR code</string>
<string name="receive_wallet_address_unified">Unified Address</string>
<string name="receive_wallet_address_sapling">Sapling Address</string>
<string name="receive_wallet_address_transparent">Transparent Address</string>
<string name="receive_copy">Copy</string>
<string name="receive_share">Share</string>
<string name="receive_clipboard_tag">Zcash Wallet Address</string>
<string name="receive_data_unable_to_share">Unable to find an application to share the QR code with.</string>
</resources>

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="wallet_address_title">My wallet addresses</string>
<string name="wallet_address_back_content_description">Back</string>
<string name="wallet_address_unified">Your Unified Address:</string>
<string name="wallet_address_header_includes">which includes</string>
<string name="wallet_address_sapling">Shielded Sapling (NU1)</string>
<string name="wallet_address_transparent">Transparent</string>
<string name="wallet_address_show">Show address</string>
<string name="wallet_address_hide">Hide address</string>
<string name="wallet_address_clipboard_tag">Zcash Wallet Address</string>
</resources>

View File

@ -3,9 +3,9 @@
<string name="not_enough_space_title">Not enough space!</string>
<string name="not_enough_space_logo_content_description"></string>
<string name="not_enough_space_description">You need approximately <xliff:g example="1" id="required_gigabytes">
%1$d</xliff:g> gig of space while synchronizing the Zcash blockchain, but only 300 megs once done. Syncing
%1$d</xliff:g> Gbyte of space while synchronizing the Zcash blockchain, but only 300 Mbyte once done. Syncing
will stay paused until more space is available.</string>
<string name="space_required_to_continue"><xliff:g example="300" id="required_megabytes">~%1$d</xliff:g> megs
<string name="space_required_to_continue"><xliff:g example="300" id="required_megabytes">~%1$d</xliff:g> Mbyte
required to continue </string>
<string name="unknown">Unknown</string>
</resources>

View File

@ -290,9 +290,6 @@ class ScreenshotTest : UiTestPrerequisites() {
composeTestRule.navigateInHomeTab(HomeTag.TAB_BALANCES)
balancesScreenshots(resContext, tag, composeTestRule)
navigateTo(NavigationTargets.WALLET_ADDRESS_DETAILS)
addressDetailsScreenshots(resContext, tag, composeTestRule)
navigateTo(NavigationTargets.HISTORY)
transactionHistoryScreenshots(resContext, tag, composeTestRule)
@ -419,18 +416,6 @@ private fun settingsScreenshots(
ScreenshotTest.takeScreenshot(tag, "Settings 1")
}
private fun addressDetailsScreenshots(
resContext: Context,
tag: String,
composeTestRule: ComposeTestRule
) {
composeTestRule.onNode(hasText(resContext.getString(R.string.wallet_address_title))).also {
it.assertExists()
}
ScreenshotTest.takeScreenshot(tag, "Addresses 1")
}
private fun transactionHistoryScreenshots(
resContext: Context,
tag: String,
@ -468,7 +453,7 @@ private fun receiveZecScreenshots(
composeTestRule.onNode(
hasContentDescription(
value = resContext.getString(R.string.receive_qr_code_content_description),
value = resContext.getString(R.string.receive_unified_content_description),
ignoreCase = true
)
).also {