diff --git a/app/src/main/java/co/electriccoin/zcash/app/ZcashApplication.kt b/app/src/main/java/co/electriccoin/zcash/app/ZcashApplication.kt index 8b7750bc6..763b0870c 100644 --- a/app/src/main/java/co/electriccoin/zcash/app/ZcashApplication.kt +++ b/app/src/main/java/co/electriccoin/zcash/app/ZcashApplication.kt @@ -3,6 +3,8 @@ package co.electriccoin.zcash.app import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.ProcessLifecycleOwner import co.electriccoin.zcash.crash.android.GlobalCrashReporter +import co.electriccoin.zcash.crash.android.di.CrashReportersProvider +import co.electriccoin.zcash.crash.android.di.crashProviderModule import co.electriccoin.zcash.di.coreModule import co.electriccoin.zcash.di.dataSourceModule import co.electriccoin.zcash.di.providerModule @@ -25,6 +27,7 @@ class ZcashApplication : CoroutineApplication() { private val standardPreferenceProvider by inject() private val flexaRepository by inject() private val applicationStateProvider: ApplicationStateProvider by inject() + private val getAvailableCrashReporters: CrashReportersProvider by inject() override fun onCreate() { super.onCreate() @@ -39,6 +42,7 @@ class ZcashApplication : CoroutineApplication() { modules( coreModule, providerModule, + crashProviderModule, dataSourceModule, repositoryModule, useCaseModule, @@ -77,7 +81,7 @@ class ZcashApplication : CoroutineApplication() { } private fun configureAnalytics() { - if (GlobalCrashReporter.register(this)) { + if (GlobalCrashReporter.register(this, getAvailableCrashReporters())) { applicationScope.launch { StandardPreferenceKeys.IS_ANALYTICS_ENABLED.observe(standardPreferenceProvider()).collect { if (it) { diff --git a/crash-android-lib/build.gradle.kts b/crash-android-lib/build.gradle.kts index aed11c7ea..38fb07e64 100644 --- a/crash-android-lib/build.gradle.kts +++ b/crash-android-lib/build.gradle.kts @@ -1,3 +1,6 @@ +import model.DistributionDimension +import model.NetworkDimension + plugins { id("com.android.library") kotlin("android") @@ -21,17 +24,39 @@ android { testOptions { execution = "ANDROIDX_TEST_ORCHESTRATOR" } + + flavorDimensions += listOf(NetworkDimension.DIMENSION_NAME, DistributionDimension.DIMENSION_NAME) + + productFlavors { + create(NetworkDimension.TESTNET.value) { + dimension = NetworkDimension.DIMENSION_NAME + } + + create(NetworkDimension.MAINNET.value) { + dimension = NetworkDimension.DIMENSION_NAME + } + + create(DistributionDimension.STORE.value) { + dimension = DistributionDimension.DIMENSION_NAME + } + + create(DistributionDimension.FOSS.value) { + dimension = DistributionDimension.DIMENSION_NAME + } + } } dependencies { api(libs.androidx.annotation) api(projects.crashLib) - implementation(platform(libs.firebase.bom)) + api(libs.bundles.koin) + + "storeImplementation"(platform(libs.firebase.bom)) + "storeImplementation"(libs.firebase.crashlytics) + "storeImplementation"(libs.firebase.crashlytics.ndk) + "storeImplementation"(libs.firebase.installations) - implementation(libs.firebase.crashlytics) - implementation(libs.firebase.crashlytics.ndk) - implementation(libs.firebase.installations) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.datetime) implementation(projects.spackleAndroidLib) diff --git a/crash-android-lib/src/foss/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReportersImpl.kt b/crash-android-lib/src/foss/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReportersImpl.kt new file mode 100644 index 000000000..5894076db --- /dev/null +++ b/crash-android-lib/src/foss/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReportersImpl.kt @@ -0,0 +1,12 @@ +package co.electriccoin.zcash.crash.android.internal + +import android.content.Context +import co.electriccoin.zcash.crash.android.internal.local.LocalCrashReporter + +class ListCrashReportersImpl : ListCrashReporters { + override fun provideReporters(context: Context): List { + return listOfNotNull( + LocalCrashReporter.getInstance(context), + ) + } +} diff --git a/crash-android-lib/src/main/AndroidManifest.xml b/crash-android-lib/src/main/AndroidManifest.xml index 9698c6101..e0c5ae868 100644 --- a/crash-android-lib/src/main/AndroidManifest.xml +++ b/crash-android-lib/src/main/AndroidManifest.xml @@ -1,32 +1,16 @@ - - + - - - - - - - - - diff --git a/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/GlobalCrashReporter.kt b/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/GlobalCrashReporter.kt index e2f76cc2a..60ab4b025 100644 --- a/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/GlobalCrashReporter.kt +++ b/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/GlobalCrashReporter.kt @@ -4,8 +4,7 @@ import android.content.Context import androidx.annotation.AnyThread import androidx.annotation.MainThread import co.electriccoin.zcash.crash.android.internal.CrashReporter -import co.electriccoin.zcash.crash.android.internal.firebase.FirebaseCrashReporter -import co.electriccoin.zcash.crash.android.internal.local.LocalCrashReporter +import co.electriccoin.zcash.crash.android.internal.ListCrashReporters import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.spackle.process.ProcessNameCompat import java.util.Collections @@ -24,7 +23,10 @@ object GlobalCrashReporter { * @return True if registration occurred and false if registration was skipped. */ @MainThread - fun register(context: Context): Boolean { + fun register( + context: Context, + reporters: ListCrashReporters + ): Boolean { if (isCrashProcess(context)) { Twig.debug { "Skipping registration for $CRASH_PROCESS_NAME_SUFFIX process" } // $NON-NLS return false @@ -34,15 +36,7 @@ object GlobalCrashReporter { if (registeredCrashReporters == null) { registeredCrashReporters = Collections.synchronizedList( - // To prevent a race condition, register the LocalCrashReporter first. - // FirebaseCrashReporter does some asynchronous registration internally, while - // LocalCrashReporter uses AndroidUncaughtExceptionHandler which needs to read - // and write the default UncaughtExceptionHandler. The only way to ensure - // interleaving doesn't happen is to register the LocalCrashReporter first. - listOfNotNull( - LocalCrashReporter.getInstance(context), - FirebaseCrashReporter(context), - ) + reporters.provideReporters(context) ) } } diff --git a/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/di/CrashReportersProvider.kt b/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/di/CrashReportersProvider.kt new file mode 100644 index 000000000..4bff0333d --- /dev/null +++ b/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/di/CrashReportersProvider.kt @@ -0,0 +1,7 @@ +package co.electriccoin.zcash.crash.android.di + +import co.electriccoin.zcash.crash.android.internal.ListCrashReportersImpl + +class CrashReportersProvider { + operator fun invoke() = ListCrashReportersImpl() +} diff --git a/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/di/ProviderModule.kt b/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/di/ProviderModule.kt new file mode 100644 index 000000000..92633b343 --- /dev/null +++ b/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/di/ProviderModule.kt @@ -0,0 +1,9 @@ +package co.electriccoin.zcash.crash.android.di + +import org.koin.core.module.dsl.factoryOf +import org.koin.dsl.module + +val crashProviderModule = + module { + factoryOf(::CrashReportersProvider) + } diff --git a/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReporters.kt b/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReporters.kt new file mode 100644 index 000000000..507df9065 --- /dev/null +++ b/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReporters.kt @@ -0,0 +1,7 @@ +package co.electriccoin.zcash.crash.android.internal + +import android.content.Context + +interface ListCrashReporters { + fun provideReporters(context: Context): List +} diff --git a/crash-android-lib/src/main/res/values/bools.xml b/crash-android-lib/src/main/res/values/bools.xml index 0ee6dcc3e..7b9abcc66 100644 --- a/crash-android-lib/src/main/res/values/bools.xml +++ b/crash-android-lib/src/main/res/values/bools.xml @@ -1,7 +1,4 @@ true - - false diff --git a/crash-android-lib/src/zcashmainnetStoreDebug/AndroidManifest.xml b/crash-android-lib/src/zcashmainnetStoreDebug/AndroidManifest.xml new file mode 100644 index 000000000..9698c6101 --- /dev/null +++ b/crash-android-lib/src/zcashmainnetStoreDebug/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/crash-android-lib/src/zcashmainnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReportersImpl.kt b/crash-android-lib/src/zcashmainnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReportersImpl.kt new file mode 100644 index 000000000..132394f8c --- /dev/null +++ b/crash-android-lib/src/zcashmainnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReportersImpl.kt @@ -0,0 +1,17 @@ +package co.electriccoin.zcash.crash.android.internal + +import android.content.Context +import co.electriccoin.zcash.crash.android.internal.local.LocalCrashReporter + +class ListCrashReportersImpl : ListCrashReporters { + override fun provideReporters(context: Context): List { + // To prevent a race condition, register the LocalCrashReporter first. + // FirebaseCrashReporter does some asynchronous registration internally, while + // LocalCrashReporter uses AndroidUncaughtExceptionHandler which needs to read + // and write the default UncaughtExceptionHandler. The only way to ensure + // interleaving doesn't happen is to register the LocalCrashReporter first. + return listOfNotNull( + LocalCrashReporter.getInstance(context), + ) + } +} diff --git a/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseAppCache.kt b/crash-android-lib/src/zcashmainnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseAppCache.kt similarity index 100% rename from crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseAppCache.kt rename to crash-android-lib/src/zcashmainnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseAppCache.kt diff --git a/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseCrashReporter.kt b/crash-android-lib/src/zcashmainnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseCrashReporter.kt similarity index 100% rename from crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseCrashReporter.kt rename to crash-android-lib/src/zcashmainnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseCrashReporter.kt diff --git a/crash-android-lib/src/zcashmainnetStoreDebug/res/values/bools.xml b/crash-android-lib/src/zcashmainnetStoreDebug/res/values/bools.xml new file mode 100644 index 000000000..0ee6dcc3e --- /dev/null +++ b/crash-android-lib/src/zcashmainnetStoreDebug/res/values/bools.xml @@ -0,0 +1,7 @@ + + + true + + false + diff --git a/crash-android-lib/src/zcashmainnetStoreRelease/AndroidManifest.xml b/crash-android-lib/src/zcashmainnetStoreRelease/AndroidManifest.xml new file mode 100644 index 000000000..9698c6101 --- /dev/null +++ b/crash-android-lib/src/zcashmainnetStoreRelease/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/crash-android-lib/src/zcashmainnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReportersImpl.kt b/crash-android-lib/src/zcashmainnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReportersImpl.kt new file mode 100644 index 000000000..63adedf8d --- /dev/null +++ b/crash-android-lib/src/zcashmainnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReportersImpl.kt @@ -0,0 +1,19 @@ +package co.electriccoin.zcash.crash.android.internal + +import android.content.Context +import co.electriccoin.zcash.crash.android.internal.firebase.FirebaseCrashReporter +import co.electriccoin.zcash.crash.android.internal.local.LocalCrashReporter + +class ListCrashReportersImpl : ListCrashReporters { + override fun provideReporters(context: Context): List { + // To prevent a race condition, register the LocalCrashReporter first. + // FirebaseCrashReporter does some asynchronous registration internally, while + // LocalCrashReporter uses AndroidUncaughtExceptionHandler which needs to read + // and write the default UncaughtExceptionHandler. The only way to ensure + // interleaving doesn't happen is to register the LocalCrashReporter first. + return listOfNotNull( + LocalCrashReporter.getInstance(context), + FirebaseCrashReporter(context), + ) + } +} diff --git a/crash-android-lib/src/zcashmainnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseAppCache.kt b/crash-android-lib/src/zcashmainnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseAppCache.kt new file mode 100644 index 000000000..3862aa435 --- /dev/null +++ b/crash-android-lib/src/zcashmainnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseAppCache.kt @@ -0,0 +1,39 @@ +package co.electriccoin.zcash.crash.android.internal.firebase + +import android.content.Context +import com.google.firebase.FirebaseApp +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext + +object FirebaseAppCache { + private val mutex = Mutex() + + @Volatile + private var cachedFirebaseApp: FirebaseAppContainer? = null + + fun peekFirebaseApp(): FirebaseApp? = cachedFirebaseApp?.firebaseApp + + suspend fun getFirebaseApp(context: Context): FirebaseApp? { + mutex.withLock { + peekFirebaseApp()?.let { + return it + } + + val firebaseAppContainer = getFirebaseAppContainer(context) + + cachedFirebaseApp = firebaseAppContainer + } + + return peekFirebaseApp() + } +} + +private suspend fun getFirebaseAppContainer(context: Context): FirebaseAppContainer = + withContext(Dispatchers.IO) { + val firebaseApp = FirebaseApp.initializeApp(context) + FirebaseAppContainer(firebaseApp) + } + +private class FirebaseAppContainer(val firebaseApp: FirebaseApp?) diff --git a/crash-android-lib/src/zcashmainnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseCrashReporter.kt b/crash-android-lib/src/zcashmainnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseCrashReporter.kt new file mode 100644 index 000000000..19b9d16a5 --- /dev/null +++ b/crash-android-lib/src/zcashmainnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseCrashReporter.kt @@ -0,0 +1,135 @@ +@file:JvmName("FirebaseCrashReporterKt") + +package co.electriccoin.zcash.crash.android.internal.firebase + +import android.content.Context +import androidx.annotation.AnyThread +import co.electriccoin.zcash.crash.android.R +import co.electriccoin.zcash.crash.android.internal.CrashReporter +import co.electriccoin.zcash.spackle.EmulatorWtfUtil +import co.electriccoin.zcash.spackle.FirebaseTestLabUtil +import co.electriccoin.zcash.spackle.SuspendingLazy +import co.electriccoin.zcash.spackle.Twig +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.firebase.installations.FirebaseInstallations +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async + +/** + * Registers an exception handler with Firebase Crashlytics. + */ +internal class FirebaseCrashReporter( + context: Context +) : CrashReporter { + @OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class) + private val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + private val initFirebaseJob: Deferred = + analyticsScope.async { + FirebaseCrashReporterImpl.getInstance(context) + } + + @AnyThread + override fun reportCaughtException(exception: Throwable) { + initFirebaseJob.invokeOnCompletionWithResult { + it?.reportCaughtException(exception) + } + } + + override fun enable() { + initFirebaseJob.invokeOnCompletionWithResult { + it?.enable() + } + } + + override fun disableAndDelete() { + initFirebaseJob.invokeOnCompletionWithResult { + it?.disableAndDelete() + } + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +private fun Deferred.invokeOnCompletionWithResult(handler: (T) -> Unit) { + invokeOnCompletion { + handler(this.getCompleted()) + } +} + +/** + * Registers an exception handler with Firebase Crashlytics. + */ +private class FirebaseCrashReporterImpl( + private val firebaseCrashlytics: FirebaseCrashlytics, + private val firebaseInstallations: FirebaseInstallations +) : CrashReporter { + @AnyThread + override fun reportCaughtException(exception: Throwable) { + error( + "Although most of the sensitive model objects implement custom [toString] methods to redact information" + + " if they were to be logged (which includes exceptions), we're encouraged to disable caught exception" + + " reporting to the remote Crashlytics service due to its security risk. Use the the local variant of" + + " the reporter to report caught exception - [LocalCrashReporter]." + ) + } + + override fun enable() { + firebaseCrashlytics.setCrashlyticsCollectionEnabled(true) + } + + override fun disableAndDelete() { + firebaseCrashlytics.setCrashlyticsCollectionEnabled(false) + firebaseCrashlytics.deleteUnsentReports() + firebaseInstallations.delete() + } + + companion object { + /* + * Note there is a tradeoff with the suspending implementation. In order to avoid disk IO + * on the main thread, there is a brief timing gap during application startup where very + * early crashes may be missed. This is a tradeoff we are willing to make in order to avoid + * ANRs. + */ + private val lazyWithArgument = + SuspendingLazy { + if (it.resources.getBoolean(R.bool.co_electriccoin_zcash_crash_is_firebase_enabled)) { + + // Workaround for disk IO on main thread in Firebase initialization + val firebaseApp = FirebaseAppCache.getFirebaseApp(it) + if (firebaseApp == null) { + Twig.warn { "Unable to initialize Crashlytics. FirebaseApp is null" } + return@SuspendingLazy null + } + + val firebaseInstallations = FirebaseInstallations.getInstance(firebaseApp) + val firebaseCrashlytics = + FirebaseCrashlytics.getInstance().apply { + setCustomKey( + CrashlyticsUserProperties.IS_TEST, + EmulatorWtfUtil.isEmulatorWtf(it) || FirebaseTestLabUtil.isFirebaseTestLab(it) + ) + } + + FirebaseCrashReporterImpl(firebaseCrashlytics, firebaseInstallations) + } else { + Twig.warn { "Unable to initialize Crashlytics. Configure API keys in the app module" } + null + } + } + + suspend fun getInstance(context: Context): CrashReporter? { + return lazyWithArgument.getInstance(context) + } + } +} + +internal object CrashlyticsUserProperties { + /** + * Flags a crash as occurring in a test environment. Set automatically to detect Firebase Test Lab and emulator.wtf + */ + const val IS_TEST = "is_test" // $NON-NLS +} diff --git a/crash-android-lib/src/zcashmainnetStoreRelease/res/values/bools.xml b/crash-android-lib/src/zcashmainnetStoreRelease/res/values/bools.xml new file mode 100644 index 000000000..0ee6dcc3e --- /dev/null +++ b/crash-android-lib/src/zcashmainnetStoreRelease/res/values/bools.xml @@ -0,0 +1,7 @@ + + + true + + false + diff --git a/crash-android-lib/src/zcashtestnetStoreDebug/AndroidManifest.xml b/crash-android-lib/src/zcashtestnetStoreDebug/AndroidManifest.xml new file mode 100644 index 000000000..9698c6101 --- /dev/null +++ b/crash-android-lib/src/zcashtestnetStoreDebug/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/crash-android-lib/src/zcashtestnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReportersImpl.kt b/crash-android-lib/src/zcashtestnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReportersImpl.kt new file mode 100644 index 000000000..132394f8c --- /dev/null +++ b/crash-android-lib/src/zcashtestnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReportersImpl.kt @@ -0,0 +1,17 @@ +package co.electriccoin.zcash.crash.android.internal + +import android.content.Context +import co.electriccoin.zcash.crash.android.internal.local.LocalCrashReporter + +class ListCrashReportersImpl : ListCrashReporters { + override fun provideReporters(context: Context): List { + // To prevent a race condition, register the LocalCrashReporter first. + // FirebaseCrashReporter does some asynchronous registration internally, while + // LocalCrashReporter uses AndroidUncaughtExceptionHandler which needs to read + // and write the default UncaughtExceptionHandler. The only way to ensure + // interleaving doesn't happen is to register the LocalCrashReporter first. + return listOfNotNull( + LocalCrashReporter.getInstance(context), + ) + } +} diff --git a/crash-android-lib/src/zcashtestnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseAppCache.kt b/crash-android-lib/src/zcashtestnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseAppCache.kt new file mode 100644 index 000000000..3862aa435 --- /dev/null +++ b/crash-android-lib/src/zcashtestnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseAppCache.kt @@ -0,0 +1,39 @@ +package co.electriccoin.zcash.crash.android.internal.firebase + +import android.content.Context +import com.google.firebase.FirebaseApp +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext + +object FirebaseAppCache { + private val mutex = Mutex() + + @Volatile + private var cachedFirebaseApp: FirebaseAppContainer? = null + + fun peekFirebaseApp(): FirebaseApp? = cachedFirebaseApp?.firebaseApp + + suspend fun getFirebaseApp(context: Context): FirebaseApp? { + mutex.withLock { + peekFirebaseApp()?.let { + return it + } + + val firebaseAppContainer = getFirebaseAppContainer(context) + + cachedFirebaseApp = firebaseAppContainer + } + + return peekFirebaseApp() + } +} + +private suspend fun getFirebaseAppContainer(context: Context): FirebaseAppContainer = + withContext(Dispatchers.IO) { + val firebaseApp = FirebaseApp.initializeApp(context) + FirebaseAppContainer(firebaseApp) + } + +private class FirebaseAppContainer(val firebaseApp: FirebaseApp?) diff --git a/crash-android-lib/src/zcashtestnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseCrashReporter.kt b/crash-android-lib/src/zcashtestnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseCrashReporter.kt new file mode 100644 index 000000000..19b9d16a5 --- /dev/null +++ b/crash-android-lib/src/zcashtestnetStoreDebug/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseCrashReporter.kt @@ -0,0 +1,135 @@ +@file:JvmName("FirebaseCrashReporterKt") + +package co.electriccoin.zcash.crash.android.internal.firebase + +import android.content.Context +import androidx.annotation.AnyThread +import co.electriccoin.zcash.crash.android.R +import co.electriccoin.zcash.crash.android.internal.CrashReporter +import co.electriccoin.zcash.spackle.EmulatorWtfUtil +import co.electriccoin.zcash.spackle.FirebaseTestLabUtil +import co.electriccoin.zcash.spackle.SuspendingLazy +import co.electriccoin.zcash.spackle.Twig +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.firebase.installations.FirebaseInstallations +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async + +/** + * Registers an exception handler with Firebase Crashlytics. + */ +internal class FirebaseCrashReporter( + context: Context +) : CrashReporter { + @OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class) + private val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + private val initFirebaseJob: Deferred = + analyticsScope.async { + FirebaseCrashReporterImpl.getInstance(context) + } + + @AnyThread + override fun reportCaughtException(exception: Throwable) { + initFirebaseJob.invokeOnCompletionWithResult { + it?.reportCaughtException(exception) + } + } + + override fun enable() { + initFirebaseJob.invokeOnCompletionWithResult { + it?.enable() + } + } + + override fun disableAndDelete() { + initFirebaseJob.invokeOnCompletionWithResult { + it?.disableAndDelete() + } + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +private fun Deferred.invokeOnCompletionWithResult(handler: (T) -> Unit) { + invokeOnCompletion { + handler(this.getCompleted()) + } +} + +/** + * Registers an exception handler with Firebase Crashlytics. + */ +private class FirebaseCrashReporterImpl( + private val firebaseCrashlytics: FirebaseCrashlytics, + private val firebaseInstallations: FirebaseInstallations +) : CrashReporter { + @AnyThread + override fun reportCaughtException(exception: Throwable) { + error( + "Although most of the sensitive model objects implement custom [toString] methods to redact information" + + " if they were to be logged (which includes exceptions), we're encouraged to disable caught exception" + + " reporting to the remote Crashlytics service due to its security risk. Use the the local variant of" + + " the reporter to report caught exception - [LocalCrashReporter]." + ) + } + + override fun enable() { + firebaseCrashlytics.setCrashlyticsCollectionEnabled(true) + } + + override fun disableAndDelete() { + firebaseCrashlytics.setCrashlyticsCollectionEnabled(false) + firebaseCrashlytics.deleteUnsentReports() + firebaseInstallations.delete() + } + + companion object { + /* + * Note there is a tradeoff with the suspending implementation. In order to avoid disk IO + * on the main thread, there is a brief timing gap during application startup where very + * early crashes may be missed. This is a tradeoff we are willing to make in order to avoid + * ANRs. + */ + private val lazyWithArgument = + SuspendingLazy { + if (it.resources.getBoolean(R.bool.co_electriccoin_zcash_crash_is_firebase_enabled)) { + + // Workaround for disk IO on main thread in Firebase initialization + val firebaseApp = FirebaseAppCache.getFirebaseApp(it) + if (firebaseApp == null) { + Twig.warn { "Unable to initialize Crashlytics. FirebaseApp is null" } + return@SuspendingLazy null + } + + val firebaseInstallations = FirebaseInstallations.getInstance(firebaseApp) + val firebaseCrashlytics = + FirebaseCrashlytics.getInstance().apply { + setCustomKey( + CrashlyticsUserProperties.IS_TEST, + EmulatorWtfUtil.isEmulatorWtf(it) || FirebaseTestLabUtil.isFirebaseTestLab(it) + ) + } + + FirebaseCrashReporterImpl(firebaseCrashlytics, firebaseInstallations) + } else { + Twig.warn { "Unable to initialize Crashlytics. Configure API keys in the app module" } + null + } + } + + suspend fun getInstance(context: Context): CrashReporter? { + return lazyWithArgument.getInstance(context) + } + } +} + +internal object CrashlyticsUserProperties { + /** + * Flags a crash as occurring in a test environment. Set automatically to detect Firebase Test Lab and emulator.wtf + */ + const val IS_TEST = "is_test" // $NON-NLS +} diff --git a/crash-android-lib/src/zcashtestnetStoreDebug/res/values/bools.xml b/crash-android-lib/src/zcashtestnetStoreDebug/res/values/bools.xml new file mode 100644 index 000000000..0ee6dcc3e --- /dev/null +++ b/crash-android-lib/src/zcashtestnetStoreDebug/res/values/bools.xml @@ -0,0 +1,7 @@ + + + true + + false + diff --git a/crash-android-lib/src/zcashtestnetStoreRelease/AndroidManifest.xml b/crash-android-lib/src/zcashtestnetStoreRelease/AndroidManifest.xml new file mode 100644 index 000000000..9698c6101 --- /dev/null +++ b/crash-android-lib/src/zcashtestnetStoreRelease/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/crash-android-lib/src/zcashtestnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReportersImpl.kt b/crash-android-lib/src/zcashtestnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReportersImpl.kt new file mode 100644 index 000000000..63adedf8d --- /dev/null +++ b/crash-android-lib/src/zcashtestnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/ListCrashReportersImpl.kt @@ -0,0 +1,19 @@ +package co.electriccoin.zcash.crash.android.internal + +import android.content.Context +import co.electriccoin.zcash.crash.android.internal.firebase.FirebaseCrashReporter +import co.electriccoin.zcash.crash.android.internal.local.LocalCrashReporter + +class ListCrashReportersImpl : ListCrashReporters { + override fun provideReporters(context: Context): List { + // To prevent a race condition, register the LocalCrashReporter first. + // FirebaseCrashReporter does some asynchronous registration internally, while + // LocalCrashReporter uses AndroidUncaughtExceptionHandler which needs to read + // and write the default UncaughtExceptionHandler. The only way to ensure + // interleaving doesn't happen is to register the LocalCrashReporter first. + return listOfNotNull( + LocalCrashReporter.getInstance(context), + FirebaseCrashReporter(context), + ) + } +} diff --git a/crash-android-lib/src/zcashtestnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseAppCache.kt b/crash-android-lib/src/zcashtestnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseAppCache.kt new file mode 100644 index 000000000..3862aa435 --- /dev/null +++ b/crash-android-lib/src/zcashtestnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseAppCache.kt @@ -0,0 +1,39 @@ +package co.electriccoin.zcash.crash.android.internal.firebase + +import android.content.Context +import com.google.firebase.FirebaseApp +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext + +object FirebaseAppCache { + private val mutex = Mutex() + + @Volatile + private var cachedFirebaseApp: FirebaseAppContainer? = null + + fun peekFirebaseApp(): FirebaseApp? = cachedFirebaseApp?.firebaseApp + + suspend fun getFirebaseApp(context: Context): FirebaseApp? { + mutex.withLock { + peekFirebaseApp()?.let { + return it + } + + val firebaseAppContainer = getFirebaseAppContainer(context) + + cachedFirebaseApp = firebaseAppContainer + } + + return peekFirebaseApp() + } +} + +private suspend fun getFirebaseAppContainer(context: Context): FirebaseAppContainer = + withContext(Dispatchers.IO) { + val firebaseApp = FirebaseApp.initializeApp(context) + FirebaseAppContainer(firebaseApp) + } + +private class FirebaseAppContainer(val firebaseApp: FirebaseApp?) diff --git a/crash-android-lib/src/zcashtestnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseCrashReporter.kt b/crash-android-lib/src/zcashtestnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseCrashReporter.kt new file mode 100644 index 000000000..19b9d16a5 --- /dev/null +++ b/crash-android-lib/src/zcashtestnetStoreRelease/kotlin/co/electriccoin/zcash/crash/android/internal/firebase/FirebaseCrashReporter.kt @@ -0,0 +1,135 @@ +@file:JvmName("FirebaseCrashReporterKt") + +package co.electriccoin.zcash.crash.android.internal.firebase + +import android.content.Context +import androidx.annotation.AnyThread +import co.electriccoin.zcash.crash.android.R +import co.electriccoin.zcash.crash.android.internal.CrashReporter +import co.electriccoin.zcash.spackle.EmulatorWtfUtil +import co.electriccoin.zcash.spackle.FirebaseTestLabUtil +import co.electriccoin.zcash.spackle.SuspendingLazy +import co.electriccoin.zcash.spackle.Twig +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.firebase.installations.FirebaseInstallations +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async + +/** + * Registers an exception handler with Firebase Crashlytics. + */ +internal class FirebaseCrashReporter( + context: Context +) : CrashReporter { + @OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class) + private val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + private val initFirebaseJob: Deferred = + analyticsScope.async { + FirebaseCrashReporterImpl.getInstance(context) + } + + @AnyThread + override fun reportCaughtException(exception: Throwable) { + initFirebaseJob.invokeOnCompletionWithResult { + it?.reportCaughtException(exception) + } + } + + override fun enable() { + initFirebaseJob.invokeOnCompletionWithResult { + it?.enable() + } + } + + override fun disableAndDelete() { + initFirebaseJob.invokeOnCompletionWithResult { + it?.disableAndDelete() + } + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +private fun Deferred.invokeOnCompletionWithResult(handler: (T) -> Unit) { + invokeOnCompletion { + handler(this.getCompleted()) + } +} + +/** + * Registers an exception handler with Firebase Crashlytics. + */ +private class FirebaseCrashReporterImpl( + private val firebaseCrashlytics: FirebaseCrashlytics, + private val firebaseInstallations: FirebaseInstallations +) : CrashReporter { + @AnyThread + override fun reportCaughtException(exception: Throwable) { + error( + "Although most of the sensitive model objects implement custom [toString] methods to redact information" + + " if they were to be logged (which includes exceptions), we're encouraged to disable caught exception" + + " reporting to the remote Crashlytics service due to its security risk. Use the the local variant of" + + " the reporter to report caught exception - [LocalCrashReporter]." + ) + } + + override fun enable() { + firebaseCrashlytics.setCrashlyticsCollectionEnabled(true) + } + + override fun disableAndDelete() { + firebaseCrashlytics.setCrashlyticsCollectionEnabled(false) + firebaseCrashlytics.deleteUnsentReports() + firebaseInstallations.delete() + } + + companion object { + /* + * Note there is a tradeoff with the suspending implementation. In order to avoid disk IO + * on the main thread, there is a brief timing gap during application startup where very + * early crashes may be missed. This is a tradeoff we are willing to make in order to avoid + * ANRs. + */ + private val lazyWithArgument = + SuspendingLazy { + if (it.resources.getBoolean(R.bool.co_electriccoin_zcash_crash_is_firebase_enabled)) { + + // Workaround for disk IO on main thread in Firebase initialization + val firebaseApp = FirebaseAppCache.getFirebaseApp(it) + if (firebaseApp == null) { + Twig.warn { "Unable to initialize Crashlytics. FirebaseApp is null" } + return@SuspendingLazy null + } + + val firebaseInstallations = FirebaseInstallations.getInstance(firebaseApp) + val firebaseCrashlytics = + FirebaseCrashlytics.getInstance().apply { + setCustomKey( + CrashlyticsUserProperties.IS_TEST, + EmulatorWtfUtil.isEmulatorWtf(it) || FirebaseTestLabUtil.isFirebaseTestLab(it) + ) + } + + FirebaseCrashReporterImpl(firebaseCrashlytics, firebaseInstallations) + } else { + Twig.warn { "Unable to initialize Crashlytics. Configure API keys in the app module" } + null + } + } + + suspend fun getInstance(context: Context): CrashReporter? { + return lazyWithArgument.getInstance(context) + } + } +} + +internal object CrashlyticsUserProperties { + /** + * Flags a crash as occurring in a test environment. Set automatically to detect Firebase Test Lab and emulator.wtf + */ + const val IS_TEST = "is_test" // $NON-NLS +} diff --git a/crash-android-lib/src/zcashtestnetStoreRelease/res/values/bools.xml b/crash-android-lib/src/zcashtestnetStoreRelease/res/values/bools.xml new file mode 100644 index 000000000..0ee6dcc3e --- /dev/null +++ b/crash-android-lib/src/zcashtestnetStoreRelease/res/values/bools.xml @@ -0,0 +1,7 @@ + + + true + + false + diff --git a/gradle.properties b/gradle.properties index 179362365..580821f7f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -156,9 +156,11 @@ ANDROID_GRADLE_PLUGIN_VERSION=8.5.0 DETEKT_VERSION=1.23.6 DETEKT_COMPOSE_RULES_VERSION=0.3.15 EMULATOR_WTF_GRADLE_PLUGIN_VERSION=0.16.2 +# Handled FIREBASE_CRASHLYTICS_BUILD_TOOLS_VERSION=2.9.9 FLANK_VERSION=23.10.1 FULLADLE_VERSION=0.17.4 +# Handled GOOGLE_PLAY_SERVICES_GRADLE_PLUGIN_VERSION=4.4.1 GRADLE_VERSIONS_PLUGIN_VERSION=0.51.0 JGIT_VERSION=6.4.0.202211300538-r @@ -194,7 +196,9 @@ ANDROIDX_UI_AUTOMATOR_VERSION=2.3.0 ANDROIDX_WORK_MANAGER_VERSION=2.9.0 ANDROIDX_BROWSER_VERSION=1.8.0 CORE_LIBRARY_DESUGARING_VERSION=2.1.2 +# Handled FIREBASE_BOM_VERSION_MATCHER=33.1.1 +# Handled GOOGLE_AUTH_LIB_JAVA_VERSION=1.18.0 JACOCO_VERSION=0.8.12 KEYSTONE_VERSION=0.7.10 @@ -206,9 +210,11 @@ KOTLINX_SERIALIZABLE_JSON_VERSION=1.6.3 KOVER_VERSION=0.7.3 LOTTIE_VERSION=6.5.0 MARKDOWN_VERSION=0.7.3 +# Should we handle? MLKIT_SCANNING_VERSION=17.3.0 -PLAY_APP_UPDATE_VERSION=2.1.0 +# We should handle PLAY_APP_UPDATE_KTX_VERSION=2.1.0 +# We should handle PLAY_PUBLISHER_API_VERSION=v3-rev20231030-2.0.0 TINK_VERSION=1.15.0 ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0 diff --git a/settings.gradle.kts b/settings.gradle.kts index da9e202b5..5d221d21f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -184,8 +184,6 @@ dependencyResolutionManagement { val lottieVersion = extra["LOTTIE_VERSION"].toString() val markdownVersion = extra["MARKDOWN_VERSION"].toString() val mlkitScanningVersion = extra["MLKIT_SCANNING_VERSION"].toString() - val playAppUpdateVersion = extra["PLAY_APP_UPDATE_VERSION"].toString() - val playAppUpdateKtxVersion = extra["PLAY_APP_UPDATE_KTX_VERSION"].toString() val tinkVersion = extra["TINK_VERSION"].toString() val zcashBip39Version = extra["ZCASH_BIP39_VERSION"].toString() val zcashSdkVersion = extra["ZCASH_SDK_VERSION"].toString() @@ -250,8 +248,6 @@ dependencyResolutionManagement { library("lottie", "com.airbnb.android:lottie-compose:$lottieVersion") library("markdown", "org.jetbrains:markdown:$markdownVersion") library("mlkit-scanning", "com.google.mlkit:barcode-scanning:$mlkitScanningVersion") - library("play-update", "com.google.android.play:app-update:$playAppUpdateVersion") - library("play-update-ktx", "com.google.android.play:app-update-ktx:$playAppUpdateKtxVersion") library("tink", "com.google.crypto.tink:tink-android:$tinkVersion") library("zcash-sdk", "cash.z.ecc.android:zcash-android-sdk:$zcashSdkVersion") library("zcash-sdk-incubator", "cash.z.ecc.android:zcash-android-sdk-incubator:$zcashSdkVersion") @@ -334,13 +330,6 @@ dependencyResolutionManagement { "koin-compose", ) ) - bundle( - "play-update", - listOf( - "play-update", - "play-update-ktx", - ) - ) } } } diff --git a/ui-integration-test/build.gradle.kts b/ui-integration-test/build.gradle.kts index e8c8fe234..24684f4c1 100644 --- a/ui-integration-test/build.gradle.kts +++ b/ui-integration-test/build.gradle.kts @@ -77,7 +77,6 @@ dependencies { implementation(libs.bundles.androidx.test) implementation(libs.bundles.androidx.compose.core) - implementation(libs.bundles.play.update) implementation(libs.androidx.compose.test.junit) implementation(libs.androidx.navigation.compose) diff --git a/ui-integration-test/src/main/java/co/electriccoin/zcash/ui/integration/test/screen/update/viewmodel/UpdateViewModelTest.kt b/ui-integration-test/src/main/java/co/electriccoin/zcash/ui/integration/test/screen/update/viewmodel/UpdateViewModelTest.kt deleted file mode 100644 index 990095991..000000000 --- a/ui-integration-test/src/main/java/co/electriccoin/zcash/ui/integration/test/screen/update/viewmodel/UpdateViewModelTest.kt +++ /dev/null @@ -1,111 +0,0 @@ -package co.electriccoin.zcash.ui.integration.test.screen.update.viewmodel - -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.lifecycle.viewModelScope -import androidx.test.filters.MediumTest -import co.electriccoin.zcash.test.UiTestPrerequisites -import co.electriccoin.zcash.ui.fixture.UpdateInfoFixture -import co.electriccoin.zcash.ui.integration.test.common.IntegrationTestingActivity -import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker -import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerMock -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.viewmodel.UpdateViewModel -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.collectIndexed -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -@ExperimentalCoroutinesApi -class UpdateViewModelTest : UiTestPrerequisites() { - @get:Rule - val composeTestRule = createAndroidComposeRule() - - private lateinit var viewModel: UpdateViewModel - private lateinit var checker: AppUpdateCheckerMock - private lateinit var initialUpdateInfo: UpdateInfo - - @Before - fun setup() { - checker = AppUpdateCheckerMock() - - initialUpdateInfo = - UpdateInfoFixture.new( - appUpdateInfo = null, - state = UpdateState.Prepared, - priority = AppUpdateChecker.Priority.LOW, - force = false - ) - - viewModel = - UpdateViewModel( - composeTestRule.activity.application, - initialUpdateInfo, - checker - ) - } - - @After - fun cleanup() { - viewModel.viewModelScope.cancel() - } - - @Test - @MediumTest - fun validate_result_of_update_methods_calls() = - runTest { - viewModel.checkForAppUpdate() - - // Although this test does not copy the real world situation, as the initial and result objects - // should be mostly the same, we test VM proper functionality. VM emits the initial object - // defined in this class, then we expect the result object from the AppUpdateCheckerMock class - // and a newly acquired AppUpdateInfo object. - viewModel.updateInfo.take(4).collectIndexed { index, incomingInfo -> - when (index) { - 0 -> { - // checkForAppUpdate initial callback - incomingInfo.also { - assertNull(it.appUpdateInfo) - - assertEquals(initialUpdateInfo.state, it.state) - assertEquals(initialUpdateInfo.appUpdateInfo, it.appUpdateInfo) - assertEquals(initialUpdateInfo.priority, it.priority) - assertEquals(initialUpdateInfo.state, it.state) - assertEquals(initialUpdateInfo.isForce, it.isForce) - } - } - 1 -> { - // checkForAppUpdate result callback - incomingInfo.also { - assertNotNull(it.appUpdateInfo) - - assertEquals(AppUpdateCheckerMock.resultUpdateInfo.state, it.state) - assertEquals(AppUpdateCheckerMock.resultUpdateInfo.priority, it.priority) - assertEquals(AppUpdateCheckerMock.resultUpdateInfo.isForce, it.isForce) - } - - // now we can start the update - viewModel.goForUpdate(composeTestRule.activity, incomingInfo.appUpdateInfo!!) - } - 2 -> { - // goForUpdate initial callback - assertNotNull(incomingInfo.appUpdateInfo) - assertEquals(UpdateState.Running, incomingInfo.state) - } - 3 -> { - // goForUpdate result callback - assertNotNull(incomingInfo.appUpdateInfo) - assertEquals(UpdateState.Done, incomingInfo.state) - } - } - } - } -} diff --git a/ui-lib/build.gradle.kts b/ui-lib/build.gradle.kts index 42875f844..9b9f8ed44 100644 --- a/ui-lib/build.gradle.kts +++ b/ui-lib/build.gradle.kts @@ -1,4 +1,6 @@ import com.android.build.api.variant.BuildConfigField +import model.DistributionDimension +import model.NetworkDimension plugins { id("com.android.library") @@ -77,6 +79,26 @@ android { ) } } + + flavorDimensions += listOf(NetworkDimension.DIMENSION_NAME, DistributionDimension.DIMENSION_NAME) + + productFlavors { + create(NetworkDimension.TESTNET.value) { + dimension = NetworkDimension.DIMENSION_NAME + } + + create(NetworkDimension.MAINNET.value) { + dimension = NetworkDimension.DIMENSION_NAME + } + + create(DistributionDimension.STORE.value) { + dimension = DistributionDimension.DIMENSION_NAME + } + + create(DistributionDimension.FOSS.value) { + dimension = DistributionDimension.DIMENSION_NAME + } + } } androidComponents { @@ -132,7 +154,6 @@ dependencies { implementation(libs.bundles.androidx.compose.core) implementation(libs.bundles.androidx.compose.extended) api(libs.bundles.koin) - implementation(libs.bundles.play.update) implementation(libs.kotlin.stdlib) implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.core) @@ -140,7 +161,7 @@ dependencies { implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.immutable) implementation(libs.kotlinx.serializable.json) - implementation(libs.mlkit.scanning) + "storeImplementation"(libs.mlkit.scanning) implementation(libs.zcash.sdk) implementation(libs.zcash.sdk.incubator) implementation(libs.zcash.bip39) diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/fixture/UpdateInfoFixtureTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/fixture/UpdateInfoFixtureTest.kt deleted file mode 100644 index b7b202530..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/fixture/UpdateInfoFixtureTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update.fixture - -import androidx.test.filters.SmallTest -import co.electriccoin.zcash.ui.fixture.UpdateInfoFixture -import org.junit.Test -import kotlin.test.assertEquals - -class UpdateInfoFixtureTest { - companion object { - val updateInfo = UpdateInfoFixture.new(appUpdateInfo = null) - } - - @Test - @SmallTest - fun fixture_result_test() { - updateInfo.also { - assertEquals(it.priority, UpdateInfoFixture.INITIAL_PRIORITY) - assertEquals(it.isForce, UpdateInfoFixture.INITIAL_FORCE) - assertEquals(it.state, UpdateInfoFixture.INITIAL_STATE) - assertEquals(it.appUpdateInfo, null) - } - } -} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/integration/UpdateViewIntegrationTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/integration/UpdateViewIntegrationTest.kt deleted file mode 100644 index 237449804..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/integration/UpdateViewIntegrationTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update.integration - -import androidx.compose.ui.test.junit4.StateRestorationTester -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.test.filters.MediumTest -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.fixture.UpdateInfoFixture -import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker -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.view.UpdateViewTestSetup -import co.electriccoin.zcash.ui.test.getStringResource -import org.junit.Rule -import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals - -class UpdateViewIntegrationTest { - @get:Rule - val composeTestRule = createComposeRule() - - @Test - @MediumTest - fun update_info_state_restoration() { - val restorationTester = StateRestorationTester(composeTestRule) - val testSetup = - newTestSetup( - UpdateInfoFixture.new( - priority = AppUpdateChecker.Priority.HIGH, - force = true, - appUpdateInfo = null, - state = UpdateState.Prepared - ) - ) - - restorationTester.setContent { - ZcashTheme { - testSetup.DefaultContent() - } - } - - assertEquals(testSetup.getUpdateInfo().priority, AppUpdateChecker.Priority.HIGH) - assertEquals(testSetup.getUpdateState(), UpdateState.Prepared) - - composeTestRule.onNodeWithText(getStringResource(R.string.update_download_button), ignoreCase = true).also { - it.performClick() - } - - // can be Running, Done, Canceled or Failed - depends on the Play API response - assertNotEquals(testSetup.getUpdateState(), UpdateState.Prepared) - - restorationTester.emulateSavedInstanceStateRestore() - - assertEquals(testSetup.getUpdateInfo().priority, AppUpdateChecker.Priority.HIGH) - assertNotEquals(testSetup.getUpdateState(), UpdateState.Prepared) - } - - private fun newTestSetup(updateInfo: UpdateInfo) = - UpdateViewTestSetup( - composeTestRule, - updateInfo - ) -} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/util/AppUpdateCheckerImplTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/util/AppUpdateCheckerImplTest.kt deleted file mode 100644 index 174a87ebb..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/util/AppUpdateCheckerImplTest.kt +++ /dev/null @@ -1,82 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update.util - -import android.app.Activity -import android.content.Context -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.filters.MediumTest -import cash.z.ecc.android.sdk.ext.onFirst -import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImpl -import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo -import co.electriccoin.zcash.ui.screen.update.model.UpdateState -import co.electriccoin.zcash.ui.test.getAppContext -import com.google.android.play.core.install.model.ActivityResult -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue - -class AppUpdateCheckerImplTest { - @get:Rule - val composeTestRule = createAndroidComposeRule() - - companion object { - val context: Context = getAppContext() - val updateChecker = AppUpdateCheckerImpl() - } - - private fun getAppUpdateInfoFlow(): Flow { - return updateChecker.newCheckForUpdateAvailabilityFlow( - context - ) - } - - @Test - @MediumTest - fun check_for_update_availability_test() = - runTest { - assertNotNull(updateChecker) - - getAppUpdateInfoFlow().onFirst { updateInfo -> - assertTrue( - listOf( - UpdateState.Failed, - UpdateState.Prepared, - UpdateState.Done - ).contains(updateInfo.state) - ) - } - } - - @Test - @MediumTest - fun start_update_availability_test() = - runTest { - getAppUpdateInfoFlow().onFirst { updateInfo -> - // In case we get result with FAILED state, e.g. app is still not released in the Google - // Play store, there is no way to continue with the test. - if (updateInfo.state == UpdateState.Failed) { - assertNull(updateInfo.appUpdateInfo) - return@onFirst - } - - assertNotNull(updateInfo.appUpdateInfo) - - updateChecker.newStartUpdateFlow( - composeTestRule.activity, - updateInfo.appUpdateInfo!! - ).onFirst { result -> - assertTrue { - listOf( - Activity.RESULT_OK, - Activity.RESULT_CANCELED, - ActivityResult.RESULT_IN_APP_UPDATE_FAILED - ).contains(result) - } - } - } - } -} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/util/PlayStoreUtilTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/util/PlayStoreUtilTest.kt deleted file mode 100644 index 2a7329b95..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/util/PlayStoreUtilTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update.util - -import android.content.Intent -import androidx.test.filters.SmallTest -import co.electriccoin.zcash.ui.test.getAppContext -import co.electriccoin.zcash.ui.util.PlayStoreUtil -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Test -import kotlin.test.assertContains - -class PlayStoreUtilTest { - companion object { - val PLAY_STORE_URI = - PlayStoreUtil.PLAY_STORE_APP_URI + - getAppContext().packageName - } - - @Test - @SmallTest - fun check_intent_for_store() { - val intent = PlayStoreUtil.newActivityIntent(getAppContext()) - assertNotNull(intent) - assertEquals(intent.action, Intent.ACTION_VIEW) - assertContains(PLAY_STORE_URI, intent.data.toString()) - assertEquals(PlayStoreUtil.FLAGS, intent.flags) - } -} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewAndroidTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewAndroidTest.kt deleted file mode 100644 index 0ba4e3b61..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewAndroidTest.kt +++ /dev/null @@ -1,77 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update.view - -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.test.espresso.Espresso -import androidx.test.filters.MediumTest -import co.electriccoin.zcash.test.UiTestPrerequisites -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.fixture.UpdateInfoFixture -import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker -import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo -import co.electriccoin.zcash.ui.screen.update.model.UpdateState -import co.electriccoin.zcash.ui.test.getStringResource -import org.junit.Rule -import org.junit.Test - -// Non-multiplatform tests that require interacting with the Android system (e.g. system back navigation) -// These don't have persistent state, so they are still unit tests. -class UpdateViewAndroidTest : UiTestPrerequisites() { - @get:Rule - val composeTestRule = createAndroidComposeRule() - - private fun newTestSetup(updateInfo: UpdateInfo) = - UpdateViewAndroidTestSetup( - updateInfo, - composeTestRule - ).apply { - setDefaultContent() - } - - @Test - @MediumTest - fun postpone_optional_update_test() { - val updateInfo = - UpdateInfoFixture.new( - priority = AppUpdateChecker.Priority.LOW, - force = false, - appUpdateInfo = null, - state = UpdateState.Prepared - ) - newTestSetup(updateInfo) - - composeTestRule.onNodeWithText(getStringResource(R.string.update_title_available), ignoreCase = true).also { - it.assertExists() - } - - Espresso.pressBack() - - composeTestRule.onNodeWithText(getStringResource(R.string.update_title_available), ignoreCase = true).also { - it.assertDoesNotExist() - } - } - - @Test - @MediumTest - fun postpone_force_update_test() { - val updateInfo = - UpdateInfoFixture.new( - priority = AppUpdateChecker.Priority.HIGH, - force = true, - appUpdateInfo = null, - state = UpdateState.Prepared - ) - newTestSetup(updateInfo) - - composeTestRule.onNodeWithText(getStringResource(R.string.update_title_required), ignoreCase = true).also { - it.assertExists() - } - - Espresso.pressBack() - - composeTestRule.onNodeWithText(getStringResource(R.string.update_title_required), ignoreCase = true).also { - it.assertExists() - } - } -} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewAndroidTestSetup.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewAndroidTestSetup.kt deleted file mode 100644 index caa379423..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewAndroidTestSetup.kt +++ /dev/null @@ -1,48 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update.view - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import co.electriccoin.zcash.ui.common.compose.LocalActivity -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerMock -import co.electriccoin.zcash.ui.screen.update.WrapUpdate -import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo -import co.electriccoin.zcash.ui.screen.update.viewmodel.UpdateViewModel - -class UpdateViewAndroidTestSetup( - updateInfo: UpdateInfo, - private val composeTestRule: AndroidComposeTestRule<*, *> -) { - private val viewModel = - UpdateViewModel( - application = composeTestRule.activity.application, - updateInfo = updateInfo, - appUpdateChecker = AppUpdateCheckerMock() - ) - - @Composable - @Suppress("TestFunctionName") - fun DefaultContent() { - CompositionLocalProvider(LocalActivity provides composeTestRule.activity) { - val updateInfo = viewModel.updateInfo.collectAsStateWithLifecycle().value - // val appUpdateInfo = updateInfo.appUpdateInfo - WrapUpdate( - updateInfo = updateInfo, - checkForUpdate = viewModel::checkForAppUpdate, - remindLater = viewModel::remindLater, - goForUpdate = {}, - onSettings = {} - ) - } - } - - fun setDefaultContent() { - composeTestRule.setContent { - ZcashTheme { - DefaultContent() - } - } - } -} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewTest.kt deleted file mode 100644 index 7c7b07f15..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewTest.kt +++ /dev/null @@ -1,159 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update.view - -import androidx.compose.ui.test.junit4.ComposeContentTestRule -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.performClick -import androidx.test.filters.MediumTest -import co.electriccoin.zcash.test.UiTestPrerequisites -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.fixture.UpdateInfoFixture -import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker -import co.electriccoin.zcash.ui.screen.update.UpdateTag -import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo -import co.electriccoin.zcash.ui.screen.update.model.UpdateState -import co.electriccoin.zcash.ui.test.getStringResource -import org.junit.Assert.assertEquals -import org.junit.Ignore -import org.junit.Rule -import org.junit.Test - -class UpdateViewTest : UiTestPrerequisites() { - @get:Rule - val composeTestRule = createComposeRule() - - @Test - @MediumTest - fun texts_force_update_test() { - val updateInfo = - UpdateInfoFixture.new( - priority = AppUpdateChecker.Priority.HIGH, - force = true, - appUpdateInfo = null, - state = UpdateState.Prepared - ) - - newTestSetup(updateInfo) - - composeTestRule.onNodeWithText( - text = getStringResource(R.string.update_title_required), - ignoreCase = true - ).also { - it.assertExists() - } - } - - @Test - @MediumTest - fun later_btn_not_force_update_test() { - val updateInfo = - UpdateInfoFixture.new( - priority = AppUpdateChecker.Priority.LOW, - force = false, - appUpdateInfo = null, - state = UpdateState.Prepared - ) - val testSetup = newTestSetup(updateInfo) - - assertEquals(0, testSetup.getOnLaterCount()) - - composeTestRule.clickLater() - - assertEquals(1, testSetup.getOnLaterCount()) - } - - @Test - @MediumTest - fun texts_not_force_update_test() { - val updateInfo = - UpdateInfoFixture.new( - priority = AppUpdateChecker.Priority.MEDIUM, - force = false, - appUpdateInfo = null, - state = UpdateState.Prepared - ) - - newTestSetup(updateInfo) - - composeTestRule.onNodeWithText( - text = getStringResource(R.string.update_title_available), - ignoreCase = true - ).also { - it.assertExists() - } - } - - @Test - @MediumTest - fun later_btn_update_test() { - val updateInfo = - UpdateInfoFixture.new( - priority = AppUpdateChecker.Priority.LOW, - force = false, - appUpdateInfo = null, - state = UpdateState.Prepared - ) - val testSetup = newTestSetup(updateInfo) - - assertEquals(0, testSetup.getOnLaterCount()) - - composeTestRule.clickLater() - - assertEquals(1, testSetup.getOnLaterCount()) - } - - @Test - @MediumTest - fun download_btn_test() { - val updateInfo = UpdateInfoFixture.new(appUpdateInfo = null) - - val testSetup = newTestSetup(updateInfo) - - assertEquals(0, testSetup.getOnDownloadCount()) - - composeTestRule.clickDownload() - - assertEquals(1, testSetup.getOnDownloadCount()) - } - - @Test - @MediumTest - @Ignore("Disable the test for now -> we have no way to click a clickable span right now") - fun play_store_ref_test() { - val updateInfo = UpdateInfoFixture.new(appUpdateInfo = null) - - val testSetup = newTestSetup(updateInfo) - - assertEquals(0, testSetup.getOnReferenceCount()) - composeTestRule.onRoot().assertExists() - - composeTestRule.onNodeWithText(getStringResource(R.string.update_link_text), substring = true,).also { - it.assertExists() - it.performClick() - } - - assertEquals(1, testSetup.getOnReferenceCount()) - } - - private fun newTestSetup(updateInfo: UpdateInfo) = - UpdateViewTestSetup( - composeTestRule, - updateInfo - ).apply { - setDefaultContent() - } -} - -private fun ComposeContentTestRule.clickLater() { - onNodeWithTag(UpdateTag.BTN_LATER).also { - it.performClick() - } -} - -private fun ComposeContentTestRule.clickDownload() { - onNodeWithTag(UpdateTag.BTN_DOWNLOAD).also { - it.performClick() - } -} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewTestSetup.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewTestSetup.kt deleted file mode 100644 index f78ce668e..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewTestSetup.kt +++ /dev/null @@ -1,73 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update.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.update.model.UpdateInfo -import co.electriccoin.zcash.ui.screen.update.model.UpdateState -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.atomic.AtomicReference - -class UpdateViewTestSetup( - private val composeTestRule: ComposeContentTestRule, - private val updateInfo: UpdateInfo -) { - private val onDownloadCount = AtomicInteger(0) - private val onLaterCount = AtomicInteger(0) - private val onReferenceCount = AtomicInteger(0) - private val updateState = AtomicReference(UpdateState.Prepared) - - fun getOnDownloadCount(): Int { - composeTestRule.waitForIdle() - return onDownloadCount.get() - } - - fun getOnLaterCount(): Int { - composeTestRule.waitForIdle() - return onLaterCount.get() - } - - fun getOnReferenceCount(): Int { - composeTestRule.waitForIdle() - return onReferenceCount.get() - } - - fun getUpdateState(): UpdateState { - composeTestRule.waitForIdle() - return updateState.get() - } - - fun getUpdateInfo(): UpdateInfo { - composeTestRule.waitForIdle() - return updateInfo - } - - @Composable - @Suppress("TestFunctionName") - fun DefaultContent() { - Update( - snackbarHostState = SnackbarHostState(), - updateInfo = updateInfo, - onDownload = { newState -> - onDownloadCount.incrementAndGet() - updateState.set(newState) - }, - onLater = { - onLaterCount.incrementAndGet() - }, - onReference = { - onReferenceCount.incrementAndGet() - }, - onSettings = {} - ) - } - - fun setDefaultContent() { - composeTestRule.setContent { - ZcashTheme { - DefaultContent() - } - } - } -} diff --git a/ui-lib/src/foss/java/co/electriccoin/zcash/ui/screen/scan/util/QrCodeAnalyzerImpl.kt b/ui-lib/src/foss/java/co/electriccoin/zcash/ui/screen/scan/util/QrCodeAnalyzerImpl.kt new file mode 100644 index 000000000..45530643a --- /dev/null +++ b/ui-lib/src/foss/java/co/electriccoin/zcash/ui/screen/scan/util/QrCodeAnalyzerImpl.kt @@ -0,0 +1,120 @@ +package co.electriccoin.zcash.ui.screen.scan.util + +import android.graphics.ImageFormat +import androidx.camera.core.ImageProxy +import co.electriccoin.zcash.spackle.Twig +import co.electriccoin.zcash.ui.screen.scankeystone.view.FramePosition +import com.google.zxing.BarcodeFormat +import com.google.zxing.BinaryBitmap +import com.google.zxing.DecodeHintType +import com.google.zxing.MultiFormatReader +import com.google.zxing.PlanarYUVLuminanceSource +import com.google.zxing.common.HybridBinarizer +import java.nio.ByteBuffer + +class QrCodeAnalyzerImpl( + private val framePosition: FramePosition, + private val onQrCodeScanned: (String) -> Unit, +) : QrCodeAnalyzer { + private val supportedImageFormats = + listOf( + ImageFormat.YUV_420_888, + ImageFormat.YUV_422_888, + ImageFormat.YUV_444_888 + ) + + override fun analyze(image: ImageProxy) { + image.use { + if (image.format in supportedImageFormats) { + val bytes = image.planes.first().buffer.toByteArray() + + Twig.verbose { + "Scan result: " + + "Frame: $framePosition, " + + "Info: ${image.imageInfo}, " + + "Image width: ${image.width}, " + + "Image height: ${image.height}" + } + + // TODO [#1380]: Leverage FramePosition in QrCodeAnalyzer + // TODO [#1380]: https://github.com/Electric-Coin-Company/zashi-android/issues/1380 + val source = + if (image.height > image.width) { + PlanarYUVLuminanceSource( + // yuvData = + bytes, + // dataWidth = + image.width, + // dataHeight = + image.height, + // left = + (image.width * LEFT_OFFSET).toInt(), + // top = + (image.height * TOP_OFFSET).toInt(), + // width = + (image.width * WIDTH_OFFSET).toInt(), + // height = + (image.height * HEIGHT_OFFSET).toInt(), + // reverseHorizontal = + false + ) + } else { + PlanarYUVLuminanceSource( + // yuvData = + bytes, + // dataWidth = + image.width, + // dataHeight = + image.height, + // left = + (image.width * TOP_OFFSET).toInt(), + // top = + (image.height * LEFT_OFFSET).toInt(), + // width = + (image.width * HEIGHT_OFFSET).toInt(), + // height = + (image.height * WIDTH_OFFSET).toInt(), + // reverseHorizontal = + false + ) + } + + val binaryBmp = BinaryBitmap(HybridBinarizer(source)) + + Twig.verbose { + "Scan result cropped: " + + "Image width: ${binaryBmp.width}, " + + "Image height: ${binaryBmp.height}" + } + + runCatching { + val result = + MultiFormatReader().apply { + setHints( + mapOf( + DecodeHintType.POSSIBLE_FORMATS to arrayListOf(BarcodeFormat.QR_CODE), + DecodeHintType.ALSO_INVERTED to true + ) + ) + }.decodeWithState(binaryBmp) + + onQrCodeScanned(result.text) + }.onFailure { + // failed to found QR code in current frame + } + } + } + } + + private fun ByteBuffer.toByteArray(): ByteArray { + rewind() + return ByteArray(remaining()).also { + get(it) + } + } +} + +private const val LEFT_OFFSET = .15 +private const val TOP_OFFSET = .25 +private const val WIDTH_OFFSET = .7 +private const val HEIGHT_OFFSET = .45 diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/CoreModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/CoreModule.kt index 16e015bc6..e78bca9b2 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/CoreModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/CoreModule.kt @@ -10,9 +10,6 @@ import co.electriccoin.zcash.preference.model.entry.PreferenceKey import co.electriccoin.zcash.ui.NavigationRouter import co.electriccoin.zcash.ui.NavigationRouterImpl import co.electriccoin.zcash.ui.preference.PersistableWalletPreferenceDefault -import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker -import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImpl -import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.singleOf import org.koin.dsl.bind import org.koin.dsl.module @@ -36,8 +33,6 @@ val coreModule = single { BiometricManager.from(get()) } - factoryOf(::AppUpdateCheckerImpl) bind AppUpdateChecker::class - factory { AndroidConfigurationFactory.new() } singleOf(::NavigationRouterImpl) bind NavigationRouter::class diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt index c30b28ab4..55f21cbdd 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt @@ -54,7 +54,6 @@ import co.electriccoin.zcash.ui.common.usecase.SaveContactUseCase import co.electriccoin.zcash.ui.common.usecase.SelectWalletAccountUseCase import co.electriccoin.zcash.ui.common.usecase.SendEmailUseCase import co.electriccoin.zcash.ui.common.usecase.SendSupportEmailUseCase -import co.electriccoin.zcash.ui.common.usecase.SensitiveSettingsVisibleUseCase import co.electriccoin.zcash.ui.common.usecase.ShareImageUseCase import co.electriccoin.zcash.ui.common.usecase.SharePCZTUseCase import co.electriccoin.zcash.ui.common.usecase.UpdateContactUseCase @@ -105,7 +104,6 @@ val useCaseModule = factoryOf(::SendEmailUseCase) factoryOf(::SendSupportEmailUseCase) factoryOf(::IsFlexaAvailableUseCase) - factoryOf(::SensitiveSettingsVisibleUseCase) factoryOf(::ObserveWalletAccountsUseCase) factoryOf(::SelectWalletAccountUseCase) factoryOf(::ObserveSelectedWalletAccountUseCase) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt index 251de101a..13247bc9a 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt @@ -1,7 +1,6 @@ package co.electriccoin.zcash.di import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationViewModel -import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.ZashiMainTopAppBarViewModel @@ -37,8 +36,6 @@ import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel import co.electriccoin.zcash.ui.screen.signkeystonetransaction.viewmodel.SignKeystoneTransactionViewModel import co.electriccoin.zcash.ui.screen.support.viewmodel.SupportViewModel import co.electriccoin.zcash.ui.screen.transactionprogress.TransactionProgressViewModel -import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo -import co.electriccoin.zcash.ui.screen.update.viewmodel.UpdateViewModel import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel import co.electriccoin.zcash.ui.screen.whatsnew.viewmodel.WhatsNewViewModel import org.koin.androidx.viewmodel.dsl.viewModel @@ -49,7 +46,6 @@ val viewModelModule = module { viewModelOf(::WalletViewModel) viewModelOf(::AuthenticationViewModel) - viewModelOf(::CheckUpdateViewModel) viewModelOf(::HomeViewModel) viewModelOf(::TransactionHistoryViewModel) viewModelOf(::OnboardingViewModel) @@ -62,13 +58,6 @@ val viewModelModule = viewModelOf(::CreateTransactionsViewModel) viewModelOf(::RestoreSuccessViewModel) viewModelOf(::WhatsNewViewModel) - viewModel { (updateInfo: UpdateInfo) -> - UpdateViewModel( - application = get(), - updateInfo = updateInfo, - appUpdateChecker = get(), - ) - } viewModelOf(::ChooseServerViewModel) viewModelOf(::AddressBookViewModel) viewModelOf(::SelectRecipientViewModel) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt index faf1e4abe..80ebd4f50 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt @@ -45,8 +45,6 @@ import co.electriccoin.zcash.ui.common.compose.LocalNavController import co.electriccoin.zcash.ui.common.model.SerializableAddress import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider import co.electriccoin.zcash.ui.common.provider.isInForeground -import co.electriccoin.zcash.ui.configuration.ConfigurationEntries -import co.electriccoin.zcash.ui.configuration.RemoteConfig import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.enterTransition import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.exitTransition import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.popEnterTransition @@ -97,7 +95,6 @@ import co.electriccoin.zcash.ui.screen.signkeystonetransaction.AndroidSignKeysto import co.electriccoin.zcash.ui.screen.signkeystonetransaction.SignKeystoneTransaction import co.electriccoin.zcash.ui.screen.transactionprogress.AndroidTransactionProgress import co.electriccoin.zcash.ui.screen.transactionprogress.TransactionProgress -import co.electriccoin.zcash.ui.screen.update.WrapCheckForUpdate import co.electriccoin.zcash.ui.screen.warning.WrapNotEnoughSpace import co.electriccoin.zcash.ui.screen.whatsnew.WrapWhatsNew import kotlinx.coroutines.flow.StateFlow @@ -447,8 +444,6 @@ private fun MainActivity.NavigationHome( // Keep the current navigation location } ) - } else if (ConfigurationEntries.IS_APP_UPDATE_CHECK_ENABLED.getValue(RemoteConfig.current)) { - WrapCheckForUpdate() } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/SynchronizationStatus.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/SynchronizationStatus.kt index d9910edf5..43b8a0f64 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/SynchronizationStatus.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/SynchronizationStatus.kt @@ -45,7 +45,6 @@ private fun BalanceWidgetPreview() { modifier = Modifier.fillMaxWidth() ) { SynchronizationStatus( - isUpdateAvailable = false, onStatusClick = {}, walletSnapshot = WalletSnapshotFixture.new(), ) @@ -55,7 +54,6 @@ private fun BalanceWidgetPreview() { @Composable fun SynchronizationStatus( - isUpdateAvailable: Boolean, onStatusClick: (StatusAction) -> Unit, walletSnapshot: WalletSnapshot, modifier: Modifier = Modifier, @@ -65,7 +63,6 @@ fun SynchronizationStatus( WalletDisplayValues.getNextValues( context = LocalContext.current, walletSnapshot = walletSnapshot, - isUpdateAvailable = isUpdateAvailable, ) Column( diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/SensitiveSettingsVisibleUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/SensitiveSettingsVisibleUseCase.kt deleted file mode 100644 index 7bcbaf9a9..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/SensitiveSettingsVisibleUseCase.kt +++ /dev/null @@ -1,30 +0,0 @@ -package co.electriccoin.zcash.ui.common.usecase - -import android.content.Context -import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT -import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.WhileSubscribed -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - -class SensitiveSettingsVisibleUseCase( - appUpdateChecker: AppUpdateChecker, - context: Context -) { - private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) - - private val flow = - appUpdateChecker.newCheckForUpdateAvailabilityFlow(context) - .map { it.isForce.not() } - .stateIn( - scope = scope, - started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), - initialValue = true - ) - - operator fun invoke() = flow -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/CheckUpdateViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/CheckUpdateViewModel.kt deleted file mode 100644 index b2527f307..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/CheckUpdateViewModel.kt +++ /dev/null @@ -1,27 +0,0 @@ -package co.electriccoin.zcash.ui.common.viewmodel - -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.viewModelScope -import cash.z.ecc.android.sdk.ext.onFirst -import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker -import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch - -class CheckUpdateViewModel( - application: Application, - private val appUpdateChecker: AppUpdateChecker -) : AndroidViewModel(application) { - val updateInfo: MutableStateFlow = MutableStateFlow(null) - - fun checkForAppUpdate() { - viewModelScope.launch { - appUpdateChecker.newCheckForUpdateAvailabilityFlow( - getApplication() - ).onFirst { newInfo -> - updateInfo.value = newInfo - } - } - } -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/configuration/ConfigurationEntries.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/configuration/ConfigurationEntries.kt index a1fe9d520..ccd4c0a66 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/configuration/ConfigurationEntries.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/configuration/ConfigurationEntries.kt @@ -4,8 +4,6 @@ import co.electriccoin.zcash.configuration.model.entry.BooleanConfigurationEntry import co.electriccoin.zcash.configuration.model.entry.ConfigKey object ConfigurationEntries { - val IS_APP_UPDATE_CHECK_ENABLED = BooleanConfigurationEntry(ConfigKey("is_update_check_enabled"), true) - /* * A troubleshooting step. If we fix our bugs, this should be unnecessary. */ diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/fixture/UpdateInfoFixture.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/fixture/UpdateInfoFixture.kt deleted file mode 100644 index a6da414dc..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/fixture/UpdateInfoFixture.kt +++ /dev/null @@ -1,24 +0,0 @@ -package co.electriccoin.zcash.ui.fixture - -import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker -import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo -import co.electriccoin.zcash.ui.screen.update.model.UpdateState -import com.google.android.play.core.appupdate.AppUpdateInfo - -object UpdateInfoFixture { - val INITIAL_PRIORITY = AppUpdateChecker.Priority.LOW - val INITIAL_STATE = UpdateState.Prepared - const val INITIAL_FORCE = false - - fun new( - priority: AppUpdateChecker.Priority = INITIAL_PRIORITY, - force: Boolean = INITIAL_FORCE, - appUpdateInfo: AppUpdateInfo? = null, - state: UpdateState = INITIAL_STATE - ) = UpdateInfo( - priority, - force, - appUpdateInfo, - state - ) -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/account/view/HistoryView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/account/view/HistoryView.kt index 1185bdf7b..09333d3c6 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/account/view/HistoryView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/account/view/HistoryView.kt @@ -154,7 +154,6 @@ internal fun HistoryContainer( // Do not calculate and use the app update information here, as the sync bar won't be displayed after // the wallet is fully restored SynchronizationStatus( - isUpdateAvailable = false, onStatusClick = onStatusClick, testTag = BalancesTag.STATUS, walletSnapshot = walletSnapshot, diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/viewmodel/AdvancedSettingsViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/viewmodel/AdvancedSettingsViewModel.kt index e8717319b..0a44b1dbb 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/viewmodel/AdvancedSettingsViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/advancedsettings/viewmodel/AdvancedSettingsViewModel.kt @@ -1,39 +1,24 @@ package co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT import co.electriccoin.zcash.ui.NavigationRouter import co.electriccoin.zcash.ui.NavigationTargets import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.common.usecase.SensitiveSettingsVisibleUseCase import co.electriccoin.zcash.ui.design.component.ButtonState import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.screen.advancedsettings.model.AdvancedSettingsState import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.WhileSubscribed -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.asStateFlow class AdvancedSettingsViewModel( - isSensitiveSettingsVisible: SensitiveSettingsVisibleUseCase, private val navigationRouter: NavigationRouter, ) : ViewModel() { - val state: StateFlow = - isSensitiveSettingsVisible() - .map { isSensitiveSettingsVisible -> - createState(isSensitiveSettingsVisible) - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), - initialValue = createState(isSensitiveSettingsVisible().value) - ) + val state: StateFlow = MutableStateFlow(createState()).asStateFlow() - private fun createState(isSensitiveSettingsVisible: Boolean) = + private fun createState() = AdvancedSettingsState( onBack = ::onBack, items = @@ -53,13 +38,13 @@ class AdvancedSettingsViewModel( icon = R.drawable.ic_advanced_settings_choose_server, onClick = ::onChooseServerClick - ).takeIf { isSensitiveSettingsVisible }, + ), ZashiListItemState( title = stringRes(R.string.advanced_settings_currency_conversion), icon = R.drawable.ic_advanced_settings_currency_conversion, onClick = ::onCurrencyConversionClick - ).takeIf { isSensitiveSettingsVisible } + ) ).toImmutableList(), deleteButton = ButtonState( diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/AndroidBalances.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/AndroidBalances.kt index e849d560f..631854b60 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/AndroidBalances.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/AndroidBalances.kt @@ -31,7 +31,6 @@ import co.electriccoin.zcash.ui.common.model.ZashiAccount import co.electriccoin.zcash.ui.common.usecase.CreateKeystoneShieldProposalUseCase import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase import co.electriccoin.zcash.ui.common.usecase.GetZashiSpendingKeyUseCase -import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.ZashiMainTopAppBarViewModel @@ -44,7 +43,6 @@ import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransact import co.electriccoin.zcash.ui.screen.support.model.SupportInfo import co.electriccoin.zcash.ui.screen.support.model.SupportInfoType import co.electriccoin.zcash.ui.screen.support.viewmodel.SupportViewModel -import co.electriccoin.zcash.ui.screen.update.model.UpdateState import co.electriccoin.zcash.ui.util.EmailUtil import co.electriccoin.zcash.ui.util.PlayStoreUtil import kotlinx.coroutines.CoroutineScope @@ -73,8 +71,6 @@ internal fun WrapBalances() { val isHideBalances = homeViewModel.isHideBalances.collectAsStateWithLifecycle().value ?: false - val checkUpdateViewModel = koinActivityViewModel() - val balanceState = walletViewModel.balanceState.collectAsStateWithLifecycle().value val supportInfo = supportViewModel.supportInfo.collectAsStateWithLifecycle().value @@ -86,7 +82,6 @@ internal fun WrapBalances() { WrapBalances( balanceState = balanceState, createTransactionsViewModel = createTransactionsViewModel, - checkUpdateViewModel = checkUpdateViewModel, isHideBalances = isHideBalances, lifecycleScope = activity.lifecycleScope, supportInfo = supportInfo, @@ -105,7 +100,6 @@ const val DEFAULT_SHIELDING_THRESHOLD = 100000L @Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod") internal fun WrapBalances( balanceState: BalanceState, - checkUpdateViewModel: CheckUpdateViewModel, createTransactionsViewModel: CreateTransactionsViewModel, lifecycleScope: CoroutineScope, isHideBalances: Boolean, @@ -121,12 +115,6 @@ internal fun WrapBalances( val snackbarHostState = remember { SnackbarHostState() } - // To show information about the app update, if available - val isUpdateAvailable = - checkUpdateViewModel.updateInfo.collectAsStateWithLifecycle().value.let { - it?.appUpdateInfo != null && it.state == UpdateState.Prepared - } - val (shieldState, setShieldState) = rememberSaveable(walletSnapshot?.isZashi, stateSaver = ShieldState.Saver) { mutableStateOf(ShieldState.None) } @@ -170,7 +158,6 @@ internal fun WrapBalances( Balances( balanceState = balanceState, isHideBalances = isHideBalances, - isUpdateAvailable = isUpdateAvailable, isShowingErrorDialog = isShowingErrorDialog, setShowErrorDialog = setShowErrorDialog, showStatusDialog = showStatusDialog.value, diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/model/WalletDisplayValues.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/model/WalletDisplayValues.kt index a94c5c1fd..6ec5683d4 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/model/WalletDisplayValues.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/model/WalletDisplayValues.kt @@ -26,7 +26,6 @@ data class WalletDisplayValues( internal fun getNextValues( context: Context, walletSnapshot: WalletSnapshot, - isUpdateAvailable: Boolean = false, ): WalletDisplayValues { var progress = PercentDecimal.ZERO_PERCENT val zecAmountText = walletSnapshot.totalBalance().toZecString() @@ -55,17 +54,8 @@ data class WalletDisplayValues( statusAction = StatusAction.Syncing } Synchronizer.Status.SYNCED -> { - if (isUpdateAvailable) { - statusText = - context.getString( - R.string.balances_status_update, - context.getString(R.string.app_name) - ) - statusAction = StatusAction.AppUpdate - } else { - statusText = context.getString(R.string.balances_status_synced) - statusAction = StatusAction.Synced - } + statusText = context.getString(R.string.balances_status_synced) + statusAction = StatusAction.Synced } Synchronizer.Status.DISCONNECTED -> { statusText = diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/view/BalancesView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/view/BalancesView.kt index a6ef9dda8..34d14d0db 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/view/BalancesView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/view/BalancesView.kt @@ -35,7 +35,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontStyle @@ -90,7 +89,6 @@ private fun ComposableBalancesPreview() { Balances( balanceState = BalanceStateFixture.new(), isHideBalances = false, - isUpdateAvailable = false, isShowingErrorDialog = false, hideStatusDialog = {}, showStatusDialog = null, @@ -114,7 +112,6 @@ private fun ComposableBalancesShieldDarkPreview() { Balances( balanceState = BalanceStateFixture.new(), isHideBalances = false, - isUpdateAvailable = false, isShowingErrorDialog = true, hideStatusDialog = {}, showStatusDialog = null, @@ -150,7 +147,6 @@ private fun ComposableBalancesShieldErrorDialogPreview() { fun Balances( balanceState: BalanceState, isHideBalances: Boolean, - isUpdateAvailable: Boolean, isShowingErrorDialog: Boolean, hideStatusDialog: () -> Unit, onContactSupport: (String?) -> Unit, @@ -178,7 +174,6 @@ fun Balances( BalancesMainContent( balanceState = balanceState, isHideBalances = isHideBalances, - isUpdateAvailable = isUpdateAvailable, onShielding = onShielding, onStatusClick = onStatusClick, walletSnapshot = walletSnapshot, @@ -287,7 +282,6 @@ fun ShieldingErrorGrpcDialog(onDone: () -> Unit) { private fun BalancesMainContent( balanceState: BalanceState, isHideBalances: Boolean, - isUpdateAvailable: Boolean, onShielding: () -> Unit, onStatusClick: (StatusAction) -> Unit, walletSnapshot: WalletSnapshot, @@ -355,7 +349,6 @@ private fun BalancesMainContent( } SynchronizationStatus( - isUpdateAvailable = isUpdateAvailable, onStatusClick = onStatusClick, testTag = BalancesTag.STATUS, walletSnapshot = walletSnapshot, diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/util/QrCodeAnalyzer.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/util/QrCodeAnalyzer.kt index 09797e421..ac9095cd0 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/util/QrCodeAnalyzer.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/util/QrCodeAnalyzer.kt @@ -1,121 +1,5 @@ package co.electriccoin.zcash.ui.screen.scan.util -import android.graphics.ImageFormat import androidx.camera.core.ImageAnalysis -import androidx.camera.core.ImageProxy -import co.electriccoin.zcash.spackle.Twig -import co.electriccoin.zcash.ui.screen.scankeystone.view.FramePosition -import com.google.zxing.BarcodeFormat -import com.google.zxing.BinaryBitmap -import com.google.zxing.DecodeHintType -import com.google.zxing.MultiFormatReader -import com.google.zxing.PlanarYUVLuminanceSource -import com.google.zxing.common.HybridBinarizer -import java.nio.ByteBuffer -class QrCodeAnalyzer( - private val framePosition: FramePosition, - private val onQrCodeScanned: (String) -> Unit, -) : ImageAnalysis.Analyzer { - private val supportedImageFormats = - listOf( - ImageFormat.YUV_420_888, - ImageFormat.YUV_422_888, - ImageFormat.YUV_444_888 - ) - - override fun analyze(image: ImageProxy) { - image.use { - if (image.format in supportedImageFormats) { - val bytes = image.planes.first().buffer.toByteArray() - - Twig.verbose { - "Scan result: " + - "Frame: $framePosition, " + - "Info: ${image.imageInfo}, " + - "Image width: ${image.width}, " + - "Image height: ${image.height}" - } - - // TODO [#1380]: Leverage FramePosition in QrCodeAnalyzer - // TODO [#1380]: https://github.com/Electric-Coin-Company/zashi-android/issues/1380 - val source = - if (image.height > image.width) { - PlanarYUVLuminanceSource( - // yuvData = - bytes, - // dataWidth = - image.width, - // dataHeight = - image.height, - // left = - (image.width * LEFT_OFFSET).toInt(), - // top = - (image.height * TOP_OFFSET).toInt(), - // width = - (image.width * WIDTH_OFFSET).toInt(), - // height = - (image.height * HEIGHT_OFFSET).toInt(), - // reverseHorizontal = - false - ) - } else { - PlanarYUVLuminanceSource( - // yuvData = - bytes, - // dataWidth = - image.width, - // dataHeight = - image.height, - // left = - (image.width * TOP_OFFSET).toInt(), - // top = - (image.height * LEFT_OFFSET).toInt(), - // width = - (image.width * HEIGHT_OFFSET).toInt(), - // height = - (image.height * WIDTH_OFFSET).toInt(), - // reverseHorizontal = - false - ) - } - - val binaryBmp = BinaryBitmap(HybridBinarizer(source)) - - Twig.verbose { - "Scan result cropped: " + - "Image width: ${binaryBmp.width}, " + - "Image height: ${binaryBmp.height}" - } - - runCatching { - val result = - MultiFormatReader().apply { - setHints( - mapOf( - DecodeHintType.POSSIBLE_FORMATS to arrayListOf(BarcodeFormat.QR_CODE), - DecodeHintType.ALSO_INVERTED to true - ) - ) - }.decodeWithState(binaryBmp) - - onQrCodeScanned(result.text) - }.onFailure { - // failed to found QR code in current frame - } - } - } - } - - private fun ByteBuffer.toByteArray(): ByteArray { - rewind() - return ByteArray(remaining()).also { - get(it) - } - } -} - -private const val LEFT_OFFSET = .15 -private const val TOP_OFFSET = .25 -private const val WIDTH_OFFSET = .7 -private const val HEIGHT_OFFSET = .45 +interface QrCodeAnalyzer : ImageAnalysis.Analyzer diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/view/ScanView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/view/ScanView.kt index 2fd166e93..8c3770a36 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/view/ScanView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/view/ScanView.kt @@ -88,7 +88,7 @@ import co.electriccoin.zcash.ui.screen.scan.ScanTag import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState import co.electriccoin.zcash.ui.screen.scan.model.ScanValidationState import co.electriccoin.zcash.ui.screen.scan.util.ImageUriToQrCodeConverter -import co.electriccoin.zcash.ui.screen.scan.util.MlkitQrCodeAnalyzer +import co.electriccoin.zcash.ui.screen.scan.util.QrCodeAnalyzerImpl import co.electriccoin.zcash.ui.screen.scankeystone.view.CAMERA_TRANSLUCENT_BORDER import co.electriccoin.zcash.ui.screen.scankeystone.view.FramePosition import com.google.accompanist.permissions.ExperimentalPermissionsApi @@ -709,7 +709,7 @@ fun ImageAnalysis.qrCodeFlow(framePosition: FramePosition): Flow { callbackFlow { setAnalyzer( ContextCompat.getMainExecutor(context), - MlkitQrCodeAnalyzer( + QrCodeAnalyzerImpl( framePosition = framePosition, onQrCodeScanned = { result -> Twig.debug { "Scan result onQrCodeScanned: $result" } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scankeystone/view/ScanKeystoneView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scankeystone/view/ScanKeystoneView.kt index 5d0776631..668f4043c 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scankeystone/view/ScanKeystoneView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scankeystone/view/ScanKeystoneView.kt @@ -88,7 +88,7 @@ import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.screen.scan.ScanTag import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState import co.electriccoin.zcash.ui.screen.scan.model.ScanValidationState -import co.electriccoin.zcash.ui.screen.scan.util.MlkitQrCodeAnalyzer +import co.electriccoin.zcash.ui.screen.scan.util.QrCodeAnalyzerImpl import co.electriccoin.zcash.ui.screen.scankeystone.model.ScanKeystoneState import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState @@ -730,7 +730,7 @@ fun ImageAnalysis.qrCodeFlow(framePosition: FramePosition): Flow { callbackFlow { setAnalyzer( ContextCompat.getMainExecutor(context), - MlkitQrCodeAnalyzer( + QrCodeAnalyzerImpl( framePosition = framePosition, onQrCodeScanned = { result -> Twig.debug { "Scan result onQrCodeScanned: $result" } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/viewmodel/SettingsViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/viewmodel/SettingsViewModel.kt index fec617486..f6db78995 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/viewmodel/SettingsViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/viewmodel/SettingsViewModel.kt @@ -22,7 +22,6 @@ import co.electriccoin.zcash.ui.common.usecase.NavigateToAddressBookUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedWalletAccountUseCase import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase -import co.electriccoin.zcash.ui.common.usecase.SensitiveSettingsVisibleUseCase import co.electriccoin.zcash.ui.configuration.ConfigurationEntries import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState import co.electriccoin.zcash.ui.design.util.stringRes @@ -44,7 +43,6 @@ import kotlinx.coroutines.launch @Suppress("TooManyFunctions") class SettingsViewModel( observeConfiguration: ObserveConfigurationUseCase, - isSensitiveSettingsVisible: SensitiveSettingsVisibleUseCase, observeSelectedWalletAccount: ObserveSelectedWalletAccountUseCase, isFlexaAvailable: IsFlexaAvailableUseCase, isCoinbaseAvailable: IsCoinbaseAvailableUseCase, @@ -104,14 +102,12 @@ class SettingsViewModel( combine( troubleshootingState, observeSelectedWalletAccount(), - isSensitiveSettingsVisible(), isFlexaAvailable.observe(), isCoinbaseAvailable.observe() - ) { troubleshootingState, account, isSensitiveSettingsVisible, isFlexaAvailable, isCoinbaseAvailable -> + ) { troubleshootingState, account, isFlexaAvailable, isCoinbaseAvailable -> createState( selectedAccount = account, troubleshootingState = troubleshootingState, - isSensitiveSettingsVisible = isSensitiveSettingsVisible, isFlexaAvailable = isFlexaAvailable == true, isCoinbaseAvailable = isCoinbaseAvailable == true ) @@ -122,7 +118,6 @@ class SettingsViewModel( createState( selectedAccount = null, troubleshootingState = null, - isSensitiveSettingsVisible = isSensitiveSettingsVisible().value, isFlexaAvailable = isFlexaAvailable.observe().value == true, isCoinbaseAvailable = isCoinbaseAvailable.observe().value == true ) @@ -131,7 +126,6 @@ class SettingsViewModel( private fun createState( selectedAccount: WalletAccount?, troubleshootingState: SettingsTroubleshootingState?, - isSensitiveSettingsVisible: Boolean, isFlexaAvailable: Boolean, isCoinbaseAvailable: Boolean, ) = SettingsState( @@ -171,7 +165,7 @@ class SettingsViewModel( null -> R.drawable.ic_integrations_flexa }.takeIf { isFlexaAvailable } ).toImmutableList() - ).takeIf { isSensitiveSettingsVisible }, + ), ZashiListItemState( title = stringRes(R.string.settings_advanced_settings), icon = R.drawable.ic_advanced_settings, diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AndroidUpdate.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AndroidUpdate.kt deleted file mode 100644 index 7a8cd3941..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AndroidUpdate.kt +++ /dev/null @@ -1,142 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update - -import android.content.Context -import androidx.activity.compose.BackHandler -import androidx.annotation.VisibleForTesting -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import co.electriccoin.zcash.di.koinActivityViewModel -import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.common.compose.LocalActivity -import co.electriccoin.zcash.ui.common.compose.LocalNavController -import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel -import co.electriccoin.zcash.ui.navigateJustOnce -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.view.Update -import co.electriccoin.zcash.ui.screen.update.viewmodel.UpdateViewModel -import co.electriccoin.zcash.ui.util.PlayStoreUtil -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import org.koin.core.parameter.parametersOf - -@Composable -internal fun WrapCheckForUpdate() { - // TODO [#403]: Manual testing of already implemented in-app update mechanisms - // TODO [#403]: https://github.com/Electric-Coin-Company/zashi-android/issues/403 - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - val checkUpdateViewModel = koinActivityViewModel() - - // Check for an app update asynchronously. We create an effect that matches the activity - // lifecycle. If the wrapping compose recomposes, the check shouldn't run again. - LaunchedEffect(true) { - checkUpdateViewModel.checkForAppUpdate() - } - - val activity = LocalActivity.current - - val inputUpdateInfo = checkUpdateViewModel.updateInfo.collectAsStateWithLifecycle().value ?: return - - val viewModel = koinActivityViewModel { parametersOf(inputUpdateInfo) } - val updateInfo = viewModel.updateInfo.collectAsStateWithLifecycle().value - val navController = LocalNavController.current - - if (updateInfo.appUpdateInfo != null && updateInfo.state == UpdateState.Prepared) { - WrapUpdate( - updateInfo = updateInfo, - checkForUpdate = viewModel::checkForAppUpdate, - remindLater = viewModel::remindLater, - goForUpdate = { - viewModel.goForUpdate( - activity = activity, - appUpdateInfo = updateInfo.appUpdateInfo - ) - }, - onSettings = { - navController.navigateJustOnce(SETTINGS) - } - ) - } -} - -@VisibleForTesting -@Composable -internal fun WrapUpdate( - updateInfo: UpdateInfo, - checkForUpdate: () -> Unit, - remindLater: () -> Unit, - goForUpdate: () -> Unit, - onSettings: () -> Unit -) { - val activity = LocalActivity.current - - val snackbarHostState = remember { SnackbarHostState() } - val scope = rememberCoroutineScope() - - when (updateInfo.state) { - UpdateState.Done, UpdateState.Canceled -> { - // just return as we are already in Home compose - return - } - - UpdateState.Failed -> { - // we need to refresh AppUpdateInfo object, as it can be used only once - checkForUpdate() - } - - UpdateState.Prepared, UpdateState.Running -> { - // valid stages - } - } - - val onLaterAction = { - if (!updateInfo.isForce && updateInfo.state != UpdateState.Running) { - remindLater() - } - } - - BackHandler { - onLaterAction() - } - - Update( - snackbarHostState, - updateInfo, - onDownload = { - // in this state of the update we have the AppUpdateInfo filled - requireNotNull(updateInfo.appUpdateInfo) - goForUpdate() - }, - onLater = onLaterAction, - onReference = { - openPlayStoreAppSite( - activity.applicationContext, - snackbarHostState, - scope - ) - }, - onSettings = onSettings - ) -} - -private fun openPlayStoreAppSite( - context: Context, - snackbarHostState: SnackbarHostState, - scope: CoroutineScope -) { - val storeIntent = PlayStoreUtil.newActivityIntent(context) - runCatching { - context.startActivity(storeIntent) - }.onFailure { - scope.launch { - snackbarHostState.showSnackbar( - message = context.getString(R.string.unable_to_open_play_store) - ) - } - } -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AppUpdateChecker.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AppUpdateChecker.kt deleted file mode 100644 index ed1383b88..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AppUpdateChecker.kt +++ /dev/null @@ -1,55 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update - -import android.content.Context -import androidx.activity.ComponentActivity -import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo -import com.google.android.play.core.appupdate.AppUpdateInfo -import kotlinx.coroutines.flow.Flow - -interface AppUpdateChecker { - val stalenessDays: Int - - enum class Priority { - LOW { - override fun priorityUpperBorder() = 1 - - override fun belongs(actualPriority: Int) = actualPriority <= this.priorityUpperBorder() - }, - MEDIUM { - override fun priorityUpperBorder() = 3 - - override fun belongs(actualPriority: Int) = - actualPriority > LOW.priorityUpperBorder() && actualPriority <= this.priorityUpperBorder() - }, - HIGH { - override fun priorityUpperBorder() = 5 - - override fun belongs(actualPriority: Int) = - actualPriority > MEDIUM.priorityUpperBorder() && actualPriority <= this.priorityUpperBorder() - }; - - abstract fun priorityUpperBorder(): Int - - abstract fun belongs(actualPriority: Int): Boolean - } - - fun getPriority(inAppUpdatePriority: Int): Priority { - return when { - Priority.LOW.belongs(inAppUpdatePriority) -> Priority.LOW - Priority.MEDIUM.belongs(inAppUpdatePriority) -> Priority.MEDIUM - Priority.HIGH.belongs(inAppUpdatePriority) -> Priority.HIGH - else -> Priority.LOW - } - } - - fun isHighPriority(inAppUpdatePriority: Int): Boolean { - return getPriority(inAppUpdatePriority) == Priority.HIGH - } - - fun newCheckForUpdateAvailabilityFlow(context: Context): Flow - - fun newStartUpdateFlow( - activity: ComponentActivity, - appUpdateInfo: AppUpdateInfo - ): Flow -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AppUpdateCheckerImpl.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AppUpdateCheckerImpl.kt deleted file mode 100644 index ec4c6366c..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AppUpdateCheckerImpl.kt +++ /dev/null @@ -1,131 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update - -import android.content.Context -import androidx.activity.ComponentActivity -import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo -import co.electriccoin.zcash.ui.screen.update.model.UpdateState -import com.google.android.play.core.appupdate.AppUpdateInfo -import com.google.android.play.core.appupdate.AppUpdateManagerFactory -import com.google.android.play.core.appupdate.AppUpdateOptions -import com.google.android.play.core.install.model.ActivityResult -import com.google.android.play.core.install.model.AppUpdateType -import com.google.android.play.core.install.model.UpdateAvailability -import kotlinx.coroutines.channels.ProducerScope -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow - -class AppUpdateCheckerImpl : AppUpdateChecker { - override val stalenessDays = DEFAULT_STALENESS_DAYS - - /** - * This function checks available app update released on Google Play. It returns UpdateInfo object - * encapsulated in Flow in case of high priority update or in case of staleness days passed. - * - * For setting up the PRIORITY of an update in Google Play - * https://developer.android.com/guide/playcore/in-app-updates/kotlin-java#update-priority. - * - * @param context - * - * @return UpdateInfo object encapsulated in Flow in case of conditions succeeded - */ - override fun newCheckForUpdateAvailabilityFlow(context: Context): Flow = - callbackFlow { - val appUpdateInfoTask = AppUpdateManagerFactory.create(context.applicationContext).appUpdateInfo - - appUpdateInfoTask.addOnCompleteListener { infoTask -> - if (!infoTask.isSuccessful) { - emitFailure(this) - return@addOnCompleteListener - } - - val appUpdateInfo = infoTask.result - if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && - appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) - ) { - // We force user to update immediately in case of high priority - // or in case of staleness days passed - if (isHighPriority(appUpdateInfo.updatePriority()) || - (appUpdateInfo.clientVersionStalenessDays() ?: -1) >= stalenessDays - ) { - emitSuccess(this, infoTask.result, UpdateState.Prepared) - } else { - emitSuccess(this, infoTask.result, UpdateState.Done) - } - } else { - // Return Done in case of no update available - emitSuccess(this, infoTask.result, UpdateState.Done) - } - } - awaitClose { - // No resources to release - } - } - - private fun emitSuccess( - producerScope: ProducerScope, - info: AppUpdateInfo, - state: UpdateState - ) { - producerScope.trySend( - UpdateInfo( - getPriority(info.updatePriority()), - isHighPriority(info.updatePriority()), - info, - state - ) - ) - } - - private fun emitFailure(producerScope: ProducerScope) { - producerScope.trySend( - UpdateInfo( - AppUpdateChecker.Priority.LOW, - false, - null, - UpdateState.Failed - ) - ) - } - - /** - * This function is used for triggering in-app update with IMMEDIATE app update type. - * - * The immediate update can result with these values: - * Activity.RESULT_OK: The user accepted and the update succeeded (which, in practice, your app - * never should never receive because it already updated). - * Activity.RESULT_CANCELED: The user denied or canceled the update. - * ActivityResult.RESULT_IN_APP_UPDATE_FAILED: The flow failed either during the user confirmation, - * the download, or the installation. - * - * @param activity - * @param appUpdateInfo object is necessary for starting the update process, - * for getting it see {@link #checkForUpdateAvailability()} - */ - override fun newStartUpdateFlow( - activity: ComponentActivity, - appUpdateInfo: AppUpdateInfo - ): Flow = - callbackFlow { - val appUpdateResultTask = - AppUpdateManagerFactory.create(activity).startUpdateFlow( - appUpdateInfo, - activity, - AppUpdateOptions.defaultOptions(AppUpdateType.IMMEDIATE) - ) - - appUpdateResultTask.addOnCompleteListener { resultTask -> - if (resultTask.isSuccessful) { - trySend(resultTask.result) - } else { - trySend(ActivityResult.RESULT_IN_APP_UPDATE_FAILED) - } - } - - awaitClose { - // No resources to release - } - } -} - -private const val DEFAULT_STALENESS_DAYS = 3 diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AppUpdateCheckerMock.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AppUpdateCheckerMock.kt deleted file mode 100644 index 1ffc84cc1..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AppUpdateCheckerMock.kt +++ /dev/null @@ -1,87 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update - -import android.app.Activity -import android.content.Context -import androidx.activity.ComponentActivity -import co.electriccoin.zcash.spackle.getPackageInfoCompat -import co.electriccoin.zcash.spackle.versionCodeCompat -import co.electriccoin.zcash.ui.fixture.UpdateInfoFixture -import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo -import co.electriccoin.zcash.ui.screen.update.model.UpdateState -import com.google.android.play.core.appupdate.AppUpdateInfo -import com.google.android.play.core.appupdate.testing.FakeAppUpdateManager -import com.google.android.play.core.install.model.AppUpdateType -import kotlinx.coroutines.channels.ProducerScope -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.flow -import kotlin.time.Duration.Companion.milliseconds - -class AppUpdateCheckerMock : AppUpdateChecker { - companion object { - private const val DEFAULT_STALENESS_DAYS = 3 - - // Used mostly for tests - val resultUpdateInfo = - UpdateInfoFixture.new( - appUpdateInfo = null, - state = UpdateState.Prepared, - priority = AppUpdateChecker.Priority.LOW, - force = false - ) - } - - override val stalenessDays = DEFAULT_STALENESS_DAYS - - override fun newCheckForUpdateAvailabilityFlow(context: Context): Flow = - callbackFlow { - val fakeAppUpdateManager = - FakeAppUpdateManager(context.applicationContext).also { - it.setClientVersionStalenessDays(stalenessDays) - it.setUpdateAvailable( - context.packageManager.getPackageInfoCompat(context.packageName, 0L).versionCodeCompat.toInt(), - AppUpdateType.IMMEDIATE - ) - it.setUpdatePriority(resultUpdateInfo.priority.priorityUpperBorder()) - } - - val appUpdateInfoTask = fakeAppUpdateManager.appUpdateInfo - - // To simulate a real-world situation - delay(100.milliseconds) - - appUpdateInfoTask.addOnCompleteListener { infoTask -> - emitResult(this, infoTask.result) - } - - awaitClose { - // No resources to release - } - } - - private fun emitResult( - producerScope: ProducerScope, - info: AppUpdateInfo - ) { - producerScope.trySend( - UpdateInfoFixture.new( - getPriority(info.updatePriority()), - isHighPriority(info.updatePriority()), - info, - resultUpdateInfo.state - ) - ) - } - - override fun newStartUpdateFlow( - activity: ComponentActivity, - appUpdateInfo: AppUpdateInfo - ): Flow = - flow { - // To simulate a real-world situation - delay(4000.milliseconds) - emit(Activity.RESULT_OK) - } -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/UpdateTag.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/UpdateTag.kt deleted file mode 100644 index 802682b06..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/UpdateTag.kt +++ /dev/null @@ -1,9 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update - -/** - * These are only used for automated testing. - */ -object UpdateTag { - const val BTN_LATER = "btn_later" - const val BTN_DOWNLOAD = "btn_download" -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/model/UpdateInfo.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/model/UpdateInfo.kt deleted file mode 100644 index 199c70948..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/model/UpdateInfo.kt +++ /dev/null @@ -1,19 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update.model - -import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker -import com.google.android.play.core.appupdate.AppUpdateInfo - -// UpdateInfo can be refactored once to have stronger representation invariants -// (eliminate the null, priority + failed state probably doesn't have much meaning, etc). -// -// sealed class UpdateInfo { -// data class Success(priority, info, state) : UpdateInfo() -// object Failed : UpdateInfo() -// } - -data class UpdateInfo( - val priority: AppUpdateChecker.Priority, - val isForce: Boolean, - val appUpdateInfo: AppUpdateInfo?, - val state: UpdateState -) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/model/UpdateState.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/model/UpdateState.kt deleted file mode 100644 index c8ded8b92..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/model/UpdateState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update.model - -enum class UpdateState { - Prepared, - Running, - Failed, - Done, - Canceled -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/view/UpdateView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/view/UpdateView.kt deleted file mode 100644 index ebbbd2cd1..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/view/UpdateView.kt +++ /dev/null @@ -1,198 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update.view - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -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.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.LinkAnnotation -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.withLink -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.dp -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.design.component.ZashiButton -import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar -import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarCloseNavigation -import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarHamburgerNavigation -import co.electriccoin.zcash.ui.design.component.zashiVerticalGradient -import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors -import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography -import co.electriccoin.zcash.ui.design.util.scaffoldPadding -import co.electriccoin.zcash.ui.fixture.UpdateInfoFixture -import co.electriccoin.zcash.ui.screen.update.UpdateTag.BTN_DOWNLOAD -import co.electriccoin.zcash.ui.screen.update.UpdateTag.BTN_LATER -import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo -import co.electriccoin.zcash.ui.screen.update.model.UpdateState - -@Composable -fun Update( - snackbarHostState: SnackbarHostState, - updateInfo: UpdateInfo, - onDownload: (state: UpdateState) -> Unit, - onLater: () -> Unit, - onReference: () -> Unit, - onSettings: () -> Unit -) { - Box( - modifier = - Modifier.background( - zashiVerticalGradient( - if (updateInfo.isForce) { - ZashiColors.Utility.WarningYellow.utilityOrange100 - } else { - ZashiColors.Utility.Purple.utilityPurple100 - } - ) - ) - ) { - Scaffold( - topBar = { - ZashiSmallTopAppBar( - title = null, - subtitle = null, - colors = ZcashTheme.colors.topAppBarColors.copyColors(containerColor = Color.Transparent), - navigationAction = { - if (updateInfo.isForce.not()) { - ZashiTopAppBarCloseNavigation(modifier = Modifier.testTag(BTN_LATER), onBack = onLater) - } - }, - hamburgerMenuActions = { - if (updateInfo.isForce) { - ZashiTopAppBarHamburgerNavigation(onSettings) - } - } - ) - }, - snackbarHost = { - SnackbarHost(snackbarHostState) - }, - containerColor = Color.Transparent - ) { - Column(modifier = Modifier.scaffoldPadding(it)) { - @Suppress("MagicNumber") - Spacer(Modifier.weight(.75f)) - Image( - modifier = Modifier.align(Alignment.CenterHorizontally), - painter = - painterResource( - if (updateInfo.isForce) { - R.drawable.ic_update_required - } else { - R.drawable.ic_update - } - ), - contentDescription = null - ) - Spacer(Modifier.height(24.dp)) - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = - if (updateInfo.isForce) { - stringResource(id = R.string.update_title_required) - } else { - stringResource(id = R.string.update_title_available) - }, - style = ZashiTypography.header6, - fontWeight = FontWeight.SemiBold, - color = ZashiColors.Text.textPrimary - ) - Spacer(Modifier.height(12.dp)) - - Text( - modifier = Modifier.fillMaxWidth(), - text = - buildAnnotatedString { - append( - if (updateInfo.isForce) { - stringResource(id = R.string.update_description_required) - } else { - stringResource(id = R.string.update_description_available) - } - ) - appendLine() - appendLine() - - withStyle( - style = - SpanStyle( - textDecoration = TextDecoration.Underline - ) - ) { - withLink( - LinkAnnotation.Clickable(CLICKABLE_TAG) { - if (updateInfo.state != UpdateState.Running) { - onReference() - } - } - ) { - append(stringResource(R.string.update_link_text)) - } - } - }, - style = ZashiTypography.textSm, - textAlign = TextAlign.Center, - color = ZashiColors.Text.textPrimary, - ) - Spacer(Modifier.weight(1f)) - ZashiButton( - modifier = Modifier.fillMaxWidth().testTag(BTN_DOWNLOAD), - text = stringResource(R.string.update_download_button), - onClick = { onDownload(UpdateState.Running) }, - enabled = updateInfo.state != UpdateState.Running, - isLoading = updateInfo.state == UpdateState.Running - ) - } - } - } -} - -@PreviewScreens -@Composable -private fun UpdatePreview() = - ZcashTheme { - Update( - snackbarHostState = SnackbarHostState(), - updateInfo = UpdateInfoFixture.new(appUpdateInfo = null), - onDownload = {}, - onLater = {}, - onReference = {}, - onSettings = {} - ) - } - -@PreviewScreens -@Composable -private fun UpdateRequiredPreview() = - ZcashTheme { - Update( - snackbarHostState = SnackbarHostState(), - updateInfo = UpdateInfoFixture.new(force = true), - onDownload = {}, - onLater = {}, - onReference = {}, - onSettings = {} - ) - } - -private const val CLICKABLE_TAG = "clickable" diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/viewmodel/UpdateViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/viewmodel/UpdateViewModel.kt deleted file mode 100644 index 59b6c7440..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/viewmodel/UpdateViewModel.kt +++ /dev/null @@ -1,66 +0,0 @@ -package co.electriccoin.zcash.ui.screen.update.viewmodel - -import android.app.Activity -import android.app.Application -import androidx.activity.ComponentActivity -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.viewModelScope -import cash.z.ecc.android.sdk.ext.onFirst -import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker -import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo -import co.electriccoin.zcash.ui.screen.update.model.UpdateState -import com.google.android.play.core.appupdate.AppUpdateInfo -import com.google.android.play.core.install.model.ActivityResult -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -class UpdateViewModel( - application: Application, - updateInfo: UpdateInfo, - private val appUpdateChecker: AppUpdateChecker -) : AndroidViewModel(application) { - val updateInfo: MutableStateFlow = MutableStateFlow(updateInfo) - - fun checkForAppUpdate() { - viewModelScope.launch { - appUpdateChecker.newCheckForUpdateAvailabilityFlow( - getApplication() - ).onFirst { newInfo -> - updateInfo.value = newInfo - } - } - } - - fun goForUpdate( - activity: ComponentActivity, - appUpdateInfo: AppUpdateInfo - ) { - if (updateInfo.value.state == UpdateState.Running) { - return - } - - updateInfo.value = updateInfo.value.copy(state = UpdateState.Running) - - viewModelScope.launch { - appUpdateChecker.newStartUpdateFlow( - activity, - appUpdateInfo - ).onFirst { resultCode -> - val state = - when (resultCode) { - Activity.RESULT_OK -> UpdateState.Done - Activity.RESULT_CANCELED -> UpdateState.Canceled - ActivityResult.RESULT_IN_APP_UPDATE_FAILED -> UpdateState.Failed - else -> UpdateState.Prepared - } - updateInfo.value = updateInfo.value.copy(state = state) - } - } - } - - fun remindLater() { - // for mvp we just return user back to the previous screen - updateInfo.update { it.copy(state = UpdateState.Canceled) } - } -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/util/MlkitQrCodeAnalyzer.kt b/ui-lib/src/store/java/co/electriccoin/zcash/ui/screen/scan/util/QrCodeAnalyzerImpl.kt similarity index 96% rename from ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/util/MlkitQrCodeAnalyzer.kt rename to ui-lib/src/store/java/co/electriccoin/zcash/ui/screen/scan/util/QrCodeAnalyzerImpl.kt index 1b53f5dcc..9f8bee901 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/util/MlkitQrCodeAnalyzer.kt +++ b/ui-lib/src/store/java/co/electriccoin/zcash/ui/screen/scan/util/QrCodeAnalyzerImpl.kt @@ -2,9 +2,7 @@ package co.electriccoin.zcash.ui.screen.scan.util import android.graphics.Bitmap import android.graphics.Matrix -import androidx.annotation.OptIn import androidx.camera.core.ExperimentalGetImage -import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.ui.screen.scankeystone.view.FramePosition @@ -13,13 +11,13 @@ import com.google.mlkit.vision.barcode.BarcodeScanning import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.common.InputImage -class MlkitQrCodeAnalyzer( +class QrCodeAnalyzerImpl( private val framePosition: FramePosition, private val onQrCodeScanned: (String) -> Unit, -) : ImageAnalysis.Analyzer { +) : QrCodeAnalyzer { private val supportedImageFormat = Barcode.FORMAT_QR_CODE - @OptIn(ExperimentalGetImage::class) + @androidx.annotation.OptIn(ExperimentalGetImage::class) override fun analyze(imageProxy: ImageProxy) { Twig.verbose { "Mlkit image proxy: ${imageProxy.imageInfo}" } diff --git a/ui-screenshot-test/build.gradle.kts b/ui-screenshot-test/build.gradle.kts index cde67f59b..47694c185 100644 --- a/ui-screenshot-test/build.gradle.kts +++ b/ui-screenshot-test/build.gradle.kts @@ -75,7 +75,6 @@ dependencies { implementation(libs.bundles.androidx.test) implementation(libs.bundles.androidx.compose.core) - implementation(libs.bundles.play.update) implementation(libs.androidx.compose.test.junit) implementation(libs.androidx.navigation.compose)