[#269] Implement autoshield information prompt
The prompt should be displayed the first time the user visits the home screen after creating/restoring a wallet.
This commit is contained in:
parent
06a25e9e96
commit
1875c5f298
|
@ -0,0 +1,53 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="app:androidTest" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
|
||||
<module name="Zcash_Wallet.app" />
|
||||
<option name="TESTING_TYPE" value="0" />
|
||||
<option name="METHOD_NAME" value="" />
|
||||
<option name="CLASS_NAME" value="" />
|
||||
<option name="PACKAGE_NAME" value="" />
|
||||
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
|
||||
<option name="EXTRA_OPTIONS" value="" />
|
||||
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
|
||||
<option name="CLEAR_LOGCAT" value="false" />
|
||||
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
|
||||
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
|
||||
<option name="FORCE_STOP_RUNNING_APP" value="true" />
|
||||
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
||||
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="2147483645" />
|
||||
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="api-9130115880275692386-873230" />
|
||||
<option name="DEBUGGER_TYPE" value="Auto" />
|
||||
<Auto>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
</Auto>
|
||||
<Hybrid>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
</Hybrid>
|
||||
<Java />
|
||||
<Native>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
</Native>
|
||||
<Profilers>
|
||||
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
|
||||
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
|
||||
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
|
||||
</Profilers>
|
||||
<method v="2">
|
||||
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
|
@ -0,0 +1,23 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="dependencyUpdates" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="dependencyUpdates" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" value="" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
|
@ -20,8 +20,10 @@ android {
|
|||
targetSdkVersion Deps.targetSdkVersion
|
||||
versionCode = Deps.versionCode
|
||||
versionName = Deps.versionName
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
if (Boolean.parseBoolean(isUseTestOrchestrator)) {
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
multiDexEnabled true
|
||||
resValue 'string', 'bugsnag_api_key', "${(project.findProperty('BUGSNAG_API_KEY') ?: System.getenv('BUGSNAG_API_KEY')) ?: ''}"
|
||||
|
||||
|
@ -92,7 +94,9 @@ android {
|
|||
// freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.FlowPreview"
|
||||
}
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
if (Boolean.parseBoolean(isUseTestOrchestrator)) {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
}
|
||||
}
|
||||
kapt {
|
||||
arguments {
|
||||
|
@ -183,8 +187,15 @@ dependencies {
|
|||
testImplementation Deps.Test.MOKITO
|
||||
testImplementation Deps.Test.MOKITO_KOTLIN
|
||||
|
||||
androidTestImplementation Deps.Kotlin.REFLECT
|
||||
androidTestImplementation Deps.Test.Android.JUNIT
|
||||
|
||||
androidTestImplementation Deps.Test.Android.CORE
|
||||
androidTestImplementation Deps.Test.Android.FRAGMENT
|
||||
androidTestImplementation Deps.Test.Android.ESPRESSO
|
||||
androidTestImplementation Deps.Test.Android.ESPRESSO_INTENTS
|
||||
androidTestImplementation Deps.Test.Android.NAVIGATION
|
||||
// androidTestImplementation is preferred, but then the androidx.fragment.app.testing.FragmentScenario$EmptyFragmentActivity isn't available
|
||||
debugImplementation Deps.Test.Android.FRAGMENT
|
||||
}
|
||||
|
||||
defaultTasks 'clean', 'assembleZcashmainnetRelease'
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
package cash.z.ecc.android
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import cash.z.ecc.android.ui.util.MemoUtil
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
|
@ -13,11 +9,11 @@ import org.junit.runners.Parameterized
|
|||
// @RunWith(Parameterized::class)
|
||||
class MemoTest(val input: String, val output: String) {
|
||||
|
||||
@Test
|
||||
fun testExtractValidAddress() = runBlocking {
|
||||
val result = MemoUtil.firstValidAddress(input, ::validateMemo)
|
||||
assertEquals(output, result)
|
||||
}
|
||||
// @Test
|
||||
// fun testExtractValidAddress() = runBlocking {
|
||||
// val result = MemoUtil.findAddressInMemo(input, ::validateMemo)
|
||||
// assertEquals(output, result)
|
||||
// }
|
||||
|
||||
suspend fun validateMemo(memo: String): Boolean {
|
||||
delay(20)
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.test.platform.app.InstrumentationRegistry
|
|||
import cash.z.ecc.android.lockbox.LockBox
|
||||
import cash.z.ecc.android.sdk.Initializer
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import cash.z.ecc.kotlin.mnemonic.Mnemonics
|
||||
import okio.Buffer
|
||||
import okio.GzipSink
|
||||
import okio.Okio
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package cash.z.ecc.android.integration
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import cash.z.ecc.android.lockbox.LockBox
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
class LockBoxTest {
|
||||
|
@ -22,6 +24,8 @@ class LockBoxTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@LargeTest
|
||||
@Ignore("This test is extremely slow")
|
||||
fun testLongString() {
|
||||
var successCount = 0
|
||||
repeat(iterations) {
|
||||
|
@ -36,6 +40,8 @@ class LockBoxTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@LargeTest
|
||||
@Ignore("This test is extremely slow")
|
||||
fun testShortString() {
|
||||
var successCount = 0
|
||||
repeat(iterations) {
|
||||
|
@ -50,6 +56,8 @@ class LockBoxTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@LargeTest
|
||||
@Ignore("This test is extremely slow")
|
||||
fun testGiantString() {
|
||||
var successCount = 0
|
||||
repeat(iterations) {
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package cash.z.ecc.android.preference
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.lang.reflect.Modifier
|
||||
import kotlin.reflect.full.memberProperties
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PreferenceKeysTest {
|
||||
@SmallTest
|
||||
@Test
|
||||
@Throws(IllegalAccessException::class)
|
||||
fun fields_public_static_and_final() {
|
||||
PreferenceKeys::class.java.fields.forEach {
|
||||
val modifiers = it.modifiers
|
||||
assertThat(Modifier.isFinal(modifiers), equalTo(true))
|
||||
assertThat(Modifier.isStatic(modifiers), equalTo(true))
|
||||
assertThat(Modifier.isPublic(modifiers), equalTo(true))
|
||||
}
|
||||
}
|
||||
|
||||
// This test is primary to prevent copy-paste errors in preference keys
|
||||
@SmallTest
|
||||
@Test
|
||||
fun key_values_unique() {
|
||||
val fieldValueSet = mutableSetOf<String>()
|
||||
|
||||
PreferenceKeys::class.memberProperties
|
||||
.map { it.getter.call() }
|
||||
.map { it as String }
|
||||
.forEach {
|
||||
assertThat("Duplicate key $it", fieldValueSet.contains(it), equalTo(false))
|
||||
|
||||
fieldValueSet.add(it)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package cash.z.ecc.android.preference
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.android.preference.model.DefaultValue
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.lang.reflect.Modifier
|
||||
import kotlin.reflect.full.memberProperties
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PreferencesTest {
|
||||
@SmallTest
|
||||
@Test
|
||||
@Throws(IllegalAccessException::class)
|
||||
fun fields_public_static_and_final() {
|
||||
Preferences::class.java.fields.forEach {
|
||||
val modifiers = it.modifiers
|
||||
assertThat(Modifier.isFinal(modifiers), equalTo(true))
|
||||
assertThat(Modifier.isStatic(modifiers), equalTo(true))
|
||||
assertThat(Modifier.isPublic(modifiers), equalTo(true))
|
||||
}
|
||||
}
|
||||
|
||||
// This test is primary to prevent copy-paste errors in preference keys
|
||||
@SmallTest
|
||||
@Test
|
||||
fun key_values_unique() {
|
||||
val fieldValueSet = mutableSetOf<String>()
|
||||
|
||||
Preferences::class.memberProperties
|
||||
.map { it.getter.call(Preferences) }
|
||||
.map { it as DefaultValue<*> }
|
||||
.forEach {
|
||||
assertThat(
|
||||
"Duplicate key ${it.key}",
|
||||
fieldValueSet.contains(it.key),
|
||||
equalTo(false)
|
||||
)
|
||||
|
||||
fieldValueSet.add(it.key)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package cash.z.ecc.android.test
|
||||
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.testing.FragmentScenario
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.testing.TestNavHostController
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
|
||||
data class FragmentNavigationScenario<T : Fragment>(
|
||||
val fragmentScenario: FragmentScenario<T>,
|
||||
val navigationController: TestNavHostController
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun <T : Fragment> new(
|
||||
fragmentScenario: FragmentScenario<T>,
|
||||
@IdRes currentDestination: Int
|
||||
): FragmentNavigationScenario<T> {
|
||||
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
|
||||
|
||||
fragmentScenario.onFragment {
|
||||
navController.setGraph(cash.z.ecc.android.R.navigation.mobile_navigation)
|
||||
navController.setCurrentDestination(currentDestination)
|
||||
|
||||
Navigation.setViewNavController(it.requireView(), navController)
|
||||
}
|
||||
|
||||
return FragmentNavigationScenario(fragmentScenario, navController)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package cash.z.ecc.android.test
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.Before
|
||||
import java.lang.AssertionError
|
||||
|
||||
/**
|
||||
* Subclass this for UI tests to ensure they run correctly. This helps when developers run tests
|
||||
* against a physical device that might have gone to sleep.
|
||||
*/
|
||||
open class UiTestPrerequisites {
|
||||
@Before
|
||||
fun verifyScreenOn() {
|
||||
if (!isScreenOn()) {
|
||||
throw AssertionError("Screen must be on for UI tests to run") // $NON-NLS
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
|
||||
private fun isScreenOn(): Boolean {
|
||||
val powerService = ApplicationProvider.getApplicationContext<Context>()
|
||||
.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
return powerService.isInteractive
|
||||
}
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
package cash.z.ecc.android.ui.home
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import androidx.fragment.app.testing.FragmentScenario
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.Intents.intended
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.preference.Preferences
|
||||
import cash.z.ecc.android.preference.SharedPreferenceFactory
|
||||
import cash.z.ecc.android.preference.model.get
|
||||
import cash.z.ecc.android.test.FragmentNavigationScenario
|
||||
import cash.z.ecc.android.test.UiTestPrerequisites
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AutoshieldingInformationFragmentTest : UiTestPrerequisites() {
|
||||
@Test
|
||||
@MediumTest
|
||||
fun dismiss_returns_home() {
|
||||
val fragmentNavigationScenario = newScenario()
|
||||
|
||||
onView(withId(cash.z.ecc.android.R.id.button_autoshield_dismiss)).also {
|
||||
it.perform(ViewActions.click())
|
||||
}
|
||||
|
||||
assertThat(
|
||||
fragmentNavigationScenario.navigationController.currentDestination?.id,
|
||||
equalTo(cash.z.ecc.android.R.id.nav_home)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun dismiss_sets_preference() {
|
||||
newScenario()
|
||||
|
||||
onView(withId(cash.z.ecc.android.R.id.button_autoshield_dismiss)).also {
|
||||
it.perform(ViewActions.click())
|
||||
}
|
||||
|
||||
assertThat(
|
||||
Preferences.isAcknowledgedAutoshieldingInformationPrompt.get(ApplicationProvider.getApplicationContext<Context>()),
|
||||
equalTo(true)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun clicking_more_info_launches_browser() {
|
||||
val fragmentNavigationScenario = newScenario()
|
||||
|
||||
onView(withId(cash.z.ecc.android.R.id.button_autoshield_more_info)).also {
|
||||
it.perform(ViewActions.click())
|
||||
}
|
||||
|
||||
assertThat(
|
||||
fragmentNavigationScenario.navigationController.currentDestination?.id,
|
||||
equalTo(cash.z.ecc.android.R.id.nav_autoshielding_info_details)
|
||||
)
|
||||
|
||||
// Note: it is difficult to verify that the browser is launched, because of how the
|
||||
// navigation component works.
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun clicking_more_info_sets_preference() {
|
||||
newScenario()
|
||||
|
||||
onView(withId(cash.z.ecc.android.R.id.button_autoshield_more_info)).also {
|
||||
it.perform(ViewActions.click())
|
||||
}
|
||||
|
||||
assertThat(
|
||||
Preferences.isAcknowledgedAutoshieldingInformationPrompt.get(ApplicationProvider.getApplicationContext<Context>()),
|
||||
equalTo(true)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun starting_fragment_does_not_launch_activities() {
|
||||
Intents.init()
|
||||
try {
|
||||
val fragmentNavigationScenario = newScenario()
|
||||
|
||||
// The test framework launches an Activity to host the Fragment under test
|
||||
// Since the class name is not a public API, this could break in the future with newer
|
||||
// versions of the AndroidX Test libraries.
|
||||
intended(
|
||||
hasComponent(
|
||||
ComponentName(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
"androidx.test.core.app.InstrumentationActivityInvoker\$BootstrapActivity"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Verifying that no other Activities (e.g. the link view) are launched without explicit
|
||||
// user interaction
|
||||
Intents.assertNoUnverifiedIntents()
|
||||
|
||||
assertThat(
|
||||
fragmentNavigationScenario.navigationController.currentDestination?.id,
|
||||
equalTo(cash.z.ecc.android.R.id.nav_autoshielding_info)
|
||||
)
|
||||
} finally {
|
||||
Intents.release()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun back_does_not_set_preference() {
|
||||
val fragmentNavigationScenario = newScenario()
|
||||
|
||||
fragmentNavigationScenario.fragmentScenario.onFragment {
|
||||
// Probably closest we can come to simulating back with the navigation test framework
|
||||
fragmentNavigationScenario.navigationController.navigateUp()
|
||||
}
|
||||
|
||||
assertThat(
|
||||
fragmentNavigationScenario.navigationController.currentDestination?.id,
|
||||
equalTo(cash.z.ecc.android.R.id.nav_home)
|
||||
)
|
||||
|
||||
assertThat(
|
||||
Preferences.isAcknowledgedAutoshieldingInformationPrompt.get(ApplicationProvider.getApplicationContext<Context>()),
|
||||
equalTo(false)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun newScenario(): FragmentNavigationScenario<AutoshieldingInformationFragment> {
|
||||
// Clear preferences for each scenario, as this most closely reflects how this fragment
|
||||
// is used in the app, as it is displayed usually on first launch
|
||||
SharedPreferenceFactory.getSharedPreferences(ApplicationProvider.getApplicationContext())
|
||||
.edit().clear().apply()
|
||||
|
||||
val scenario = FragmentScenario.launchInContainer(
|
||||
AutoshieldingInformationFragment::class.java,
|
||||
null,
|
||||
cash.z.ecc.android.R.style.ZcashTheme,
|
||||
null
|
||||
)
|
||||
|
||||
return FragmentNavigationScenario.new(
|
||||
scenario,
|
||||
cash.z.ecc.android.R.id.nav_autoshielding_info
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package cash.z.ecc.android.ext
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
/**
|
||||
* A safer alternative to [Fragment.requireContext], as it avoids leaking Fragment or Activity context
|
||||
* when Application context is often sufficient.
|
||||
*/
|
||||
fun Fragment.requireApplicationContext() = requireContext().applicationContext
|
|
@ -124,6 +124,7 @@ object Report {
|
|||
SCAN,
|
||||
AUTO_SHIELD_FINAL("autoshield.final"),
|
||||
AUTO_SHIELD_AVAILABLE("autoshield.available"),
|
||||
AUTO_SHIELD_INFORMATION("autoshield.information"),
|
||||
SEND_ADDRESS("send.address"),
|
||||
SEND_CONFIRM("send.confirm"),
|
||||
SEND_FINAL("send.final"),
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package cash.z.ecc.android.preference
|
||||
|
||||
internal object PreferenceKeys {
|
||||
const val IS_AUTOSHIELDING_INFO_ACKNOWLEDGED = "is_autoshielding_info_acknowledged"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package cash.z.ecc.android.preference
|
||||
|
||||
import cash.z.ecc.android.preference.model.BooleanDefaultValue
|
||||
|
||||
object Preferences {
|
||||
val isAcknowledgedAutoshieldingInformationPrompt =
|
||||
BooleanDefaultValue(PreferenceKeys.IS_AUTOSHIELDING_INFO_ACKNOWLEDGED, false)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package cash.z.ecc.android.preference
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object SharedPreferenceFactory {
|
||||
private const val DEFAULT_SHARED_PREFERENCES = "cash.z.ecc.default"
|
||||
|
||||
fun getSharedPreferences(context: Context) = context.getSharedPreferences(DEFAULT_SHARED_PREFERENCES, Context.MODE_PRIVATE)
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package cash.z.ecc.android.preference.model
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import cash.z.ecc.android.preference.SharedPreferenceFactory
|
||||
|
||||
/**
|
||||
* A default value represents a preference key, along with its default value. It does not, by itself,
|
||||
* know how to read or write values from the preference repository.
|
||||
*/
|
||||
data class BooleanDefaultValue(override val key: String, internal val defaultValue: Boolean) :
|
||||
DefaultValue<Boolean> {
|
||||
init {
|
||||
require(key.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
fun BooleanDefaultValue.get(context: Context) = get(
|
||||
SharedPreferenceFactory.getSharedPreferences(
|
||||
context
|
||||
)
|
||||
)
|
||||
|
||||
internal fun BooleanDefaultValue.get(sharedPreferences: SharedPreferences) =
|
||||
sharedPreferences.getBoolean(key, defaultValue)
|
||||
|
||||
fun BooleanDefaultValue.put(context: Context, newValue: Boolean) = put(
|
||||
SharedPreferenceFactory.getSharedPreferences(
|
||||
context
|
||||
),
|
||||
newValue
|
||||
)
|
||||
|
||||
internal fun BooleanDefaultValue.put(sharedPreferences: SharedPreferences, newValue: Boolean) =
|
||||
sharedPreferences.edit().putBoolean(key, newValue).apply()
|
|
@ -0,0 +1,23 @@
|
|||
package cash.z.ecc.android.preference.model
|
||||
|
||||
/**
|
||||
* A key and a default value for a key-value store of preferences.
|
||||
*
|
||||
* Use of this interface avoids duplication or accidental variation in default value, because key
|
||||
* and default are defined together just once.
|
||||
*
|
||||
* Note that T is not fully generic and should be one of the supported types: Boolean, ... (other types to be added in the future)
|
||||
*
|
||||
* @see BooleanDefaultValue
|
||||
*/
|
||||
/*
|
||||
* Although primitives would be nice, Objects don't increase memory usage much
|
||||
* because of the autoboxing cache on the JVM. For example, Boolean's true/false values
|
||||
* are cached.
|
||||
*/
|
||||
interface DefaultValue<T> {
|
||||
// Note: the default value is not available through the public interface in order to prevent
|
||||
// clients from accidentally using the default value instead of the stored value.
|
||||
|
||||
val key: String
|
||||
}
|
|
@ -87,7 +87,7 @@ import kotlinx.coroutines.flow.collect
|
|||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
class MainActivity : AppCompatActivity(R.layout.main_activity) {
|
||||
|
||||
@Inject
|
||||
lateinit var mainViewModel: MainViewModel
|
||||
|
@ -141,7 +141,6 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.main_activity)
|
||||
initNavigation()
|
||||
initLoadScreen()
|
||||
|
||||
|
@ -653,6 +652,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
dialogViewBinding.dialogMessage.setText(msgResId)
|
||||
if (dialog != null) dialog?.dismiss()
|
||||
// TODO: This should be moved to a DialogFragment, otherwise unmanaged dialogs go away during Activity configuration changes
|
||||
dialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(titleResId)
|
||||
.setView(dialogViewBinding.root)
|
||||
|
|
|
@ -21,7 +21,15 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class BaseFragment<T : ViewBinding> : Fragment() {
|
||||
val mainActivity: MainActivity? get() = activity as MainActivity?
|
||||
// Normally will be of type MainActivity, but will be null when run under automated tests.
|
||||
// A future enhancement would be to move analytics. For example, refactor it out of the Activity
|
||||
// so that we don't have to cast. Or at least put analytics into an interface, so that we're more
|
||||
// explicitly casting to Analytics rather than MainActivity.
|
||||
val mainActivity: MainActivity? get() = if (activity is MainActivity) {
|
||||
activity as MainActivity
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
lateinit var binding: T
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package cash.z.ecc.android.ui.home
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import cash.z.ecc.android.R
|
||||
import cash.z.ecc.android.databinding.FragmentAutoShieldInformationBinding
|
||||
import cash.z.ecc.android.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.feedback.Report
|
||||
import cash.z.ecc.android.preference.Preferences
|
||||
import cash.z.ecc.android.preference.model.put
|
||||
import cash.z.ecc.android.ui.base.BaseFragment
|
||||
|
||||
/*
|
||||
* If the user presses the Android back button, the backstack will be popped and the user returns
|
||||
* to the app home screen. The preference will not be set in that case, because it could be considered
|
||||
* that the user did not acknowledge this prompt.
|
||||
*/
|
||||
class AutoshieldingInformationFragment : BaseFragment<FragmentAutoShieldInformationBinding>() {
|
||||
override val screen = Report.Screen.AUTO_SHIELD_INFORMATION
|
||||
|
||||
override fun inflate(inflater: LayoutInflater): FragmentAutoShieldInformationBinding =
|
||||
FragmentAutoShieldInformationBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.buttonAutoshieldDismiss.setOnClickListener {
|
||||
Preferences.isAcknowledgedAutoshieldingInformationPrompt.put(
|
||||
requireApplicationContext(),
|
||||
true
|
||||
)
|
||||
findNavController().navigate(R.id.action_nav_autoshielding_info_to_home)
|
||||
}
|
||||
binding.buttonAutoshieldMoreInfo.setOnClickListener {
|
||||
Preferences.isAcknowledgedAutoshieldingInformationPrompt.put(
|
||||
requireApplicationContext(),
|
||||
true
|
||||
)
|
||||
try {
|
||||
findNavController().navigate(R.id.action_nav_autoshielding_info_to_browser)
|
||||
} catch (e: Exception) {
|
||||
// ActivityNotFoundException could happen on certain devices, like Android TV, Android Things, etc.
|
||||
|
||||
// SecurityException shouldn't occur, but just in case we catch all exceptions to
|
||||
// prevent another package on the device from crashing us if that package tries to be malicious
|
||||
// by adding permissions or changing export status dynamically.
|
||||
|
||||
// In the future, it might also be desirable to display a Toast or Snackbar indicating
|
||||
// that the browser couldn't be launched
|
||||
|
||||
findNavController().navigate(R.id.action_nav_autoshielding_info_to_home)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ import cash.z.ecc.android.ext.disabledIf
|
|||
import cash.z.ecc.android.ext.goneIf
|
||||
import cash.z.ecc.android.ext.invisibleIf
|
||||
import cash.z.ecc.android.ext.onClickNavTo
|
||||
import cash.z.ecc.android.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.ext.showSharedLibraryCriticalError
|
||||
import cash.z.ecc.android.ext.toAppColor
|
||||
import cash.z.ecc.android.ext.toColoredSpan
|
||||
|
@ -32,6 +33,8 @@ import cash.z.ecc.android.feedback.Report.Tap.HOME_HISTORY
|
|||
import cash.z.ecc.android.feedback.Report.Tap.HOME_PROFILE
|
||||
import cash.z.ecc.android.feedback.Report.Tap.HOME_RECEIVE
|
||||
import cash.z.ecc.android.feedback.Report.Tap.HOME_SEND
|
||||
import cash.z.ecc.android.preference.Preferences
|
||||
import cash.z.ecc.android.preference.model.get
|
||||
import cash.z.ecc.android.sdk.Synchronizer.Status.DISCONNECTED
|
||||
import cash.z.ecc.android.sdk.Synchronizer.Status.STOPPED
|
||||
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
|
||||
|
@ -178,7 +181,12 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
private fun onSyncReady() {
|
||||
twig("Sync ready! Monitoring synchronizer state...")
|
||||
monitorUiModelChanges()
|
||||
maybeInterruptUser()
|
||||
|
||||
if (!Preferences.isAcknowledgedAutoshieldingInformationPrompt.get(requireApplicationContext())) {
|
||||
mainActivity?.safeNavigate(R.id.action_nav_home_to_autoshielding_info)
|
||||
} else {
|
||||
maybeInterruptUser()
|
||||
}
|
||||
|
||||
twig("HomeFragment.onSyncReady COMPLETE")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/background_home">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_hit_area_top"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.04" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_send_amount_top"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.13" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_content_start"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.15" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_content_end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.85" />
|
||||
|
||||
<!-- TODO: the color isn't exactly right -->
|
||||
<ImageView
|
||||
android:id="@+id/icon_autoshielding"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:elevation="6dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.212"
|
||||
app:layout_constraintWidth_percent="0.4"
|
||||
android:src="@drawable/ic_check_shield"
|
||||
app:tint="@color/colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
style="@style/TextAppearance.AppCompat.Body1"
|
||||
android:text="@string/autoshielding_title_text"
|
||||
android:textSize="20sp"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/icon_autoshielding" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
style="@style/TextAppearance.AppCompat.Body1"
|
||||
android:text="@string/autoshielding_body_text"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_title" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_autoshield_dismiss"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Zcash.Button"
|
||||
android:gravity="center"
|
||||
android:padding="12dp"
|
||||
android:text="@string/autoshielding_button_positive"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="#000000"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_description"
|
||||
app:layout_constraintVertical_bias="0.1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_autoshield_more_info"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Zcash.Button.OutlinedButton"
|
||||
android:gravity="center"
|
||||
android:padding="12dp"
|
||||
android:text="@string/autoshielding_button_neutral"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/text_light"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_autoshield_dismiss" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -19,6 +19,9 @@
|
|||
<action
|
||||
android:id="@+id/action_nav_home_to_create_wallet"
|
||||
app:destination="@id/nav_landing" />
|
||||
<action
|
||||
android:id="@+id/action_nav_home_to_autoshielding_info"
|
||||
app:destination="@id/nav_autoshielding_info" />
|
||||
<action
|
||||
android:id="@+id/action_nav_home_to_send"
|
||||
app:destination="@id/nav_send"
|
||||
|
@ -116,6 +119,26 @@
|
|||
app:popUpTo="@id/nav_home"
|
||||
app:popUpToInclusive="false"/>
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/nav_autoshielding_info"
|
||||
android:name="cash.z.ecc.android.ui.home.AutoshieldingInformationFragment"
|
||||
tools:layout="@layout/fragment_auto_shield_information" >
|
||||
<action
|
||||
android:id="@+id/action_nav_autoshielding_info_to_home"
|
||||
app:destination="@id/nav_home"
|
||||
app:popUpTo="@id/nav_home"
|
||||
app:popUpToInclusive="true" />
|
||||
<action
|
||||
android:id="@+id/action_nav_autoshielding_info_to_browser"
|
||||
app:destination="@id/nav_autoshielding_info_details"
|
||||
app:popUpTo="@id/nav_home"
|
||||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
|
||||
<activity
|
||||
android:id="@+id/nav_autoshielding_info_details"
|
||||
app:action="android.intent.action.VIEW"
|
||||
app:data="@string/autoshield_explanation_url" />
|
||||
|
||||
<!-- -->
|
||||
<!-- Send Navigation -->
|
||||
|
@ -229,7 +252,7 @@
|
|||
app:popUpTo="@id/nav_home"
|
||||
app:popUpToInclusive="false"/>
|
||||
</fragment>
|
||||
|
||||
|
||||
<!-- -->
|
||||
<!-- Wallet Setup Navigation -->
|
||||
<!-- -->
|
||||
|
@ -266,6 +289,7 @@
|
|||
</fragment>
|
||||
|
||||
|
||||
|
||||
<!-- -->
|
||||
<!-- Global actions -->
|
||||
<!-- -->
|
||||
|
|
|
@ -38,6 +38,11 @@
|
|||
<string name="missing_home_dialog_no_balance_message">To make full use of this wallet, deposit funds to your address.</string>
|
||||
<string name="missing_home_instruction_fund_now">Fund Now</string>
|
||||
|
||||
<string name="missing_autoshielding_title_text">We now <b>shield by default</b>. This is to ensure maximum privacy.</string>
|
||||
<string name="missing_autoshielding_body_text"><b>Autoshielding</b> means any funds coming into your transparent address will automatically be moved to your shielded wallet.\n\nWe’ll always update you on the latest privacy-preserving best practices and recommendations.</string>
|
||||
<string name="missing_autoshielding_button_positive">Great!</string>
|
||||
<string name="missing_autoshielding_button_neutral">Tell me more</string>
|
||||
|
||||
<!-- Screen: Landing -->
|
||||
<string name="missing_landing_backup_skipped_message_1">Are you sure? Without a backup, funds can be lost FOREVER!</string>
|
||||
<string name="missing_landing_backup_skipped_message_2">You can\'t backup later. You\'re probably going to lose your funds!</string>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<!-- ======================================================================================= -->
|
||||
<!-- Strings that are missing translations. Individually, these can be moved into -->
|
||||
<!-- the 'translated-urls.xml' file, after they are translated to all supported languages -->
|
||||
<!-- ======================================================================================= -->
|
||||
<string name="missing_autoshield_explanation_url">https://electriccoin.co/blog/unified-addresses-in-zcash-explained/</string>
|
||||
</resources>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<string name="autoshield_explanation_url">@string/missing_autoshield_explanation_url</string>
|
||||
</resources>
|
|
@ -56,6 +56,12 @@
|
|||
<string name="home_no_balance">@string/translated_balance_nofunds</string>
|
||||
<string name="home_title">@string/translated_balance_amounttosend</string>
|
||||
|
||||
<!-- Screen: Autoshielding prompt -->
|
||||
<string name="autoshielding_title_text">@string/missing_autoshielding_title_text</string>
|
||||
<string name="autoshielding_body_text">@string/missing_autoshielding_body_text</string>
|
||||
<string name="autoshielding_button_positive">@string/missing_autoshielding_button_positive</string>
|
||||
<string name="autoshielding_button_neutral">@string/missing_autoshielding_button_neutral</string>
|
||||
|
||||
<!-- Screen: Balance Detail -->
|
||||
<string name="balance_detail_button_send_transparent_funds" tools:ignore="MissingTranslation">Shield Transparent Funds</string>
|
||||
<string name="balance_detail_text_total_title" tools:ignore="MissingTranslation">= TOTAL</string>
|
||||
|
|
|
@ -6,7 +6,7 @@ object Deps {
|
|||
const val kotlinVersion = "1.5.10"
|
||||
const val navigationVersion = "2.3.5"
|
||||
|
||||
const val compileSdkVersion = 30
|
||||
const val compileSdkVersion = 31
|
||||
const val buildToolsVersion = "30.0.3"
|
||||
const val minSdkVersion = 21
|
||||
const val targetSdkVersion = 30
|
||||
|
@ -20,8 +20,8 @@ object Deps {
|
|||
const val APPCOMPAT = "androidx.appcompat:appcompat:1.4.0-alpha02"
|
||||
const val BIOMETRICS = "androidx.biometric:biometric:1.2.0-alpha03"
|
||||
const val CONSTRAINT_LAYOUT = "androidx.constraintlayout:constraintlayout:2.1.0-beta02"
|
||||
const val CORE_KTX = "androidx.core:core-ktx:1.6.0-rc01"
|
||||
const val FRAGMENT_KTX = "androidx.fragment:fragment-ktx:1.3.3"
|
||||
const val CORE_KTX = "androidx.core:core-ktx:1.6.0"
|
||||
const val FRAGMENT_KTX = "androidx.fragment:fragment-ktx:1.3.6"
|
||||
const val LEGACY = "androidx.legacy:legacy-support-v4:1.0.0"
|
||||
const val MULTIDEX = "androidx.multidex:multidex:2.0.1"
|
||||
const val PAGING = "androidx.paging:paging-runtime-ktx:2.1.2"
|
||||
|
@ -30,7 +30,7 @@ object Deps {
|
|||
val CAMERA2 = "androidx.camera:camera-camera2:$version"
|
||||
val CORE = "androidx.camera:camera-core:$version"
|
||||
val LIFECYCLE = "androidx.camera:camera-lifecycle:$version"
|
||||
object View : Version("1.0.0-alpha25") {
|
||||
object View : Version("1.0.0-alpha27") {
|
||||
val EXT = "androidx.camera:camera-extensions:$version"
|
||||
val VIEW = "androidx.camera:camera-view:$version"
|
||||
}
|
||||
|
@ -74,6 +74,7 @@ object Deps {
|
|||
}
|
||||
object Kotlin : Version(kotlinVersion) {
|
||||
val STDLIB = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version"
|
||||
val REFLECT = "org.jetbrains.kotlin:kotlin-reflect:$version"
|
||||
object Coroutines : Version("1.4.2") {
|
||||
val ANDROID = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version"
|
||||
val CORE = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"
|
||||
|
@ -96,10 +97,16 @@ object Deps {
|
|||
|
||||
object Test {
|
||||
const val JUNIT = "junit:junit:4.13.2"
|
||||
const val MOKITO = "org.mockito:mockito-android:3.11.1"
|
||||
const val MOKITO = "org.mockito:mockito-android:3.12.4"
|
||||
const val MOKITO_KOTLIN = "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
||||
object Android {
|
||||
const val JUNIT = "androidx.test.ext:junit:1.1.3-alpha06"
|
||||
const val CORE = "androidx.test:core:1.4.0"
|
||||
const val RULES = "androidx.test:rules:1.4.0"
|
||||
const val JUNIT = "androidx.test.ext:junit:1.1.3"
|
||||
const val FRAGMENT = "androidx.fragment:fragment-testing:1.4.0-alpha08"
|
||||
const val ESPRESSO = "androidx.test.espresso:espresso-core:3.4.0"
|
||||
const val ESPRESSO_INTENTS = "androidx.test.espresso:espresso-intents:3.4.0"
|
||||
const val NAVIGATION = "androidx.navigation:navigation-testing:2.3.0-alpha01"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,4 +16,8 @@ android.enableJetifier=true
|
|||
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
||||
android.useAndroidX=true
|
||||
dagger.fastInit=enabled
|
||||
android.builder.sdkDownload=true
|
||||
android.builder.sdkDownload=true
|
||||
|
||||
# Optionally configures test orchestrator. While it provides isolated tests, it also nearly doubles
|
||||
# the time it takes for test to run.
|
||||
isUseTestOrchestrator=false
|
Loading…
Reference in New Issue