[#1031] Export of private app data

* [#1031] Export of private app data UI+logic

- Closes #1031

* [#1031] Export of private app data tests

* Move provider to app/manifest

To avoid: The application could not be installed: INSTALL_FAILED_CONFLICTING_PROVIDER

* [#1037] Debuggable release build

- So we’re able to log or debug release app build while testing
- Default value false
- Closes #1037

* Fix file provider path in release build
This commit is contained in:
Honza Rychnovský 2023-11-13 14:53:16 +01:00 committed by GitHub
parent 70d5721845
commit 494d068168
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 718 additions and 45 deletions

View File

@ -125,9 +125,12 @@ android {
"proguard-project.txt"
)
val isReleaseBuildDebuggable = project.property("IS_RELEASE_BUILD_DEBUGGABLE")
.toString().toBoolean()
isDebuggable = isReleaseBuildDebuggable
val isSignReleaseBuildWithDebugKey = project.property("IS_SIGN_RELEASE_BUILD_WITH_DEBUG_KEY")
.toString().toBoolean()
if (isReleaseSigningConfigured) {
signingConfig = signingConfigs.getByName("release")
} else if (isSignReleaseBuildWithDebugKey) {

View File

@ -28,6 +28,16 @@
android:shell="true"
tools:targetApi="29" />
<provider
android:name="co.electriccoin.zcash.ui.screen.exportdata.util.ShareFileProvider"
android:authorities="co.electriccoin.zcash.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/share_file_provider_paths" />
</provider>
</application>
</manifest>

View File

@ -112,6 +112,7 @@ tasks {
"ZCASH_RELEASE_KEY_ALIAS_PASSWORD" to "",
"IS_SIGN_RELEASE_BUILD_WITH_DEBUG_KEY" to "false",
"IS_RELEASE_BUILD_DEBUGGABLE" to "false",
"ZCASH_GOOGLE_PLAY_SERVICE_ACCOUNT" to "",
"ZCASH_GOOGLE_PLAY_SERVICE_ACCOUNT_KEY" to "",

View File

@ -85,6 +85,9 @@ ZCASH_RELEASE_KEY_ALIAS_PASSWORD=
# be useful, for example, for running benchmark tests against a release build of the app signed with
# the default debug key configuration.
IS_SIGN_RELEASE_BUILD_WITH_DEBUG_KEY=false
# Switch this property to true only if you need the release build to be debuggable. It can be helpful, for example,
# for logging or debugging minified release app build.
IS_RELEASE_BUILD_DEBUGGABLE=false
# Set the Google Play Service Account email address to enable deployment
# Note that this property is not currently used due to #1033
@ -181,8 +184,8 @@ ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
ZCASH_BIP39_VERSION=1.0.6
ZXING_VERSION=3.5.1
# Ensure a non-snapshot version is used before releasing to production.
ZCASH_SDK_VERSION=2.0.2
# WARNING: Ensure a non-snapshot version is used before releasing to production.
ZCASH_SDK_VERSION=2.0.2-SNAPSHOT
# Toolchain is the Java version used to build the application, which is separate from the
# Java version used to run the application.

View File

@ -32,6 +32,7 @@ android {
"src/main/res/ui/about",
"src/main/res/ui/backup",
"src/main/res/ui/common",
"src/main/res/ui/export_data",
"src/main/res/ui/history",
"src/main/res/ui/home",
"src/main/res/ui/onboarding",

View File

@ -13,6 +13,18 @@
<activity
android:name="co.electriccoin.zcash.ui.common.UiTestingActivity"
android:exported="false" />
<!-- This copies main/AndroidManifest -->
<provider
android:name="co.electriccoin.zcash.ui.screen.exportdata.util.TestShareFileProvider"
android:authorities="co.electriccoin.zcash.provider_test"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/share_file_provider_paths" />
</provider>
</application>
</manifest>

View File

@ -1,5 +1,6 @@
package co.electriccoin.zcash.ui.fixture
import android.content.Context
import cash.z.ecc.android.sdk.CloseableSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
@ -151,6 +152,10 @@ internal class MockSynchronizer : CloseableSynchronizer {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
}
override suspend fun getExistingDataDbFilePath(context: Context, network: ZcashNetwork, alias: String): String {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
}
companion object {
fun new() = MockSynchronizer()
}

View File

@ -0,0 +1,35 @@
package co.electriccoin.zcash.ui.screen.exportdata.util
import android.content.Intent
import androidx.test.filters.SmallTest
import co.electriccoin.zcash.ui.test.getAppContext
import org.junit.Assert.assertEquals
import org.junit.Test
import kotlin.io.path.pathString
import kotlin.test.Ignore
import kotlin.test.assertContains
class FileShareUtilTest {
// TODO [#1034]: Finish disabled FileShareUtilTest
// TODO [#1034]: https://github.com/zcash/secant-android-wallet/issues/1034
@Ignore("Temporary file permission is not correctly set")
@Test
@SmallTest
fun check_intent_for_private_data_file_sharing() {
val tempFilePath = kotlin.io.path.createTempFile(
directory = getAppContext().cacheDir.toPath(),
suffix = ".sqlite3"
)
val intent = FileShareUtil.newShareContentIntent(
getAppContext(),
tempFilePath.pathString
)
assertEquals(intent.action, Intent.ACTION_VIEW)
assertEquals(
FileShareUtil.SHARE_OUTSIDE_THE_APP_FLAGS or FileShareUtil.SHARE_CONTENT_PERMISSION_FLAGS,
intent.flags
)
assertContains(FileShareUtil.ZASHI_INTERNAL_DATA_AUTHORITY, intent.data.toString())
}
}

View File

@ -0,0 +1,9 @@
package co.electriccoin.zcash.ui.screen.exportdata.util
import androidx.core.content.FileProvider
import co.electriccoin.zcash.ui.R
/**
* Internal content provider for the private data database file.
*/
internal class TestShareFileProvider : FileProvider(R.xml.share_file_provider_paths)

View File

@ -0,0 +1,132 @@
package co.electriccoin.zcash.ui.screen.exportdata.view
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
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.compose.ui.test.performScrollTo
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Rule
import kotlin.test.Test
import kotlin.test.assertEquals
class ExportPrivateDataViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
@Test
@MediumTest
fun default_ui_state_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnBackCount())
assertEquals(false, testSetup.getOnAgree())
assertEquals(0, testSetup.getOnConfirmCount())
composeTestRule.onNodeWithTag(ExportPrivateDataScreenTag.AGREE_CHECKBOX_TAG).also {
it.performScrollTo()
it.assertExists()
it.assertIsDisplayed()
it.assertHasClickAction()
it.assertIsEnabled()
}
composeTestRule.onNodeWithText(getStringResource(R.string.export_data_confirm), ignoreCase = true).also {
it.performScrollTo()
it.assertExists()
it.assertIsDisplayed()
it.assertHasClickAction()
it.assertIsNotEnabled()
}
composeTestRule.onNodeWithTag(ExportPrivateDataScreenTag.WARNING_TEXT_TAG).also {
it.performScrollTo()
it.assertExists()
it.assertIsDisplayed()
}
composeTestRule.onNodeWithTag(ExportPrivateDataScreenTag.ADDITIONAL_TEXT_TAG).also {
it.performScrollTo()
it.assertExists()
it.assertIsDisplayed()
}
}
@Test
@MediumTest
fun back_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.clickBack()
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun click_disabled_confirm_button_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnConfirmCount())
assertEquals(false, testSetup.getOnAgree())
composeTestRule.clickConfirm()
assertEquals(0, testSetup.getOnConfirmCount())
assertEquals(false, testSetup.getOnAgree())
}
@Test
@MediumTest
fun click_enabled_confirm_button_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnConfirmCount())
assertEquals(false, testSetup.getOnAgree())
composeTestRule.clickAgree()
assertEquals(0, testSetup.getOnConfirmCount())
assertEquals(true, testSetup.getOnAgree())
composeTestRule.clickConfirm()
assertEquals(1, testSetup.getOnConfirmCount())
assertEquals(true, testSetup.getOnAgree())
}
private fun newTestSetup() = ExportPrivateDataViewTestSetup(composeTestRule).apply {
setDefaultContent()
}
}
private fun ComposeContentTestRule.clickBack() {
onNodeWithContentDescription(getStringResource(R.string.support_back_content_description)).also {
it.performClick()
}
}
private fun ComposeContentTestRule.clickConfirm() {
onNodeWithText(getStringResource(R.string.export_data_confirm), ignoreCase = true).also {
it.performScrollTo()
it.performClick()
}
}
private fun ComposeContentTestRule.clickAgree() {
onNodeWithText(getStringResource(R.string.export_data_agree)).also {
it.performScrollTo()
it.performClick()
}
}

View File

@ -0,0 +1,57 @@
package co.electriccoin.zcash.ui.screen.exportdata.view
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
class ExportPrivateDataViewTestSetup(private val composeTestRule: ComposeContentTestRule) {
private val onBackCount = AtomicInteger(0)
private val onAgree = AtomicBoolean(false)
private val onConfirmCount = AtomicInteger(0)
fun getOnBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
}
fun getOnAgree(): Boolean {
composeTestRule.waitForIdle()
return onAgree.get()
}
fun getOnConfirmCount(): Int {
composeTestRule.waitForIdle()
return onConfirmCount.get()
}
@Composable
@Suppress("TestFunctionName")
fun DefaultContent() {
ExportPrivateData(
SnackbarHostState(),
onBack = {
onBackCount.incrementAndGet()
},
onAgree = {
onAgree.getAndSet(it)
},
onConfirm = {
onConfirmCount.incrementAndGet()
}
)
}
fun setDefaultContent() {
composeTestRule.setContent {
ZcashTheme {
DefaultContent()
}
}
}
}

View File

@ -15,6 +15,7 @@ class SettingsViewTestSetup(
private val onDocumentationCount = AtomicInteger(0)
private val onPrivacyPolicyCount = AtomicInteger(0)
private val onFeedbackCount = AtomicInteger(0)
private val onExportPrivateData = AtomicInteger(0)
private val onAboutCount = AtomicInteger(0)
private val onRescanCount = AtomicInteger(0)
private val onBackgroundSyncChangedCount = AtomicInteger(0)
@ -46,6 +47,11 @@ class SettingsViewTestSetup(
return onFeedbackCount.get()
}
fun getExportPrivateDataCount(): Int {
composeTestRule.waitForIdle()
return onExportPrivateData.get()
}
fun getAboutCount(): Int {
composeTestRule.waitForIdle()
return onAboutCount.get()
@ -91,6 +97,9 @@ class SettingsViewTestSetup(
onFeedback = {
onFeedbackCount.incrementAndGet()
},
onExportPrivateData = {
onExportPrivateData.incrementAndGet()
},
onAbout = {
onAboutCount.incrementAndGet()
},

View File

@ -106,6 +106,23 @@ class SettingsViewTest : UiTestPrerequisites() {
assertEquals(1, testSetup.getPrivacyPolicyCount())
}
@Test
@MediumTest
fun on_export_private_data_test() {
val testSetup = SettingsViewTestSetup(composeTestRule, TroubleshootingParametersFixture.new())
assertEquals(0, testSetup.getExportPrivateDataCount())
composeTestRule.onNodeWithText(
text = getStringResource(R.string.settings_export_private_data),
ignoreCase = true
).also {
it.performClick()
}
assertEquals(1, testSetup.getExportPrivateDataCount())
}
@Test
@MediumTest
fun on_about_test() {

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="test_database" path="." />
</paths>

View File

@ -3,7 +3,9 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<uses-feature
android:name="android.hardware.camera.any"
android:required="false" />
<application
android:icon="@mipmap/ic_launcher_square"
@ -14,8 +16,9 @@
android:name=".MainActivity"
android:exported="false"
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.App.Starting"/>
android:theme="@style/Theme.App.Starting"
android:windowSoftInputMode="adjustResize" />
</application>
</manifest>
</manifest>

View File

@ -12,6 +12,7 @@ import co.electriccoin.zcash.ui.NavigationArguments.SEND_AMOUNT
import co.electriccoin.zcash.ui.NavigationArguments.SEND_MEMO
import co.electriccoin.zcash.ui.NavigationArguments.SEND_RECIPIENT_ADDRESS
import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
import co.electriccoin.zcash.ui.NavigationTargets.EXPORT_PRIVATE_DATA
import co.electriccoin.zcash.ui.NavigationTargets.HISTORY
import co.electriccoin.zcash.ui.NavigationTargets.HOME
import co.electriccoin.zcash.ui.NavigationTargets.RECEIVE
@ -26,6 +27,7 @@ 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
import co.electriccoin.zcash.ui.screen.receive.WrapReceive
@ -50,13 +52,13 @@ internal fun MainActivity.Navigation() {
NavHost(navController = navController, startDestination = HOME) {
composable(HOME) {
WrapHome(
goAbout = { navController.navigateJustOnce(ABOUT) },
goHistory = { navController.navigateJustOnce(HISTORY) },
goReceive = { navController.navigateJustOnce(RECEIVE) },
goSeedPhrase = { navController.navigateJustOnce(SEED) },
goSend = { navController.navigateJustOnce(SEND) },
goSettings = { navController.navigateJustOnce(SETTINGS) },
goSupport = { navController.navigateJustOnce(SUPPORT) },
goAbout = { navController.navigateJustOnce(ABOUT) },
goReceive = { navController.navigateJustOnce(RECEIVE) },
goSend = { navController.navigateJustOnce(SEND) },
goHistory = { navController.navigateJustOnce(HISTORY) }
)
if (ConfigurationEntries.IS_APP_UPDATE_CHECK_ENABLED.getValue(RemoteConfig.current)) {
@ -72,11 +74,14 @@ internal fun MainActivity.Navigation() {
}
composable(SETTINGS) {
WrapSettings(
goAbout = {
navController.navigateJustOnce(ABOUT)
},
goBack = {
navController.popBackStackJustOnce(SETTINGS)
},
goAbout = {
navController.navigateJustOnce(ABOUT)
goExportPrivateData = {
navController.navigateJustOnce(EXPORT_PRIVATE_DATA)
}
)
}
@ -134,7 +139,12 @@ internal fun MainActivity.Navigation() {
goBack = { navController.popBackStackJustOnce(SCAN) }
)
}
composable(EXPORT_PRIVATE_DATA) {
WrapExportPrivateData(
goBack = { navController.popBackStackJustOnce(EXPORT_PRIVATE_DATA) },
onConfirm = { navController.popBackStackJustOnce(EXPORT_PRIVATE_DATA) }
)
}
composable(HISTORY) {
WrapHistory(goBack = { navController.navigateUp() })
}
@ -176,25 +186,16 @@ object NavigationArguments {
}
object NavigationTargets {
const val HOME = "home"
const val WALLET_ADDRESS_DETAILS = "wallet_address_details"
const val SETTINGS = "settings"
const val SEED = "seed"
const val RECEIVE = "receive"
const val REQUEST = "request"
const val HISTORY = "history"
const val SEND = "send"
const val SUPPORT = "support"
const val ABOUT = "about"
const val EXPORT_PRIVATE_DATA = "export_private_data"
const val HISTORY = "history"
const val HOME = "home"
const val RECEIVE = "receive"
const val REQUEST = "request"
const val SCAN = "scan"
const val SEED = "seed"
const val SEND = "send"
const val SETTINGS = "settings"
const val SUPPORT = "support"
const val WALLET_ADDRESS_DETAILS = "wallet_address_details"
}

View File

@ -0,0 +1,101 @@
package co.electriccoin.zcash.ui.screen.exportdata
import android.content.Context
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.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.screen.exportdata.util.FileShareUtil
import co.electriccoin.zcash.ui.screen.exportdata.view.ExportPrivateData
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
@Composable
internal fun MainActivity.WrapExportPrivateData(
goBack: () -> Unit,
onConfirm: () -> Unit
) {
WrapExportPrivateData(
this,
onBack = goBack,
onShare = onConfirm
)
}
@Composable
internal fun WrapExportPrivateData(
activity: ComponentActivity,
onBack: () -> Unit,
onShare: () -> Unit
) {
val walletViewModel by activity.viewModels<WalletViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
if (synchronizer == null) {
// Display loading indicator
} else {
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
ExportPrivateData(
snackbarHostState = snackbarHostState,
onBack = onBack,
onAgree = {
// Needed for UI testing only
},
onConfirm = {
scope.launch {
shareData(
context = activity.applicationContext,
synchronizer = synchronizer,
snackbarHostState = snackbarHostState,
).collect { shareResult ->
if (shareResult) {
onShare()
}
}
}
}
)
}
}
fun shareData(
context: Context,
synchronizer: Synchronizer,
snackbarHostState: SnackbarHostState,
): Flow<Boolean> = callbackFlow {
val shareIntent = FileShareUtil.newShareContentIntent(
context,
// Example of the expected db file absolute path:
// /data/user/0/co.electriccoin.zcash.debug/no_backup/co.electricoin.zcash/zcash_sdk_mainnet_data.sqlite3
(synchronizer as SdkSynchronizer).getExistingDataDbFilePath(
context = context,
network = ZcashNetwork.fromResources(context)
)
)
runCatching {
context.startActivity(shareIntent)
trySend(true)
}.onFailure {
snackbarHostState.showSnackbar(
message = context.getString(R.string.export_data_unable_to_share)
)
trySend(false)
}
awaitClose {
// No resources to release
}
}

View File

@ -0,0 +1,54 @@
package co.electriccoin.zcash.ui.screen.exportdata.util
import android.content.Context
import android.content.Intent
import androidx.core.content.FileProvider
import co.electriccoin.zcash.ui.R
import java.io.File
object FileShareUtil {
const val SHARE_OUTSIDE_THE_APP_FLAGS = Intent.FLAG_ACTIVITY_NEW_TASK
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_INTERNAL_DATA_AUTHORITY = "co.electriccoin.zcash.provider" // NON-NLS
/**
* Returns a new share internal app data intent with necessary permission granted exclusively to the data file.
*
* @param dataFilePath The private data file path we want to share
*
* @return Intent for launching an app for sharing
*/
internal fun newShareContentIntent(
context: Context,
dataFilePath: String
): Intent {
val fileUri = FileProvider.getUriForFile(
context,
ZASHI_INTERNAL_DATA_AUTHORITY,
File(dataFilePath)
)
val dataIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, fileUri)
type = ZASHI_INTERNAL_DATA_MIME_TYPE
}
val shareDataIntent = Intent.createChooser(
dataIntent,
context.getString(R.string.export_data_export_data_chooser_title)
).apply {
addFlags(
SHARE_CONTENT_PERMISSION_FLAGS or
SHARE_OUTSIDE_THE_APP_FLAGS
)
}
return shareDataIntent
}
}

View File

@ -0,0 +1,9 @@
package co.electriccoin.zcash.ui.screen.exportdata.util
import androidx.core.content.FileProvider
import co.electriccoin.zcash.ui.R
/**
* Internal content provider for the private data database file.
*/
internal class ShareFileProvider : FileProvider(R.xml.share_file_provider_paths)

View File

@ -0,0 +1,10 @@
package co.electriccoin.zcash.ui.screen.exportdata.view
/**
* These are only used for automated testing.
*/
object ExportPrivateDataScreenTag {
const val AGREE_CHECKBOX_TAG = "agree_checkbox"
const val WARNING_TEXT_TAG = "warning_text"
const val ADDITIONAL_TEXT_TAG = "additional_text"
}

View File

@ -0,0 +1,161 @@
package co.electriccoin.zcash.ui.screen.exportdata.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
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.CheckBox
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@Preview("Export Private Data")
@Composable
private fun ExportPrivateDataPreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
ExportPrivateData(
snackbarHostState = SnackbarHostState(),
onBack = {},
onAgree = {},
onConfirm = {},
)
}
}
}
// TODO [#998]: Check and enhance screen dark mode
// TODO [#998]: https://github.com/zcash/secant-android-wallet/issues/998
@Composable
fun ExportPrivateData(
snackbarHostState: SnackbarHostState,
onBack: () -> Unit,
onAgree: (Boolean) -> Unit,
onConfirm: () -> Unit,
) {
Scaffold(
topBar = { ExportPrivateDataTopAppBar(onBack = onBack) },
snackbarHost = { SnackbarHost(snackbarHostState) },
) { paddingValues ->
ExportPrivateDataContent(
onAgree = onAgree,
onConfirm = onConfirm,
modifier = Modifier
.fillMaxSize()
.padding(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding(),
start = ZcashTheme.dimens.spacingHuge,
end = ZcashTheme.dimens.spacingHuge
)
.verticalScroll(rememberScrollState())
)
}
}
@Composable
private fun ExportPrivateDataTopAppBar(
onBack: () -> Unit,
) {
SmallTopAppBar(
backText = stringResource(R.string.export_data_back).uppercase(),
backContentDescriptionText = stringResource(R.string.export_data_back_content_description),
onBack = onBack,
)
}
@Composable
private fun ExportPrivateDataContent(
onAgree: (Boolean) -> Unit,
onConfirm: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painterResource(id = R.drawable.zashi_logo_without_text),
stringResource(R.string.zcash_logo_content_description),
Modifier.fillMaxWidth()
)
Spacer(Modifier.height(ZcashTheme.dimens.spacingXlarge))
Text(
text = stringResource(R.string.export_data_header),
style = ZcashTheme.typography.secondary.headlineMedium,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault))
Body(
modifier = Modifier.testTag(ExportPrivateDataScreenTag.WARNING_TEXT_TAG),
text = stringResource(R.string.export_data_text_1)
)
Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault))
Text(
modifier = Modifier.testTag(ExportPrivateDataScreenTag.ADDITIONAL_TEXT_TAG),
text = stringResource(R.string.export_data_text_2),
fontSize = 14.sp
)
Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault))
val checkedState = rememberSaveable { mutableStateOf(false) }
CheckBox(
modifier = Modifier
.align(Alignment.Start)
.fillMaxWidth(),
checked = checkedState.value,
onCheckedChange = {
checkedState.value = it
onAgree(it)
},
text = stringResource(R.string.export_data_agree),
checkBoxTestTag = ExportPrivateDataScreenTag.AGREE_CHECKBOX_TAG
)
Spacer(
modifier = Modifier
.fillMaxHeight()
.weight(MINIMAL_WEIGHT)
)
PrimaryButton(
onClick = onConfirm,
text = stringResource(R.string.export_data_confirm).uppercase(),
enabled = checkedState.value
)
Spacer(Modifier.height(ZcashTheme.dimens.spacingXlarge))
}
}

View File

@ -9,7 +9,7 @@ object WebBrowserUtil {
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_MULTIPLE_TASK
const val ZCASH_PRIVACY_POLICY_URI = "https://z.cash/privacy-policy/"
const val ZCASH_PRIVACY_POLICY_URI = "https://z.cash/privacy-policy/" // NON-NLS
/**
* Returns new action view app intent. We assume the a web browser app is installed.

View File

@ -17,13 +17,15 @@ import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
@Composable
internal fun MainActivity.WrapSettings(
goBack: () -> Unit,
goAbout: () -> Unit,
goBack: () -> Unit,
goExportPrivateData: () -> Unit,
) {
WrapSettings(
activity = this,
goAbout = goAbout,
goBack = goBack,
goAbout = goAbout
goExportPrivateData = goExportPrivateData
)
}
@ -32,6 +34,7 @@ private fun WrapSettings(
activity: ComponentActivity,
goBack: () -> Unit,
goAbout: () -> Unit,
goExportPrivateData: () -> Unit,
) {
val walletViewModel by activity.viewModels<WalletViewModel>()
val settingsViewModel by activity.viewModels<SettingsViewModel>()
@ -65,6 +68,7 @@ private fun WrapSettings(
onPrivacyPolicy = {},
onFeedback = {},
onAbout = goAbout,
onExportPrivateData = goExportPrivateData,
onRescanWallet = {
walletViewModel.rescanBlockchain()
},

View File

@ -57,6 +57,7 @@ private fun PreviewSettings() {
onDocumentation = {},
onPrivacyPolicy = {},
onFeedback = {},
onExportPrivateData = {},
onAbout = {},
onRescanWallet = {},
onBackgroundSyncSettingsChanged = {},
@ -76,6 +77,7 @@ fun Settings(
onDocumentation: () -> Unit,
onPrivacyPolicy: () -> Unit,
onFeedback: () -> Unit,
onExportPrivateData: () -> Unit,
onAbout: () -> Unit,
onRescanWallet: () -> Unit,
onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
@ -107,6 +109,7 @@ fun Settings(
onDocumentation = onDocumentation,
onPrivacyPolicy = onPrivacyPolicy,
onFeedback = onFeedback,
onExportPrivateData = onExportPrivateData,
onAbout = onAbout,
)
}
@ -224,12 +227,13 @@ private fun TroubleshootingMenu(
}
@Composable
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LongMethod")
private fun SettingsMainContent(
onBackup: () -> Unit,
onDocumentation: () -> Unit,
onPrivacyPolicy: () -> Unit,
onFeedback: () -> Unit,
onExportPrivateData: () -> Unit,
onAbout: () -> Unit,
modifier: Modifier = Modifier
) {
@ -284,6 +288,17 @@ private fun SettingsMainContent(
Spacer(modifier = Modifier.height(dimens.spacingDefault))
PrimaryButton(
onClick = onExportPrivateData,
text = stringResource(R.string.settings_export_private_data),
outerPaddingValues = PaddingValues(
horizontal = dimens.spacingNone,
vertical = dimens.spacingSmall
),
)
Spacer(modifier = Modifier.height(dimens.spacingDefault))
Spacer(
modifier = Modifier
.fillMaxHeight()

View File

@ -1,4 +1,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="fiat_currency_conversion_rate_unavailable">Unavailable</string>
<string name="empty_char">-</string>
<string name="zcash_logo_content_description">Zcash logo</string>
</resources>

View File

@ -0,0 +1,16 @@
<resources>
<string name="export_data_header">Consent for Exporting Private Data</string>
<string name="export_data_text_1">By clicking \"I Agree\" below, you give your consent to export Zashis private
data which includes the entire history of the wallet, all private information, memos, amounts and recipient
addresses, even for your shielded activity.*\n\nThis private data also gives the ability to see certain future
actions you take with Zashi.\n\nSharing this private data is irrevocable — once you have shared this private
data with someone, there is no way to revoke their access.</string>
<string name="export_data_text_2">*Note that this private data does not give them the ability to spend your
funds, only the ability to see what you do with your funds.</string>
<string name="export_data_confirm">Export private data</string>
<string name="export_data_agree">I agree</string>
<string name="export_data_back">Back</string>
<string name="export_data_back_content_description">Back</string>
<string name="export_data_export_data_chooser_title">Share internal Zashi data with:</string>
<string name="export_data_unable_to_share">Unable to find an application to share with.</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<!-- Android Studio complains about root-path. Search for an alternative way of approaching no_backup folder -->
<root-path name="root" path="/data/data/co.electriccoin.zcash.debug/no_backup/co.electricoin.zcash/." />
<root-path name="root" path="/data/data/co.electriccoin.zcash/no_backup/co.electricoin.zcash/." />
</paths>

View File

@ -1,6 +0,0 @@
<resources>
<string name="new_wallet_header">My secret phrase</string>
<string name="new_wallet_body">These words represent your funds and the security used to protect them.\n\nBack them up now!.</string>
<string name="new_wallet_button_finished">I wrote it down</string>
<string name="new_wallet_button_copy">Copy to buffer</string>
</resources>

View File

@ -12,7 +12,6 @@
<string name="security_warning_confirm">confirm</string>
<string name="security_warning_acknowledge">I acknowledge</string>
<string name="security_warning_back_content_description">Back</string>
<string name="zcash_logo_content_description">Zcash logo</string>
<string name="security_warning_header">Security warning:</string>
<string name="security_warning_back">back</string>
<string name="security_warning_unable_to_web_browser">Unable to find a web browser app.</string>

View File

@ -8,10 +8,11 @@
<string name="settings_troubleshooting_enable_keep_screen_on">Keep screen on during sync</string>
<string name="settings_troubleshooting_enable_analytics">Report crashes</string>
<string name="settings_backup_wallet">Backup wallet</string>
<string name="settings_backup_wallet">Recovery phrase</string>
<string name="settings_documentation">Documentation</string>
<string name="settings_privacy_policy">Privacy policy</string>
<string name="settings_send_us_feedback">Send us feedback</string>
<string name="settings_export_private_data">Export private data</string>
<string name="settings_about">About</string>
</resources>