[#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:
parent
70d5721845
commit
494d068168
|
@ -125,9 +125,12 @@ android {
|
||||||
"proguard-project.txt"
|
"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")
|
val isSignReleaseBuildWithDebugKey = project.property("IS_SIGN_RELEASE_BUILD_WITH_DEBUG_KEY")
|
||||||
.toString().toBoolean()
|
.toString().toBoolean()
|
||||||
|
|
||||||
if (isReleaseSigningConfigured) {
|
if (isReleaseSigningConfigured) {
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
} else if (isSignReleaseBuildWithDebugKey) {
|
} else if (isSignReleaseBuildWithDebugKey) {
|
||||||
|
|
|
@ -28,6 +28,16 @@
|
||||||
android:shell="true"
|
android:shell="true"
|
||||||
tools:targetApi="29" />
|
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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -112,6 +112,7 @@ tasks {
|
||||||
"ZCASH_RELEASE_KEY_ALIAS_PASSWORD" to "",
|
"ZCASH_RELEASE_KEY_ALIAS_PASSWORD" to "",
|
||||||
|
|
||||||
"IS_SIGN_RELEASE_BUILD_WITH_DEBUG_KEY" to "false",
|
"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" to "",
|
||||||
"ZCASH_GOOGLE_PLAY_SERVICE_ACCOUNT_KEY" to "",
|
"ZCASH_GOOGLE_PLAY_SERVICE_ACCOUNT_KEY" to "",
|
||||||
|
|
|
@ -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
|
# be useful, for example, for running benchmark tests against a release build of the app signed with
|
||||||
# the default debug key configuration.
|
# the default debug key configuration.
|
||||||
IS_SIGN_RELEASE_BUILD_WITH_DEBUG_KEY=false
|
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
|
# Set the Google Play Service Account email address to enable deployment
|
||||||
# Note that this property is not currently used due to #1033
|
# 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
|
ZCASH_BIP39_VERSION=1.0.6
|
||||||
ZXING_VERSION=3.5.1
|
ZXING_VERSION=3.5.1
|
||||||
|
|
||||||
# Ensure a non-snapshot version is used before releasing to production.
|
# WARNING: Ensure a non-snapshot version is used before releasing to production.
|
||||||
ZCASH_SDK_VERSION=2.0.2
|
ZCASH_SDK_VERSION=2.0.2-SNAPSHOT
|
||||||
|
|
||||||
# Toolchain is the Java version used to build the application, which is separate from the
|
# Toolchain is the Java version used to build the application, which is separate from the
|
||||||
# Java version used to run the application.
|
# Java version used to run the application.
|
||||||
|
|
|
@ -32,6 +32,7 @@ android {
|
||||||
"src/main/res/ui/about",
|
"src/main/res/ui/about",
|
||||||
"src/main/res/ui/backup",
|
"src/main/res/ui/backup",
|
||||||
"src/main/res/ui/common",
|
"src/main/res/ui/common",
|
||||||
|
"src/main/res/ui/export_data",
|
||||||
"src/main/res/ui/history",
|
"src/main/res/ui/history",
|
||||||
"src/main/res/ui/home",
|
"src/main/res/ui/home",
|
||||||
"src/main/res/ui/onboarding",
|
"src/main/res/ui/onboarding",
|
||||||
|
|
|
@ -13,6 +13,18 @@
|
||||||
<activity
|
<activity
|
||||||
android:name="co.electriccoin.zcash.ui.common.UiTestingActivity"
|
android:name="co.electriccoin.zcash.ui.common.UiTestingActivity"
|
||||||
android:exported="false" />
|
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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package co.electriccoin.zcash.ui.fixture
|
package co.electriccoin.zcash.ui.fixture
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import cash.z.ecc.android.sdk.CloseableSynchronizer
|
import cash.z.ecc.android.sdk.CloseableSynchronizer
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
|
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.")
|
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 {
|
companion object {
|
||||||
fun new() = MockSynchronizer()
|
fun new() = MockSynchronizer()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ class SettingsViewTestSetup(
|
||||||
private val onDocumentationCount = AtomicInteger(0)
|
private val onDocumentationCount = AtomicInteger(0)
|
||||||
private val onPrivacyPolicyCount = AtomicInteger(0)
|
private val onPrivacyPolicyCount = AtomicInteger(0)
|
||||||
private val onFeedbackCount = AtomicInteger(0)
|
private val onFeedbackCount = AtomicInteger(0)
|
||||||
|
private val onExportPrivateData = AtomicInteger(0)
|
||||||
private val onAboutCount = AtomicInteger(0)
|
private val onAboutCount = AtomicInteger(0)
|
||||||
private val onRescanCount = AtomicInteger(0)
|
private val onRescanCount = AtomicInteger(0)
|
||||||
private val onBackgroundSyncChangedCount = AtomicInteger(0)
|
private val onBackgroundSyncChangedCount = AtomicInteger(0)
|
||||||
|
@ -46,6 +47,11 @@ class SettingsViewTestSetup(
|
||||||
return onFeedbackCount.get()
|
return onFeedbackCount.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getExportPrivateDataCount(): Int {
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
return onExportPrivateData.get()
|
||||||
|
}
|
||||||
|
|
||||||
fun getAboutCount(): Int {
|
fun getAboutCount(): Int {
|
||||||
composeTestRule.waitForIdle()
|
composeTestRule.waitForIdle()
|
||||||
return onAboutCount.get()
|
return onAboutCount.get()
|
||||||
|
@ -91,6 +97,9 @@ class SettingsViewTestSetup(
|
||||||
onFeedback = {
|
onFeedback = {
|
||||||
onFeedbackCount.incrementAndGet()
|
onFeedbackCount.incrementAndGet()
|
||||||
},
|
},
|
||||||
|
onExportPrivateData = {
|
||||||
|
onExportPrivateData.incrementAndGet()
|
||||||
|
},
|
||||||
onAbout = {
|
onAbout = {
|
||||||
onAboutCount.incrementAndGet()
|
onAboutCount.incrementAndGet()
|
||||||
},
|
},
|
||||||
|
|
|
@ -106,6 +106,23 @@ class SettingsViewTest : UiTestPrerequisites() {
|
||||||
assertEquals(1, testSetup.getPrivacyPolicyCount())
|
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
|
@Test
|
||||||
@MediumTest
|
@MediumTest
|
||||||
fun on_about_test() {
|
fun on_about_test() {
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<cache-path name="test_database" path="." />
|
||||||
|
</paths>
|
|
@ -3,7 +3,9 @@
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<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
|
<application
|
||||||
android:icon="@mipmap/ic_launcher_square"
|
android:icon="@mipmap/ic_launcher_square"
|
||||||
|
@ -14,8 +16,9 @@
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:theme="@style/Theme.App.Starting"
|
||||||
android:theme="@style/Theme.App.Starting"/>
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -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_MEMO
|
||||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_RECIPIENT_ADDRESS
|
import co.electriccoin.zcash.ui.NavigationArguments.SEND_RECIPIENT_ADDRESS
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
|
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.HISTORY
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.HOME
|
import co.electriccoin.zcash.ui.NavigationTargets.HOME
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.RECEIVE
|
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.configuration.RemoteConfig
|
||||||
import co.electriccoin.zcash.ui.screen.about.WrapAbout
|
import co.electriccoin.zcash.ui.screen.about.WrapAbout
|
||||||
import co.electriccoin.zcash.ui.screen.address.WrapWalletAddresses
|
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.history.WrapHistory
|
||||||
import co.electriccoin.zcash.ui.screen.home.WrapHome
|
import co.electriccoin.zcash.ui.screen.home.WrapHome
|
||||||
import co.electriccoin.zcash.ui.screen.receive.WrapReceive
|
import co.electriccoin.zcash.ui.screen.receive.WrapReceive
|
||||||
|
@ -50,13 +52,13 @@ internal fun MainActivity.Navigation() {
|
||||||
NavHost(navController = navController, startDestination = HOME) {
|
NavHost(navController = navController, startDestination = HOME) {
|
||||||
composable(HOME) {
|
composable(HOME) {
|
||||||
WrapHome(
|
WrapHome(
|
||||||
|
goAbout = { navController.navigateJustOnce(ABOUT) },
|
||||||
|
goHistory = { navController.navigateJustOnce(HISTORY) },
|
||||||
|
goReceive = { navController.navigateJustOnce(RECEIVE) },
|
||||||
goSeedPhrase = { navController.navigateJustOnce(SEED) },
|
goSeedPhrase = { navController.navigateJustOnce(SEED) },
|
||||||
|
goSend = { navController.navigateJustOnce(SEND) },
|
||||||
goSettings = { navController.navigateJustOnce(SETTINGS) },
|
goSettings = { navController.navigateJustOnce(SETTINGS) },
|
||||||
goSupport = { navController.navigateJustOnce(SUPPORT) },
|
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)) {
|
if (ConfigurationEntries.IS_APP_UPDATE_CHECK_ENABLED.getValue(RemoteConfig.current)) {
|
||||||
|
@ -72,11 +74,14 @@ internal fun MainActivity.Navigation() {
|
||||||
}
|
}
|
||||||
composable(SETTINGS) {
|
composable(SETTINGS) {
|
||||||
WrapSettings(
|
WrapSettings(
|
||||||
|
goAbout = {
|
||||||
|
navController.navigateJustOnce(ABOUT)
|
||||||
|
},
|
||||||
goBack = {
|
goBack = {
|
||||||
navController.popBackStackJustOnce(SETTINGS)
|
navController.popBackStackJustOnce(SETTINGS)
|
||||||
},
|
},
|
||||||
goAbout = {
|
goExportPrivateData = {
|
||||||
navController.navigateJustOnce(ABOUT)
|
navController.navigateJustOnce(EXPORT_PRIVATE_DATA)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -134,7 +139,12 @@ internal fun MainActivity.Navigation() {
|
||||||
goBack = { navController.popBackStackJustOnce(SCAN) }
|
goBack = { navController.popBackStackJustOnce(SCAN) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
composable(EXPORT_PRIVATE_DATA) {
|
||||||
|
WrapExportPrivateData(
|
||||||
|
goBack = { navController.popBackStackJustOnce(EXPORT_PRIVATE_DATA) },
|
||||||
|
onConfirm = { navController.popBackStackJustOnce(EXPORT_PRIVATE_DATA) }
|
||||||
|
)
|
||||||
|
}
|
||||||
composable(HISTORY) {
|
composable(HISTORY) {
|
||||||
WrapHistory(goBack = { navController.navigateUp() })
|
WrapHistory(goBack = { navController.navigateUp() })
|
||||||
}
|
}
|
||||||
|
@ -176,25 +186,16 @@ object NavigationArguments {
|
||||||
}
|
}
|
||||||
|
|
||||||
object NavigationTargets {
|
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 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 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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
@ -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"
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ object WebBrowserUtil {
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
Intent.FLAG_ACTIVITY_MULTIPLE_TASK
|
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.
|
* Returns new action view app intent. We assume the a web browser app is installed.
|
||||||
|
|
|
@ -17,13 +17,15 @@ import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MainActivity.WrapSettings(
|
internal fun MainActivity.WrapSettings(
|
||||||
goBack: () -> Unit,
|
|
||||||
goAbout: () -> Unit,
|
goAbout: () -> Unit,
|
||||||
|
goBack: () -> Unit,
|
||||||
|
goExportPrivateData: () -> Unit,
|
||||||
) {
|
) {
|
||||||
WrapSettings(
|
WrapSettings(
|
||||||
activity = this,
|
activity = this,
|
||||||
|
goAbout = goAbout,
|
||||||
goBack = goBack,
|
goBack = goBack,
|
||||||
goAbout = goAbout
|
goExportPrivateData = goExportPrivateData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +34,7 @@ private fun WrapSettings(
|
||||||
activity: ComponentActivity,
|
activity: ComponentActivity,
|
||||||
goBack: () -> Unit,
|
goBack: () -> Unit,
|
||||||
goAbout: () -> Unit,
|
goAbout: () -> Unit,
|
||||||
|
goExportPrivateData: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val walletViewModel by activity.viewModels<WalletViewModel>()
|
val walletViewModel by activity.viewModels<WalletViewModel>()
|
||||||
val settingsViewModel by activity.viewModels<SettingsViewModel>()
|
val settingsViewModel by activity.viewModels<SettingsViewModel>()
|
||||||
|
@ -65,6 +68,7 @@ private fun WrapSettings(
|
||||||
onPrivacyPolicy = {},
|
onPrivacyPolicy = {},
|
||||||
onFeedback = {},
|
onFeedback = {},
|
||||||
onAbout = goAbout,
|
onAbout = goAbout,
|
||||||
|
onExportPrivateData = goExportPrivateData,
|
||||||
onRescanWallet = {
|
onRescanWallet = {
|
||||||
walletViewModel.rescanBlockchain()
|
walletViewModel.rescanBlockchain()
|
||||||
},
|
},
|
||||||
|
|
|
@ -57,6 +57,7 @@ private fun PreviewSettings() {
|
||||||
onDocumentation = {},
|
onDocumentation = {},
|
||||||
onPrivacyPolicy = {},
|
onPrivacyPolicy = {},
|
||||||
onFeedback = {},
|
onFeedback = {},
|
||||||
|
onExportPrivateData = {},
|
||||||
onAbout = {},
|
onAbout = {},
|
||||||
onRescanWallet = {},
|
onRescanWallet = {},
|
||||||
onBackgroundSyncSettingsChanged = {},
|
onBackgroundSyncSettingsChanged = {},
|
||||||
|
@ -76,6 +77,7 @@ fun Settings(
|
||||||
onDocumentation: () -> Unit,
|
onDocumentation: () -> Unit,
|
||||||
onPrivacyPolicy: () -> Unit,
|
onPrivacyPolicy: () -> Unit,
|
||||||
onFeedback: () -> Unit,
|
onFeedback: () -> Unit,
|
||||||
|
onExportPrivateData: () -> Unit,
|
||||||
onAbout: () -> Unit,
|
onAbout: () -> Unit,
|
||||||
onRescanWallet: () -> Unit,
|
onRescanWallet: () -> Unit,
|
||||||
onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
|
onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
|
||||||
|
@ -107,6 +109,7 @@ fun Settings(
|
||||||
onDocumentation = onDocumentation,
|
onDocumentation = onDocumentation,
|
||||||
onPrivacyPolicy = onPrivacyPolicy,
|
onPrivacyPolicy = onPrivacyPolicy,
|
||||||
onFeedback = onFeedback,
|
onFeedback = onFeedback,
|
||||||
|
onExportPrivateData = onExportPrivateData,
|
||||||
onAbout = onAbout,
|
onAbout = onAbout,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -224,12 +227,13 @@ private fun TroubleshootingMenu(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList", "LongMethod")
|
||||||
private fun SettingsMainContent(
|
private fun SettingsMainContent(
|
||||||
onBackup: () -> Unit,
|
onBackup: () -> Unit,
|
||||||
onDocumentation: () -> Unit,
|
onDocumentation: () -> Unit,
|
||||||
onPrivacyPolicy: () -> Unit,
|
onPrivacyPolicy: () -> Unit,
|
||||||
onFeedback: () -> Unit,
|
onFeedback: () -> Unit,
|
||||||
|
onExportPrivateData: () -> Unit,
|
||||||
onAbout: () -> Unit,
|
onAbout: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
@ -284,6 +288,17 @@ private fun SettingsMainContent(
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(dimens.spacingDefault))
|
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(
|
Spacer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="fiat_currency_conversion_rate_unavailable">Unavailable</string>
|
<string name="fiat_currency_conversion_rate_unavailable">Unavailable</string>
|
||||||
<string name="empty_char">-</string>
|
<string name="empty_char">-</string>
|
||||||
|
<string name="zcash_logo_content_description">Zcash logo</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -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 Zashi’s 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>
|
|
@ -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>
|
|
@ -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>
|
|
|
@ -12,7 +12,6 @@
|
||||||
<string name="security_warning_confirm">confirm</string>
|
<string name="security_warning_confirm">confirm</string>
|
||||||
<string name="security_warning_acknowledge">I acknowledge</string>
|
<string name="security_warning_acknowledge">I acknowledge</string>
|
||||||
<string name="security_warning_back_content_description">Back</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_header">Security warning:</string>
|
||||||
<string name="security_warning_back">back</string>
|
<string name="security_warning_back">back</string>
|
||||||
<string name="security_warning_unable_to_web_browser">Unable to find a web browser app.</string>
|
<string name="security_warning_unable_to_web_browser">Unable to find a web browser app.</string>
|
||||||
|
|
|
@ -8,10 +8,11 @@
|
||||||
<string name="settings_troubleshooting_enable_keep_screen_on">Keep screen on during sync</string>
|
<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_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_documentation">Documentation</string>
|
||||||
<string name="settings_privacy_policy">Privacy policy</string>
|
<string name="settings_privacy_policy">Privacy policy</string>
|
||||||
<string name="settings_send_us_feedback">Send us feedback</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>
|
<string name="settings_about">About</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue