[#312] Scan QR Screen Scaffold
* [#312] [#309] Scaffold Scan QR Screen - Screen scaffolding. - Model classes. - Screen states handling. - Needed dependencies added. - Camera permission handling. Redirect to Settings. - Added SettingsUtilTest class. - Added view classes tests. - Renamed tag class in update package. - Fix the scan frame size while changing the screen orientation. - Suppress Lint warning Co-authored-by: Carter Jernigan <git@carterjernigan.com>
This commit is contained in:
parent
a8be47bf89
commit
caf58f963a
|
@ -81,9 +81,11 @@ PLAY_PUBLISHER_PLUGIN_VERSION=3.7.0
|
||||||
ANDROIDX_ACTIVITY_VERSION=1.4.0
|
ANDROIDX_ACTIVITY_VERSION=1.4.0
|
||||||
ANDROIDX_ANNOTATION_VERSION=1.3.0
|
ANDROIDX_ANNOTATION_VERSION=1.3.0
|
||||||
ANDROIDX_APPCOMPAT_VERSION=1.4.1
|
ANDROIDX_APPCOMPAT_VERSION=1.4.1
|
||||||
|
ANDROIDX_CAMERA_VERSION=1.1.0-beta03
|
||||||
ANDROIDX_COMPOSE_COMPILER_VERSION=1.2.0-alpha08
|
ANDROIDX_COMPOSE_COMPILER_VERSION=1.2.0-alpha08
|
||||||
ANDROIDX_COMPOSE_MATERIAL3_VERSION=1.0.0-alpha09
|
ANDROIDX_COMPOSE_MATERIAL3_VERSION=1.0.0-alpha09
|
||||||
ANDROIDX_COMPOSE_VERSION=1.1.1
|
ANDROIDX_COMPOSE_VERSION=1.1.1
|
||||||
|
ANDROIDX_CONSTRAINTLAYOUT_VERSION=1.0.0
|
||||||
ANDROIDX_CORE_VERSION=1.7.0
|
ANDROIDX_CORE_VERSION=1.7.0
|
||||||
ANDROIDX_ESPRESSO_VERSION=3.4.0
|
ANDROIDX_ESPRESSO_VERSION=3.4.0
|
||||||
ANDROIDX_LIFECYCLE_VERSION=2.4.1
|
ANDROIDX_LIFECYCLE_VERSION=2.4.1
|
||||||
|
|
|
@ -114,9 +114,11 @@ dependencyResolutionManagement {
|
||||||
val androidxActivityVersion = extra["ANDROIDX_ACTIVITY_VERSION"].toString()
|
val androidxActivityVersion = extra["ANDROIDX_ACTIVITY_VERSION"].toString()
|
||||||
val androidxAnnotationVersion = extra["ANDROIDX_ANNOTATION_VERSION"].toString()
|
val androidxAnnotationVersion = extra["ANDROIDX_ANNOTATION_VERSION"].toString()
|
||||||
val androidxAppcompatVersion = extra["ANDROIDX_APPCOMPAT_VERSION"].toString()
|
val androidxAppcompatVersion = extra["ANDROIDX_APPCOMPAT_VERSION"].toString()
|
||||||
|
val androidxCameraVersion = extra["ANDROIDX_CAMERA_VERSION"].toString()
|
||||||
val androidxComposeCompilerVersion = extra["ANDROIDX_COMPOSE_COMPILER_VERSION"].toString()
|
val androidxComposeCompilerVersion = extra["ANDROIDX_COMPOSE_COMPILER_VERSION"].toString()
|
||||||
val androidxComposeMaterial3Version = extra["ANDROIDX_COMPOSE_MATERIAL3_VERSION"].toString()
|
val androidxComposeMaterial3Version = extra["ANDROIDX_COMPOSE_MATERIAL3_VERSION"].toString()
|
||||||
val androidxComposeVersion = extra["ANDROIDX_COMPOSE_VERSION"].toString()
|
val androidxComposeVersion = extra["ANDROIDX_COMPOSE_VERSION"].toString()
|
||||||
|
val androidxConstraintlayoutVersion = extra["ANDROIDX_CONSTRAINTLAYOUT_VERSION"].toString()
|
||||||
val androidxCoreVersion = extra["ANDROIDX_CORE_VERSION"].toString()
|
val androidxCoreVersion = extra["ANDROIDX_CORE_VERSION"].toString()
|
||||||
val androidxEspressoVersion = extra["ANDROIDX_ESPRESSO_VERSION"].toString()
|
val androidxEspressoVersion = extra["ANDROIDX_ESPRESSO_VERSION"].toString()
|
||||||
val androidxLifecycleVersion = extra["ANDROIDX_LIFECYCLE_VERSION"].toString()
|
val androidxLifecycleVersion = extra["ANDROIDX_LIFECYCLE_VERSION"].toString()
|
||||||
|
@ -152,6 +154,9 @@ dependencyResolutionManagement {
|
||||||
library("androidx-activity-compose", "androidx.activity:activity-compose:$androidxActivityVersion")
|
library("androidx-activity-compose", "androidx.activity:activity-compose:$androidxActivityVersion")
|
||||||
library("androidx-annotation", "androidx.annotation:annotation:$androidxAnnotationVersion")
|
library("androidx-annotation", "androidx.annotation:annotation:$androidxAnnotationVersion")
|
||||||
library("androidx-appcompat", "androidx.appcompat:appcompat:$androidxAppcompatVersion")
|
library("androidx-appcompat", "androidx.appcompat:appcompat:$androidxAppcompatVersion")
|
||||||
|
library("androidx-camera", "androidx.camera:camera-camera2:$androidxCameraVersion")
|
||||||
|
library("androidx-camera-lifecycle", "androidx.camera:camera-lifecycle:$androidxCameraVersion")
|
||||||
|
library("androidx-camera-view", "androidx.camera:camera-view:$androidxCameraVersion")
|
||||||
library("androidx-compose-foundation", "androidx.compose.foundation:foundation:$androidxComposeVersion")
|
library("androidx-compose-foundation", "androidx.compose.foundation:foundation:$androidxComposeVersion")
|
||||||
library("androidx-compose-material3", "androidx.compose.material3:material3:$androidxComposeMaterial3Version")
|
library("androidx-compose-material3", "androidx.compose.material3:material3:$androidxComposeMaterial3Version")
|
||||||
library("androidx-compose-material-icons-core", "androidx.compose.material:material-icons-core:$androidxComposeVersion")
|
library("androidx-compose-material-icons-core", "androidx.compose.material:material-icons-core:$androidxComposeVersion")
|
||||||
|
@ -160,6 +165,7 @@ dependencyResolutionManagement {
|
||||||
library("androidx-compose-ui", "androidx.compose.ui:ui:$androidxComposeVersion")
|
library("androidx-compose-ui", "androidx.compose.ui:ui:$androidxComposeVersion")
|
||||||
library("androidx-compose-compiler", "androidx.compose.compiler:compiler:$androidxComposeCompilerVersion")
|
library("androidx-compose-compiler", "androidx.compose.compiler:compiler:$androidxComposeCompilerVersion")
|
||||||
library("androidx-core", "androidx.core:core-ktx:$androidxCoreVersion")
|
library("androidx-core", "androidx.core:core-ktx:$androidxCoreVersion")
|
||||||
|
library("androidx-constraintlayout", "androidx.constraintlayout:constraintlayout-compose:$androidxConstraintlayoutVersion")
|
||||||
library("androidx-lifecycle-livedata", "androidx.lifecycle:lifecycle-livedata-ktx:$androidxLifecycleVersion")
|
library("androidx-lifecycle-livedata", "androidx.lifecycle:lifecycle-livedata-ktx:$androidxLifecycleVersion")
|
||||||
library("androidx-navigation-compose", "androidx.navigation:navigation-compose:$androidxNavigationComposeVersion")
|
library("androidx-navigation-compose", "androidx.navigation:navigation-compose:$androidxNavigationComposeVersion")
|
||||||
library("androidx-security-crypto", "androidx.security:security-crypto-ktx:$androidxSecurityCryptoVersion")
|
library("androidx-security-crypto", "androidx.security:security-crypto-ktx:$androidxSecurityCryptoVersion")
|
||||||
|
@ -172,6 +178,7 @@ dependencyResolutionManagement {
|
||||||
library("kotlin-test", "org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
|
library("kotlin-test", "org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
|
||||||
library("kotlinx-coroutines-android", "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinxCoroutinesVersion")
|
library("kotlinx-coroutines-android", "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinxCoroutinesVersion")
|
||||||
library("kotlinx-coroutines-core", "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
|
library("kotlinx-coroutines-core", "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
|
||||||
|
library("kotlinx-coroutines-guava", "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$kotlinxCoroutinesVersion")
|
||||||
library("kotlinx-datetime", "org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion")
|
library("kotlinx-datetime", "org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion")
|
||||||
library("play-core", "com.google.android.play:core:$playCoreVersion")
|
library("play-core", "com.google.android.play:core:$playCoreVersion")
|
||||||
library("play-core-ktx", "com.google.android.play:core-ktx:$playCoreKtxVersion")
|
library("play-core-ktx", "com.google.android.play:core-ktx:$playCoreKtxVersion")
|
||||||
|
@ -195,6 +202,14 @@ dependencyResolutionManagement {
|
||||||
library("kotlinx-coroutines-test", "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion")
|
library("kotlinx-coroutines-test", "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion")
|
||||||
|
|
||||||
// Bundles
|
// Bundles
|
||||||
|
bundle(
|
||||||
|
"androidx-camera",
|
||||||
|
listOf(
|
||||||
|
"androidx-camera",
|
||||||
|
"androidx-camera-lifecycle",
|
||||||
|
"androidx-camera-view"
|
||||||
|
)
|
||||||
|
)
|
||||||
bundle(
|
bundle(
|
||||||
"androidx-compose-core",
|
"androidx-compose-core",
|
||||||
listOf(
|
listOf(
|
||||||
|
|
|
@ -34,6 +34,7 @@ android {
|
||||||
"src/main/res/ui/home",
|
"src/main/res/ui/home",
|
||||||
"src/main/res/ui/onboarding",
|
"src/main/res/ui/onboarding",
|
||||||
"src/main/res/ui/profile",
|
"src/main/res/ui/profile",
|
||||||
|
"src/main/res/ui/scan",
|
||||||
"src/main/res/ui/restore",
|
"src/main/res/ui/restore",
|
||||||
"src/main/res/ui/request",
|
"src/main/res/ui/request",
|
||||||
"src/main/res/ui/seed",
|
"src/main/res/ui/seed",
|
||||||
|
@ -54,16 +55,19 @@ dependencies {
|
||||||
implementation(libs.androidx.activity)
|
implementation(libs.androidx.activity)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
implementation(libs.androidx.annotation)
|
implementation(libs.androidx.annotation)
|
||||||
|
implementation(libs.androidx.constraintlayout)
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.lifecycle.livedata)
|
implementation(libs.androidx.lifecycle.livedata)
|
||||||
implementation(libs.androidx.splash)
|
implementation(libs.androidx.splash)
|
||||||
implementation(libs.androidx.workmanager)
|
implementation(libs.androidx.workmanager)
|
||||||
|
implementation(libs.bundles.androidx.camera)
|
||||||
implementation(libs.bundles.androidx.compose.core)
|
implementation(libs.bundles.androidx.compose.core)
|
||||||
implementation(libs.bundles.androidx.compose.extended)
|
implementation(libs.bundles.androidx.compose.extended)
|
||||||
implementation(libs.bundles.play.core)
|
implementation(libs.bundles.play.core)
|
||||||
implementation(libs.kotlin.stdlib)
|
implementation(libs.kotlin.stdlib)
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.kotlinx.coroutines.guava)
|
||||||
implementation(libs.kotlinx.datetime)
|
implementation(libs.kotlinx.datetime)
|
||||||
implementation(libs.zcash.sdk)
|
implementation(libs.zcash.sdk)
|
||||||
implementation(libs.zcash.bip39)
|
implementation(libs.zcash.bip39)
|
||||||
|
|
|
@ -14,6 +14,9 @@
|
||||||
<activity
|
<activity
|
||||||
android:name="co.electriccoin.zcash.ui.screen.update.TestUpdateActivity"
|
android:name="co.electriccoin.zcash.ui.screen.update.TestUpdateActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name="co.electriccoin.zcash.ui.screen.scan.TestScanActivity"
|
||||||
|
android:exported="false" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.scan
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||||
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
|
|
||||||
|
class TestScanActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setupUiContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupUiContent() {
|
||||||
|
setContent {
|
||||||
|
ZcashTheme {
|
||||||
|
GradientSurface(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
|
) {
|
||||||
|
WrapScan(
|
||||||
|
this,
|
||||||
|
goBack = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.scan.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.test.assertContains
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
|
||||||
|
class SettingsUtilTest {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val SETTINGS_URI = SettingsUtil.SETTINGS_URI_PREFIX +
|
||||||
|
ApplicationProvider.getApplicationContext<Context>().packageName
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SmallTest
|
||||||
|
fun check_intent_to_settings() {
|
||||||
|
val intent = SettingsUtil.newSettingsIntent(ApplicationProvider.getApplicationContext<Context>().packageName)
|
||||||
|
assertNotNull(intent)
|
||||||
|
assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, intent.action)
|
||||||
|
assertContains(intent.categories, Intent.CATEGORY_DEFAULT)
|
||||||
|
assertEquals(SettingsUtil.FLAGS, intent.flags)
|
||||||
|
assertEquals(SETTINGS_URI, intent.data.toString())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.scan.view
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import androidx.test.rule.GrantPermissionRule
|
||||||
|
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||||
|
import co.electriccoin.zcash.ui.R
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.TestScanActivity
|
||||||
|
import co.electriccoin.zcash.ui.test.getStringResource
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
// TODO [#313]: https://github.com/zcash/secant-android-wallet/issues/313
|
||||||
|
class ScanPermissionGrantedViewTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val composeTestRule = createAndroidComposeRule<TestScanActivity>()
|
||||||
|
|
||||||
|
// To automatically have CAMERA permission granted for all test in the class. Note, there is no
|
||||||
|
// way to revoke the granted permission after it's granted.
|
||||||
|
@get:Rule
|
||||||
|
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.CAMERA)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@MediumTest
|
||||||
|
fun back() {
|
||||||
|
val testSetup = newTestSetup()
|
||||||
|
|
||||||
|
assertEquals(0, testSetup.getOnBackCount())
|
||||||
|
|
||||||
|
composeTestRule.clickBack()
|
||||||
|
|
||||||
|
assertEquals(1, testSetup.getOnBackCount())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@MediumTest
|
||||||
|
fun check_all_ui_elements_displayed() {
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText(getStringResource(R.string.scan_header)).also {
|
||||||
|
it.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithTag(ScanTag.CAMERA_VIEW).also {
|
||||||
|
it.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithTag(ScanTag.QR_FRAME).also {
|
||||||
|
it.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithTag(ScanTag.TEXT_STATE).also {
|
||||||
|
it.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newTestSetup() = ScanViewTestSetup(composeTestRule).apply {
|
||||||
|
setDefaultContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ComposeContentTestRule.clickBack() {
|
||||||
|
onNodeWithContentDescription(getStringResource(R.string.scan_back_content_description)).also {
|
||||||
|
it.performClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.scan.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 co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
|
// TODO [#313]: https://github.com/zcash/secant-android-wallet/issues/313
|
||||||
|
class ScanViewTestSetup(
|
||||||
|
private val composeTestRule: ComposeContentTestRule
|
||||||
|
) {
|
||||||
|
private val onScanCount = AtomicInteger(0)
|
||||||
|
private val onOpenSettingsCount = AtomicInteger(0)
|
||||||
|
private val onBackCount = AtomicInteger(0)
|
||||||
|
private val scanState = AtomicReference(ScanState.Permission)
|
||||||
|
|
||||||
|
fun getOnScanCount(): Int {
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
return onScanCount.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOnOpenSettingsCount(): Int {
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
return onOpenSettingsCount.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOnBackCount(): Int {
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
return onBackCount.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getScanState(): ScanState {
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
return scanState.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun getDefaultContent() {
|
||||||
|
Scan(
|
||||||
|
snackbarHostState = SnackbarHostState(),
|
||||||
|
onBack = {
|
||||||
|
onBackCount.incrementAndGet()
|
||||||
|
},
|
||||||
|
onScan = {
|
||||||
|
onScanCount.incrementAndGet()
|
||||||
|
},
|
||||||
|
onOpenSettings = {
|
||||||
|
onOpenSettingsCount.incrementAndGet()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultContent() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
ZcashTheme {
|
||||||
|
getDefaultContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,10 +7,11 @@ import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.onRoot
|
import androidx.compose.ui.test.onRoot
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
|
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||||
import co.electriccoin.zcash.ui.R
|
import co.electriccoin.zcash.ui.R
|
||||||
import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker
|
import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker
|
||||||
import co.electriccoin.zcash.ui.screen.update.RestoreTag
|
|
||||||
import co.electriccoin.zcash.ui.screen.update.TestUpdateActivity
|
import co.electriccoin.zcash.ui.screen.update.TestUpdateActivity
|
||||||
|
import co.electriccoin.zcash.ui.screen.update.UpdateTag
|
||||||
import co.electriccoin.zcash.ui.screen.update.fixture.UpdateInfoFixture
|
import co.electriccoin.zcash.ui.screen.update.fixture.UpdateInfoFixture
|
||||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
|
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
|
||||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
||||||
|
@ -20,7 +21,7 @@ import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class UpdateViewTest {
|
class UpdateViewTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val composeTestRule = createAndroidComposeRule<TestUpdateActivity>()
|
val composeTestRule = createAndroidComposeRule<TestUpdateActivity>()
|
||||||
|
@ -122,7 +123,7 @@ class UpdateViewTest {
|
||||||
|
|
||||||
assertEquals(0, testSetup.getOnDownloadCount())
|
assertEquals(0, testSetup.getOnDownloadCount())
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(RestoreTag.PROGRESSBAR_DOWNLOADING).also {
|
composeTestRule.onNodeWithText(UpdateTag.PROGRESSBAR_DOWNLOADING).also {
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,13 +159,13 @@ class UpdateViewTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ComposeContentTestRule.clickLater() {
|
private fun ComposeContentTestRule.clickLater() {
|
||||||
onNodeWithTag(RestoreTag.BTN_LATER).also {
|
onNodeWithTag(UpdateTag.BTN_LATER).also {
|
||||||
it.performClick()
|
it.performClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ComposeContentTestRule.clickDownload() {
|
private fun ComposeContentTestRule.clickDownload() {
|
||||||
onNodeWithTag(RestoreTag.BTN_DOWNLOAD).also {
|
onNodeWithTag(UpdateTag.BTN_DOWNLOAD).also {
|
||||||
it.performClick()
|
it.performClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="co.electriccoin.zcash.ui">
|
package="co.electriccoin.zcash.ui">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:icon="@mipmap/ic_launcher_square"
|
android:icon="@mipmap/ic_launcher_square"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
|
|
@ -47,6 +47,7 @@ import co.electriccoin.zcash.ui.screen.request.view.Request
|
||||||
import co.electriccoin.zcash.ui.screen.restore.view.RestoreWallet
|
import co.electriccoin.zcash.ui.screen.restore.view.RestoreWallet
|
||||||
import co.electriccoin.zcash.ui.screen.restore.viewmodel.CompleteWordSetState
|
import co.electriccoin.zcash.ui.screen.restore.viewmodel.CompleteWordSetState
|
||||||
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
|
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.WrapScan
|
||||||
import co.electriccoin.zcash.ui.screen.seed.view.Seed
|
import co.electriccoin.zcash.ui.screen.seed.view.Seed
|
||||||
import co.electriccoin.zcash.ui.screen.send.view.Send
|
import co.electriccoin.zcash.ui.screen.send.view.Send
|
||||||
import co.electriccoin.zcash.ui.screen.settings.view.Settings
|
import co.electriccoin.zcash.ui.screen.settings.view.Settings
|
||||||
|
@ -253,7 +254,7 @@ class MainActivity : ComponentActivity() {
|
||||||
NavHost(navController = navController, startDestination = NAV_HOME) {
|
NavHost(navController = navController, startDestination = NAV_HOME) {
|
||||||
composable(NAV_HOME) {
|
composable(NAV_HOME) {
|
||||||
WrapHome(
|
WrapHome(
|
||||||
goScan = {},
|
goScan = { navController.navigate(NAV_SCAN) },
|
||||||
goProfile = { navController.navigate(NAV_PROFILE) },
|
goProfile = { navController.navigate(NAV_PROFILE) },
|
||||||
goSend = { navController.navigate(NAV_SEND) },
|
goSend = { navController.navigate(NAV_SEND) },
|
||||||
goRequest = { navController.navigate(NAV_REQUEST) }
|
goRequest = { navController.navigate(NAV_REQUEST) }
|
||||||
|
@ -307,6 +308,9 @@ class MainActivity : ComponentActivity() {
|
||||||
composable(NAV_ABOUT) {
|
composable(NAV_ABOUT) {
|
||||||
WrapAbout(goBack = { navController.popBackStack() })
|
WrapAbout(goBack = { navController.popBackStack() })
|
||||||
}
|
}
|
||||||
|
composable(NAV_SCAN) {
|
||||||
|
WrapScan(goBack = { navController.popBackStack() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -491,6 +495,9 @@ class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
const val NAV_ABOUT = "about"
|
const val NAV_ABOUT = "about"
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
const val NAV_SCAN = "scan"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.scan
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import co.electriccoin.zcash.ui.MainActivity
|
||||||
|
import co.electriccoin.zcash.ui.R
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.util.SettingsUtil
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.view.Scan
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun MainActivity.WrapScan(
|
||||||
|
goBack: () -> Unit
|
||||||
|
) {
|
||||||
|
WrapScan(this, goBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WrapScan(
|
||||||
|
activity: ComponentActivity,
|
||||||
|
goBack: () -> Unit
|
||||||
|
) {
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
Scan(
|
||||||
|
snackbarHostState,
|
||||||
|
onBack = goBack,
|
||||||
|
onScan = {},
|
||||||
|
onOpenSettings = {
|
||||||
|
runCatching {
|
||||||
|
activity.startActivity(SettingsUtil.newSettingsIntent(activity.packageName))
|
||||||
|
}.onFailure {
|
||||||
|
// This case should not really happen, as the Settings app should be available on every
|
||||||
|
// Android device, but we need to handle it somehow.
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = activity.getString(R.string.scan_settings_open_failed)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.scan
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are only used for automated testing.
|
||||||
|
*/
|
||||||
|
object ScanTag {
|
||||||
|
const val TEXT_STATE = "text_state"
|
||||||
|
const val CAMERA_VIEW = "camera_view"
|
||||||
|
const val QR_FRAME = "frame"
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.scan.model
|
||||||
|
|
||||||
|
enum class ScanState {
|
||||||
|
Failed,
|
||||||
|
Permission,
|
||||||
|
Scanning,
|
||||||
|
Success
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.scan.util
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
|
||||||
|
object SettingsUtil {
|
||||||
|
|
||||||
|
internal const val SETTINGS_URI_PREFIX = "package:"
|
||||||
|
|
||||||
|
internal const val FLAGS = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
|
Intent.FLAG_ACTIVITY_NO_HISTORY or
|
||||||
|
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an intent to the system Settings page of the app given by packageName parameter.
|
||||||
|
*
|
||||||
|
* @param packageName of the app, which should be opened in the Settings
|
||||||
|
*
|
||||||
|
* @return Intent for launching the system Settings app
|
||||||
|
*/
|
||||||
|
internal fun newSettingsIntent(
|
||||||
|
packageName: String
|
||||||
|
): Intent {
|
||||||
|
return Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
addCategory(Intent.CATEGORY_DEFAULT)
|
||||||
|
data = Uri.parse("$SETTINGS_URI_PREFIX$packageName")
|
||||||
|
flags = FLAGS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,333 @@
|
||||||
|
package co.electriccoin.zcash.ui.screen.scan.view
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.camera.core.CameraSelector
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.camera.view.PreviewView
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SmallTopAppBar
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.SnackbarResult
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.constraintlayout.compose.ConstraintLayout
|
||||||
|
import androidx.constraintlayout.compose.Dimension
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import co.electriccoin.zcash.ui.R
|
||||||
|
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||||
|
import co.electriccoin.zcash.ui.design.component.SecondaryButton
|
||||||
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.guava.await
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
// TODO [#423]: https://github.com/zcash/secant-android-wallet/issues/423
|
||||||
|
// TODO [#313]: https://github.com/zcash/secant-android-wallet/issues/313
|
||||||
|
@Preview("Scan")
|
||||||
|
@Composable
|
||||||
|
fun PreviewScan() {
|
||||||
|
ZcashTheme(darkTheme = true) {
|
||||||
|
GradientSurface {
|
||||||
|
Scan(
|
||||||
|
snackbarHostState = SnackbarHostState(),
|
||||||
|
onBack = {},
|
||||||
|
onScan = {},
|
||||||
|
onOpenSettings = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
@Composable
|
||||||
|
fun Scan(
|
||||||
|
snackbarHostState: SnackbarHostState,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onScan: (String) -> Unit,
|
||||||
|
onOpenSettings: () -> Unit,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = { ScanTopAppBar(onBack = onBack) },
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
|
) {
|
||||||
|
ScanMainContent(
|
||||||
|
onScan,
|
||||||
|
onOpenSettings,
|
||||||
|
onBack,
|
||||||
|
snackbarHostState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScanBottomItems(
|
||||||
|
scanState: ScanState,
|
||||||
|
onOpenSettings: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.scan_hint),
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(CenterHorizontally)
|
||||||
|
.padding(horizontal = 24.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = when (scanState) {
|
||||||
|
ScanState.Permission -> stringResource(id = R.string.scan_state_permission)
|
||||||
|
ScanState.Scanning -> stringResource(id = R.string.scan_state_scanning)
|
||||||
|
ScanState.Success -> stringResource(id = R.string.scan_state_success)
|
||||||
|
ScanState.Failed -> stringResource(id = R.string.scan_state_failed)
|
||||||
|
},
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(CenterHorizontally)
|
||||||
|
.padding(horizontal = 24.dp, vertical = 8.dp)
|
||||||
|
.testTag(ScanTag.TEXT_STATE)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (scanState == ScanState.Permission) {
|
||||||
|
SecondaryButton(
|
||||||
|
onClick = onOpenSettings,
|
||||||
|
text = stringResource(id = R.string.scan_settings_button),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 24.dp, vertical = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ScanTopAppBar(onBack: () -> Unit) {
|
||||||
|
SmallTopAppBar(
|
||||||
|
title = { Text(text = stringResource(id = R.string.scan_header)) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = onBack
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.scan_back_content_description)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED_VARIABLE", "UNUSED_PARAMETER", "MagicNumber", "LongMethod")
|
||||||
|
@Composable
|
||||||
|
private fun ScanMainContent(
|
||||||
|
onScan: (String) -> Unit,
|
||||||
|
onOpenSettings: () -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
snackbarHostState: SnackbarHostState
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
|
val (scanState, setScanState) = rememberSaveable {
|
||||||
|
mutableStateOf(
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
|
||||||
|
PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
ScanState.Scanning
|
||||||
|
} else {
|
||||||
|
ScanState.Permission
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val (scanResult, setScanResult) = rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
|
val launcher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.RequestPermission(),
|
||||||
|
onResult = { granted ->
|
||||||
|
if (granted) {
|
||||||
|
setScanState(ScanState.Scanning)
|
||||||
|
} else {
|
||||||
|
setScanState(ScanState.Permission)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// We use a random value to show the permission popup after a user grants the CAMERA permission
|
||||||
|
// outside of the app (in Settings).
|
||||||
|
LaunchedEffect(key1 = UUID.randomUUID()) {
|
||||||
|
launcher.launch(Manifest.permission.CAMERA)
|
||||||
|
}
|
||||||
|
|
||||||
|
val cameraProviderFlow = remember {
|
||||||
|
flow<ProcessCameraProvider> { emit(ProcessCameraProvider.getInstance(context).await()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// we calculate the best frame size for the current device screen
|
||||||
|
val framePossibleSize = remember { mutableStateOf(IntSize.Zero) }
|
||||||
|
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val frameActualSize = if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
|
(framePossibleSize.value.height * 0.85).roundToInt()
|
||||||
|
} else {
|
||||||
|
(framePossibleSize.value.width * 0.7).roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
ConstraintLayout(modifier = Modifier.fillMaxSize()) {
|
||||||
|
val (frame, bottomItems) = createRefs()
|
||||||
|
|
||||||
|
if (scanState == ScanState.Scanning) {
|
||||||
|
ScanCameraView(
|
||||||
|
onBack,
|
||||||
|
cameraProviderFlow,
|
||||||
|
lifecycleOwner,
|
||||||
|
snackbarHostState
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.constrainAs(frame) {
|
||||||
|
top.linkTo(parent.top)
|
||||||
|
bottom.linkTo(bottomItems.top)
|
||||||
|
start.linkTo(parent.start)
|
||||||
|
end.linkTo(parent.end)
|
||||||
|
width = Dimension.fillToConstraints
|
||||||
|
height = Dimension.fillToConstraints
|
||||||
|
}.onSizeChanged { coordinates ->
|
||||||
|
framePossibleSize.value = coordinates
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
ScanFrame(frameActualSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = Modifier.constrainAs(bottomItems) { bottom.linkTo(parent.bottom) }) {
|
||||||
|
ScanBottomItems(scanState, onOpenSettings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
@Composable
|
||||||
|
fun ScanFrame(frameSize: Int) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(with(LocalDensity.current) { frameSize.toDp() })
|
||||||
|
.background(Color.Transparent)
|
||||||
|
.border(BorderStroke(10.dp, Color.White), RoundedCornerShape(10))
|
||||||
|
.testTag(ScanTag.QR_FRAME)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScanCameraView(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
cameraProviderFlow: Flow<ProcessCameraProvider>,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
snackbarHostState: SnackbarHostState
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val cameraProvider = cameraProviderFlow.collectAsState(initial = null).value
|
||||||
|
if (null == cameraProvider) {
|
||||||
|
// Show loading indicator
|
||||||
|
} else {
|
||||||
|
AndroidView(
|
||||||
|
factory = { factoryContext ->
|
||||||
|
val previewView = PreviewView(factoryContext).apply {
|
||||||
|
this.scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||||
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val preview = androidx.camera.core.Preview.Builder().build()
|
||||||
|
val selector = CameraSelector.Builder()
|
||||||
|
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
||||||
|
.build()
|
||||||
|
preview.setSurfaceProvider(previewView.surfaceProvider)
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
// we must unbind the use-cases before rebinding them
|
||||||
|
cameraProvider.unbindAll()
|
||||||
|
cameraProvider.bindToLifecycle(
|
||||||
|
lifecycleOwner,
|
||||||
|
selector,
|
||||||
|
preview
|
||||||
|
)
|
||||||
|
}.onFailure {
|
||||||
|
scope.launch {
|
||||||
|
val snackbarResult = snackbarHostState.showSnackbar(
|
||||||
|
message = context.getString(R.string.scan_setup_failed),
|
||||||
|
actionLabel = context.getString(R.string.scan_setup_back),
|
||||||
|
)
|
||||||
|
if (snackbarResult == SnackbarResult.ActionPerformed) {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previewView
|
||||||
|
},
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.testTag(ScanTag.CAMERA_VIEW),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package co.electriccoin.zcash.ui.screen.support.model
|
package co.electriccoin.zcash.ui.screen.support.model
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||||
import co.electriccoin.zcash.spackle.AndroidApiVersion
|
import co.electriccoin.zcash.spackle.AndroidApiVersion
|
||||||
|
@ -22,6 +23,7 @@ class EnvironmentInfo(val locale: Locale, val monetarySeparators: MonetarySepara
|
||||||
currentLocaleLegacy(context)
|
currentLocaleLegacy(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
private fun currentLocaleNPlus(context: Context) = context.resources.configuration.locales[0]
|
private fun currentLocaleNPlus(context: Context) = context.resources.configuration.locales[0]
|
||||||
|
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
|
|
|
@ -3,7 +3,7 @@ package co.electriccoin.zcash.ui.screen.update
|
||||||
/**
|
/**
|
||||||
* These are only used for automated testing.
|
* These are only used for automated testing.
|
||||||
*/
|
*/
|
||||||
object RestoreTag {
|
object UpdateTag {
|
||||||
const val BTN_LATER = "btn_later"
|
const val BTN_LATER = "btn_later"
|
||||||
const val BTN_DOWNLOAD = "btn_download"
|
const val BTN_DOWNLOAD = "btn_download"
|
||||||
const val PROGRESSBAR_DOWNLOADING = "progressbar_downloading"
|
const val PROGRESSBAR_DOWNLOADING = "progressbar_downloading"
|
|
@ -31,7 +31,7 @@ import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
||||||
import co.electriccoin.zcash.ui.design.component.Reference
|
import co.electriccoin.zcash.ui.design.component.Reference
|
||||||
import co.electriccoin.zcash.ui.design.component.TertiaryButton
|
import co.electriccoin.zcash.ui.design.component.TertiaryButton
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.screen.update.RestoreTag
|
import co.electriccoin.zcash.ui.screen.update.UpdateTag
|
||||||
import co.electriccoin.zcash.ui.screen.update.fixture.UpdateInfoFixture
|
import co.electriccoin.zcash.ui.screen.update.fixture.UpdateInfoFixture
|
||||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
|
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
|
||||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
||||||
|
@ -96,7 +96,7 @@ fun UpdateOverlayRunning(updateInfo: UpdateInfo) {
|
||||||
.background(ZcashTheme.colors.overlay.copy(0.5f))
|
.background(ZcashTheme.colors.overlay.copy(0.5f))
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.testTag(RestoreTag.PROGRESSBAR_DOWNLOADING),
|
.testTag(UpdateTag.PROGRESSBAR_DOWNLOADING),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
|
@ -136,7 +136,7 @@ private fun UpdateBottomAppBar(
|
||||||
text = stringResource(R.string.update_download_button),
|
text = stringResource(R.string.update_download_button),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.testTag(RestoreTag.BTN_DOWNLOAD),
|
.testTag(UpdateTag.BTN_DOWNLOAD),
|
||||||
enabled = updateInfo.state != UpdateState.Running
|
enabled = updateInfo.state != UpdateState.Running
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ private fun UpdateBottomAppBar(
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.testTag(RestoreTag.BTN_LATER),
|
.testTag(UpdateTag.BTN_LATER),
|
||||||
enabled = !updateInfo.isForce && updateInfo.state != UpdateState.Running
|
enabled = !updateInfo.isForce && updateInfo.state != UpdateState.Running
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="scan_header">Scan a Zcash QR</string>
|
||||||
|
<string name="scan_back_content_description">Back</string>
|
||||||
|
|
||||||
|
<string name="scan_hint">We will validate any Zcash URI and take you to the appropriate action.</string>
|
||||||
|
|
||||||
|
<string name="scan_settings_button">Enable camera permission</string>
|
||||||
|
<string name="scan_settings_open_failed">Unable to launch Settings app.</string>
|
||||||
|
|
||||||
|
<string name="scan_setup_failed">Unable to run QR scanner.</string>
|
||||||
|
<string name="scan_setup_back">Back</string>
|
||||||
|
|
||||||
|
<string name="scan_state_permission">Permission for camera is necessary.</string>
|
||||||
|
<string name="scan_state_scanning">Scanning…</string>
|
||||||
|
<string name="scan_state_success">Successfully scanned.</string>
|
||||||
|
<string name="scan_state_failed">Scanning failed.</string>
|
||||||
|
</resources>
|
Loading…
Reference in New Issue