[#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"
|
||||
)
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 "",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 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()
|
||||
},
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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-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>
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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_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.
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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_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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue