From 4c4ef46efe4f51c53d9d61da3d5ef21b953b6872 Mon Sep 17 00:00:00 2001 From: Kevin Gorham Date: Wed, 10 Jun 2020 07:49:38 -0400 Subject: [PATCH 1/4] Updated to the newly refactored (and published) SDK. --- app/build.gradle | 78 ++++++++--------- .../android/integration/IntegrationTest.kt | 2 +- .../java/cash/z/ecc/android/ZcashWalletApp.kt | 8 +- .../di/component/InitializerSubcomponent.kt | 2 +- .../di/component/SynchronizerSubcomponent.kt | 4 +- .../android/di/module/InitializerModule.kt | 2 +- .../android/di/module/SynchronizerModule.kt | 4 +- .../java/cash/z/ecc/android/ext/Dialogs.kt | 85 +++++++++++++++++++ .../java/cash/z/ecc/android/ext/EditText.kt | 6 +- .../cash/z/ecc/android/ui/MainActivity.kt | 8 +- .../android/ui/detail/TransactionAdapter.kt | 2 +- .../ui/detail/TransactionViewHolder.kt | 4 +- .../android/ui/detail/WalletDetailFragment.kt | 12 +-- .../ui/detail/WalletDetailViewModel.kt | 4 +- .../z/ecc/android/ui/home/HomeFragment.kt | 14 +-- .../z/ecc/android/ui/home/HomeViewModel.kt | 16 ++-- .../z/ecc/android/ui/home/MagicSnakeLoader.kt | 2 +- .../ecc/android/ui/profile/ProfileFragment.kt | 4 +- .../android/ui/profile/ProfileViewModel.kt | 4 +- .../ecc/android/ui/receive/ReceiveFragment.kt | 4 +- .../android/ui/receive/ReceiveViewModel.kt | 4 +- .../cash/z/ecc/android/ui/scan/QrAnalyzer.kt | 6 +- .../z/ecc/android/ui/scan/ScanFragment.kt | 79 +++++++++++++---- .../z/ecc/android/ui/scan/ScanViewModel.kt | 4 +- .../android/ui/send/SendAddressFragment.kt | 13 +-- .../android/ui/send/SendConfirmFragment.kt | 4 +- .../ecc/android/ui/send/SendFinalFragment.kt | 8 +- .../z/ecc/android/ui/send/SendViewModel.kt | 15 ++-- .../z/ecc/android/ui/setup/LandingFragment.kt | 2 +- .../z/ecc/android/ui/setup/RestoreFragment.kt | 4 +- .../z/ecc/android/ui/setup/SeedWordAdapter.kt | 2 +- .../android/ui/setup/WalletSetupViewModel.kt | 10 +-- .../cash/z/ecc/android/SendViewModelTest.kt | 2 +- build.gradle | 5 +- .../java/cash/z/ecc/android/Dependencies.kt | 76 +++++++++++++---- gradle/wrapper/gradle-wrapper.properties | 4 +- mnemonic/build.gradle | 2 +- .../cash/z/ecc/kotlin/mnemonic/Mnemonics.kt | 57 +++---------- settings.gradle | 3 +- 39 files changed, 347 insertions(+), 218 deletions(-) create mode 100644 app/src/main/java/cash/z/ecc/android/ext/Dialogs.kt diff --git a/app/build.gradle b/app/build.gradle index 7a14477..1c44b4a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,9 +11,10 @@ apply plugin: 'com.google.firebase.firebase-perf' archivesBaseName = 'zcash-android-wallet' group = 'cash.z.ecc.android' -version = '1.0.0-alpha25' +version = '1.0.0-alpha28' android { + ndkVersion "21.1.6352462" compileSdkVersion Deps.compileSdkVersion buildToolsVersion Deps.buildToolsVersion viewBinding.enabled = true @@ -27,7 +28,6 @@ android { testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunnerArguments clearPackageData: 'true' multiDexEnabled true -// manifestPlaceholders = [rollbarToken: properties["rollbarToken"]] } flavorDimensions 'network' productFlavors { @@ -112,30 +112,40 @@ dependencies { implementation project(':feedback') implementation project(':mnemonic') implementation project(':lockbox') - implementation project(':sdk') implementation project(':chipsinputlayout') // Zcash - implementation 'com.github.zcash:zcash-android-wallet-plugins:1.0.0' - + implementation Deps.Zcash.ANDROID_WALLET_PLUGINS + zcashtestnetImplementation Deps.Zcash.Sdk.TESTNET + zcashmainnetImplementation Deps.Zcash.Sdk.MAINNET + // Kotlin implementation Deps.Kotlin.STDLIB // Android + implementation Deps.AndroidX.ANNOTATION implementation Deps.AndroidX.APPCOMPAT - implementation Deps.AndroidX.CORE_KTX implementation Deps.AndroidX.CONSTRAINT_LAYOUT - implementation Deps.AndroidX.Lifecycle.LIFECYCLE_RUNTIME_KTX + implementation Deps.AndroidX.CORE_KTX + implementation Deps.AndroidX.FRAGMENT_KTX + implementation Deps.AndroidX.LEGACY + implementation Deps.AndroidX.PAGING + implementation Deps.AndroidX.CameraX.CAMERA2 + implementation Deps.AndroidX.CameraX.CORE + implementation Deps.AndroidX.CameraX.LIFECYCLE + implementation Deps.AndroidX.CameraX.View.EXT + implementation Deps.AndroidX.CameraX.View.VIEW implementation Deps.AndroidX.Lifecycle.LIFECYCLE_EXTENSIONS + implementation Deps.AndroidX.Lifecycle.LIFECYCLE_RUNTIME_KTX implementation Deps.AndroidX.Navigation.FRAGMENT_KTX implementation Deps.AndroidX.Navigation.UI_KTX - implementation "androidx.room:room-ktx:2.2.3" - implementation "androidx.paging:paging-runtime-ktx:2.1.1" - implementation 'com.google.guava:guava:27.0.1-android' - kapt "androidx.room:room-compiler:2.2.3" + implementation Deps.AndroidX.Room.ROOM_KTX + kapt Deps.AndroidX.Room.ROOM_COMPILER // Google + implementation Deps.Google.GUAVA implementation Deps.Google.MATERIAL + implementation Deps.Google.ML_VISION // QR Scanner // Dagger implementation Deps.Dagger.ANDROID_SUPPORT @@ -143,44 +153,26 @@ dependencies { kapt Deps.Dagger.COMPILER // grpc-java - implementation "io.grpc:grpc-okhttp:1.25.0" - implementation "io.grpc:grpc-android:1.25.0" - implementation "io.grpc:grpc-protobuf-lite:1.25.0" - implementation "io.grpc:grpc-stub:1.25.0" - implementation 'javax.annotation:javax.annotation-api:1.3.2' - // solves error: Duplicate class com.google.common.util.concurrent.ListenableFuture found in modules jetified-guava-26.0-android.jar (com.google.guava:guava:26.0-android) and listenablefuture-1.0.jar (com.google.guava:listenablefuture:1.0) - // per this recommendation from Chris Povirk, given guava's decision to split ListenableFuture away from Guava: https://groups.google.com/d/msg/guava-discuss/GghaKwusjcY/bCIAKfzOEwAJ - implementation 'com.google.guava:guava:27.0.1-android' + implementation Deps.Grpc.ANDROID + implementation Deps.Grpc.OKHTTP + implementation Deps.Grpc.PROTOBUG + implementation Deps.Grpc.STUB + implementation Deps.JavaX.JAVA_ANNOTATION - // Analytics - implementation 'com.mixpanel.android:mixpanel-android:5.6.3' - implementation 'com.google.firebase:firebase-analytics:17.2.2' - implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' - implementation 'com.crashlytics.sdk.android:crashlytics-ndk:2.1.1' - implementation 'com.google.firebase:firebase-perf:19.0.5' - - // QR Scanning - implementation 'com.google.firebase:firebase-ml-vision:24.0.1' - implementation 'androidx.camera:camera-core:1.0.0-alpha10' - implementation 'androidx.camera:camera-camera2:1.0.0-alpha10' - implementation "androidx.camera:camera-view:1.0.0-alpha07" - implementation "androidx.camera:camera-extensions:1.0.0-alpha07" - implementation "androidx.camera:camera-lifecycle:1.0.0-alpha10" + // Analytics (for dogfooding/crash-reporting/feedback only on internal team builds) + implementation Deps.Analytics.CRASHLYTICS + implementation Deps.Analytics.CRASHLYTICS_NDK + implementation Deps.Analytics.FIREBASE + implementation Deps.Analytics.FIREBASE_PERF + implementation Deps.Analytics.MIXPANEL // Misc. - implementation 'com.airbnb.android:lottie:3.1.0' - implementation 'com.facebook.stetho:stetho:1.5.1' - // check for build errors at https://jitpack.io/com/github/gmale/chips-input-layout//build.log -// implementation 'com.github.gmale:chips-input-layout:4750760a7222bc04ca48266b387456d2b03541a7' - - implementation 'androidx.annotation:annotation:1.1.0' + implementation Deps.Misc.LOTTIE // Tests - - implementation 'androidx.legacy:legacy-support-v4:1.0.0' + testImplementation Deps.Test.COROUTINES_TEST testImplementation Deps.Test.JUNIT - testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.3' + testImplementation Deps.Test.MOKITO androidTestImplementation Deps.Test.Android.JUNIT androidTestImplementation Deps.Test.Android.ESPRESSO } diff --git a/app/src/androidTest/java/cash/z/ecc/android/integration/IntegrationTest.kt b/app/src/androidTest/java/cash/z/ecc/android/integration/IntegrationTest.kt index c60373f..9425ef7 100644 --- a/app/src/androidTest/java/cash/z/ecc/android/integration/IntegrationTest.kt +++ b/app/src/androidTest/java/cash/z/ecc/android/integration/IntegrationTest.kt @@ -5,7 +5,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import cash.z.ecc.android.lockbox.LockBox import cash.z.ecc.kotlin.mnemonic.Mnemonics -import cash.z.wallet.sdk.Initializer +import cash.z.ecc.android.sdk.Initializer import okio.Buffer import okio.GzipSink import okio.Okio diff --git a/app/src/main/java/cash/z/ecc/android/ZcashWalletApp.kt b/app/src/main/java/cash/z/ecc/android/ZcashWalletApp.kt index 809014c..fc3ab61 100644 --- a/app/src/main/java/cash/z/ecc/android/ZcashWalletApp.kt +++ b/app/src/main/java/cash/z/ecc/android/ZcashWalletApp.kt @@ -8,10 +8,10 @@ import androidx.camera.core.CameraXConfig import cash.z.ecc.android.di.component.AppComponent import cash.z.ecc.android.di.component.DaggerAppComponent import cash.z.ecc.android.feedback.FeedbackCoordinator -import cash.z.wallet.sdk.ext.SilentTwig -import cash.z.wallet.sdk.ext.TroubleshootingTwig -import cash.z.wallet.sdk.ext.Twig -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.ext.SilentTwig +import cash.z.ecc.android.sdk.ext.TroubleshootingTwig +import cash.z.ecc.android.sdk.ext.Twig +import cash.z.ecc.android.sdk.ext.twig import kotlinx.coroutines.* import javax.inject.Inject diff --git a/app/src/main/java/cash/z/ecc/android/di/component/InitializerSubcomponent.kt b/app/src/main/java/cash/z/ecc/android/di/component/InitializerSubcomponent.kt index 0e8b8e8..5212d3a 100644 --- a/app/src/main/java/cash/z/ecc/android/di/component/InitializerSubcomponent.kt +++ b/app/src/main/java/cash/z/ecc/android/di/component/InitializerSubcomponent.kt @@ -4,7 +4,7 @@ import cash.z.ecc.android.ZcashWalletApp import cash.z.ecc.android.di.annotation.ActivityScope import cash.z.ecc.android.di.annotation.SynchronizerScope import cash.z.ecc.android.di.module.InitializerModule -import cash.z.wallet.sdk.Initializer +import cash.z.ecc.android.sdk.Initializer import dagger.BindsInstance import dagger.Subcomponent diff --git a/app/src/main/java/cash/z/ecc/android/di/component/SynchronizerSubcomponent.kt b/app/src/main/java/cash/z/ecc/android/di/component/SynchronizerSubcomponent.kt index ef7c602..8c4f2b6 100644 --- a/app/src/main/java/cash/z/ecc/android/di/component/SynchronizerSubcomponent.kt +++ b/app/src/main/java/cash/z/ecc/android/di/component/SynchronizerSubcomponent.kt @@ -3,8 +3,8 @@ package cash.z.ecc.android.di.component import androidx.lifecycle.ViewModelProvider import cash.z.ecc.android.di.annotation.SynchronizerScope import cash.z.ecc.android.di.module.SynchronizerModule -import cash.z.wallet.sdk.Initializer -import cash.z.wallet.sdk.Synchronizer +import cash.z.ecc.android.sdk.Initializer +import cash.z.ecc.android.sdk.Synchronizer import dagger.BindsInstance import dagger.Subcomponent import javax.inject.Named diff --git a/app/src/main/java/cash/z/ecc/android/di/module/InitializerModule.kt b/app/src/main/java/cash/z/ecc/android/di/module/InitializerModule.kt index 4c605e3..aa21008 100644 --- a/app/src/main/java/cash/z/ecc/android/di/module/InitializerModule.kt +++ b/app/src/main/java/cash/z/ecc/android/di/module/InitializerModule.kt @@ -1,7 +1,7 @@ package cash.z.ecc.android.di.module import android.content.Context -import cash.z.wallet.sdk.Initializer +import cash.z.ecc.android.sdk.Initializer import dagger.Module import dagger.Provides import dagger.Reusable diff --git a/app/src/main/java/cash/z/ecc/android/di/module/SynchronizerModule.kt b/app/src/main/java/cash/z/ecc/android/di/module/SynchronizerModule.kt index 8af5e00..93248d9 100644 --- a/app/src/main/java/cash/z/ecc/android/di/module/SynchronizerModule.kt +++ b/app/src/main/java/cash/z/ecc/android/di/module/SynchronizerModule.kt @@ -2,8 +2,8 @@ package cash.z.ecc.android.di.module import android.content.Context import cash.z.ecc.android.di.annotation.SynchronizerScope -import cash.z.wallet.sdk.Initializer -import cash.z.wallet.sdk.Synchronizer +import cash.z.ecc.android.sdk.Initializer +import cash.z.ecc.android.sdk.Synchronizer import dagger.Module import dagger.Provides diff --git a/app/src/main/java/cash/z/ecc/android/ext/Dialogs.kt b/app/src/main/java/cash/z/ecc/android/ext/Dialogs.kt new file mode 100644 index 0000000..27f6f84 --- /dev/null +++ b/app/src/main/java/cash/z/ecc/android/ext/Dialogs.kt @@ -0,0 +1,85 @@ +package cash.z.ecc.android.ext + +import android.app.ActivityManager +import android.app.Dialog +import android.content.Context +import android.content.Intent +import android.provider.Settings +import androidx.core.content.getSystemService +import com.google.android.material.dialog.MaterialAlertDialogBuilder + + +fun Context.showClearDataConfirmation(onDismiss: () -> Unit = {}, onCancel: () -> Unit = {}): Dialog { + return MaterialAlertDialogBuilder(this) + .setTitle("Nuke Wallet?") + .setMessage("WARNING: Potential Loss of Funds\n\nClearing all wallet data and can result in a loss of funds, if you cannot locate your correct seed phrase.\n\nPlease confirm that you have your 24-word seed phrase available before proceeding.") + .setCancelable(false) + .setPositiveButton("Cancel") { dialog, _ -> + dialog.dismiss() + onDismiss() + onCancel() + } + .setNegativeButton("Erase Wallet") { dialog, _ -> + dialog.dismiss() + onDismiss() + getSystemService()?.clearApplicationUserData() + } + .show() +} + +fun Context.showUninitializedError(error: Throwable? = null, onDismiss: () -> Unit = {}): Dialog { + return MaterialAlertDialogBuilder(this) + .setTitle("Wallet Improperly Initialized") + .setMessage("This wallet has not been initialized correctly! Perhaps an error occurred during install.\n\nThis can be fixed with a reset. First, locate your backup seed phrase, then CLEAR DATA and reimport it.") + .setCancelable(false) + .setPositiveButton("Exit") { dialog, _ -> + dialog.dismiss() + onDismiss() + if (error != null) throw error + } + .setNegativeButton("Clear Data") { dialog, _ -> + showClearDataConfirmation(onDismiss, onCancel = { + // do not let the user back into the app because we cannot recover from this case + showUninitializedError(error, onDismiss) + }) + } + .show() +} + +fun Context.showScanFailure(error: Throwable?, onCancel: () -> Unit = {}, onDismiss: () -> Unit = {}): Dialog { + val message = if (error == null) { + "Unknown error" + } else { + "${error.message}${if (error.cause != null) "\n\nCaused by: ${error.cause}" else ""}" + } + return MaterialAlertDialogBuilder(this) + .setTitle("Scan Failure") + .setMessage(message) + .setCancelable(true) + .setPositiveButton("Retry") { d, _ -> + d.dismiss() + onDismiss() + } + .setNegativeButton("Ignore") { d, _ -> + d.dismiss() + onCancel() + onDismiss() + } + .show() +} + +fun Context.showCriticalProcessorError(error: Throwable?, onRetry: () -> Unit = {}): Dialog { + return MaterialAlertDialogBuilder(this) + .setTitle("Processor Error") + .setMessage(error?.message ?: "Critical error while processing blocks!") + .setCancelable(false) + .setPositiveButton("Retry") { d, _ -> + d.dismiss() + onRetry() + } + .setNegativeButton("Exit") { dialog, _ -> + dialog.dismiss() + throw error ?: RuntimeException("Critical error while processing blocks and the user chose to exit.") + } + .show() +} \ No newline at end of file diff --git a/app/src/main/java/cash/z/ecc/android/ext/EditText.kt b/app/src/main/java/cash/z/ecc/android/ext/EditText.kt index 24d6064..adba13b 100644 --- a/app/src/main/java/cash/z/ecc/android/ext/EditText.kt +++ b/app/src/main/java/cash/z/ecc/android/ext/EditText.kt @@ -3,9 +3,9 @@ package cash.z.ecc.android.ext import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.widget.EditText import android.widget.TextView -import cash.z.wallet.sdk.ext.convertZecToZatoshi -import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.ext.convertZecToZatoshi +import cash.z.ecc.android.sdk.ext.safelyConvertToBigDecimal +import cash.z.ecc.android.sdk.ext.twig fun EditText.onEditorActionDone(block: (EditText) -> Unit) { this.setOnEditorActionListener { _, actionId, _ -> diff --git a/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt b/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt index d46ab6e..617a007 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt @@ -38,10 +38,10 @@ import cash.z.ecc.android.feedback.Report.Error.NonFatal.Reorg import cash.z.ecc.android.feedback.Report.NonUserAction.FEEDBACK_STOPPED import cash.z.ecc.android.feedback.Report.NonUserAction.SYNC_START import cash.z.ecc.android.feedback.Report.Tap.COPY_ADDRESS -import cash.z.wallet.sdk.Initializer -import cash.z.wallet.sdk.exception.CompactBlockProcessorException -import cash.z.wallet.sdk.ext.ZcashSdk -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.Initializer +import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException +import cash.z.ecc.android.sdk.ext.ZcashSdk +import cash.z.ecc.android.sdk.ext.twig import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.launch diff --git a/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionAdapter.kt b/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionAdapter.kt index 22aa563..ad12b56 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionAdapter.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionAdapter.kt @@ -5,7 +5,7 @@ import android.view.ViewGroup import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil import cash.z.ecc.android.R -import cash.z.wallet.sdk.entity.ConfirmedTransaction +import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction class TransactionAdapter : PagedListAdapter>( diff --git a/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt b/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt index 3bca6c7..9494555 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt @@ -11,8 +11,8 @@ import cash.z.ecc.android.ui.MainActivity import cash.z.ecc.android.ui.send.SendViewModel import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX import cash.z.ecc.android.ui.util.toUtf8Memo -import cash.z.wallet.sdk.entity.ConfirmedTransaction -import cash.z.wallet.sdk.ext.* +import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction +import cash.z.ecc.android.sdk.ext.* import com.google.android.material.dialog.MaterialAlertDialogBuilder import java.nio.charset.Charset import java.text.SimpleDateFormat diff --git a/app/src/main/java/cash/z/ecc/android/ui/detail/WalletDetailFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/detail/WalletDetailFragment.kt index 9305458..8595cb3 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/detail/WalletDetailFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/detail/WalletDetailFragment.kt @@ -15,12 +15,12 @@ import cash.z.ecc.android.ext.toColoredSpan import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report.Tap.DETAIL_BACK import cash.z.ecc.android.ui.base.BaseFragment -import cash.z.wallet.sdk.block.CompactBlockProcessor.WalletBalance -import cash.z.wallet.sdk.entity.ConfirmedTransaction -import cash.z.wallet.sdk.ext.collectWith -import cash.z.wallet.sdk.ext.convertZatoshiToZecString -import cash.z.wallet.sdk.ext.toAbbreviatedAddress -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.block.CompactBlockProcessor.WalletBalance +import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction +import cash.z.ecc.android.sdk.ext.collectWith +import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString +import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress +import cash.z.ecc.android.sdk.ext.twig import kotlinx.coroutines.launch diff --git a/app/src/main/java/cash/z/ecc/android/ui/detail/WalletDetailViewModel.kt b/app/src/main/java/cash/z/ecc/android/ui/detail/WalletDetailViewModel.kt index 09f965a..ffdd2d0 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/detail/WalletDetailViewModel.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/detail/WalletDetailViewModel.kt @@ -1,8 +1,8 @@ package cash.z.ecc.android.ui.detail import androidx.lifecycle.ViewModel -import cash.z.wallet.sdk.Synchronizer -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.Synchronizer +import cash.z.ecc.android.sdk.ext.twig import javax.inject.Inject class WalletDetailViewModel @Inject constructor() : ViewModel() { diff --git a/app/src/main/java/cash/z/ecc/android/ui/home/HomeFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/home/HomeFragment.kt index 078a376..731446e 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/home/HomeFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/home/HomeFragment.kt @@ -18,13 +18,13 @@ import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.* import cash.z.ecc.android.ui.send.SendViewModel import cash.z.ecc.android.ui.setup.WalletSetupViewModel import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.NO_SEED -import cash.z.wallet.sdk.Synchronizer -import cash.z.wallet.sdk.Synchronizer.Status.* -import cash.z.wallet.sdk.block.CompactBlockProcessor -import cash.z.wallet.sdk.ext.convertZatoshiToZecString -import cash.z.wallet.sdk.ext.convertZecToZatoshi -import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.Synchronizer +import cash.z.ecc.android.sdk.Synchronizer.Status.* +import cash.z.ecc.android.sdk.block.CompactBlockProcessor +import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString +import cash.z.ecc.android.sdk.ext.convertZecToZatoshi +import cash.z.ecc.android.sdk.ext.safelyConvertToBigDecimal +import cash.z.ecc.android.sdk.ext.twig import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* diff --git a/app/src/main/java/cash/z/ecc/android/ui/home/HomeViewModel.kt b/app/src/main/java/cash/z/ecc/android/ui/home/HomeViewModel.kt index 63395be..5e0dfec 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/home/HomeViewModel.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/home/HomeViewModel.kt @@ -1,14 +1,14 @@ package cash.z.ecc.android.ui.home import androidx.lifecycle.ViewModel -import cash.z.wallet.sdk.SdkSynchronizer -import cash.z.wallet.sdk.Synchronizer -import cash.z.wallet.sdk.Synchronizer.Status.* -import cash.z.wallet.sdk.block.CompactBlockProcessor -import cash.z.wallet.sdk.exception.RustLayerException -import cash.z.wallet.sdk.ext.ZcashSdk.MINERS_FEE_ZATOSHI -import cash.z.wallet.sdk.ext.ZcashSdk.ZATOSHI_PER_ZEC -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.SdkSynchronizer +import cash.z.ecc.android.sdk.Synchronizer +import cash.z.ecc.android.sdk.Synchronizer.Status.* +import cash.z.ecc.android.sdk.block.CompactBlockProcessor +import cash.z.ecc.android.sdk.exception.RustLayerException +import cash.z.ecc.android.sdk.ext.ZcashSdk.MINERS_FEE_ZATOSHI +import cash.z.ecc.android.sdk.ext.ZcashSdk.ZATOSHI_PER_ZEC +import cash.z.ecc.android.sdk.ext.twig import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.flow.* import javax.inject.Inject diff --git a/app/src/main/java/cash/z/ecc/android/ui/home/MagicSnakeLoader.kt b/app/src/main/java/cash/z/ecc/android/ui/home/MagicSnakeLoader.kt index 57f812e..a9514c7 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/home/MagicSnakeLoader.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/home/MagicSnakeLoader.kt @@ -1,7 +1,7 @@ package cash.z.ecc.android.ui.home import android.animation.ValueAnimator -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.ext.twig import com.airbnb.lottie.LottieAnimationView class MagicSnakeLoader( diff --git a/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileFragment.kt index 4d4088a..2d64156 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileFragment.kt @@ -19,8 +19,8 @@ import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report.Funnel.UserFeedback import cash.z.ecc.android.feedback.Report.Tap.* import cash.z.ecc.android.ui.base.BaseFragment -import cash.z.wallet.sdk.ext.toAbbreviatedAddress -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress +import cash.z.ecc.android.sdk.ext.twig import kotlinx.coroutines.launch import okio.Okio import java.io.File diff --git a/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileViewModel.kt b/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileViewModel.kt index 5b8e351..2745a9e 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileViewModel.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/profile/ProfileViewModel.kt @@ -1,8 +1,8 @@ package cash.z.ecc.android.ui.profile import androidx.lifecycle.ViewModel -import cash.z.wallet.sdk.Synchronizer -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.Synchronizer +import cash.z.ecc.android.sdk.ext.twig import javax.inject.Inject class ProfileViewModel @Inject constructor() : ViewModel() { diff --git a/app/src/main/java/cash/z/ecc/android/ui/receive/ReceiveFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/receive/ReceiveFragment.kt index 68777c4..9524f6a 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/receive/ReceiveFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/receive/ReceiveFragment.kt @@ -13,8 +13,8 @@ import cash.z.ecc.android.ext.onClickNavTo import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report.Tap.* import cash.z.ecc.android.ui.base.BaseFragment -import cash.z.wallet.sdk.ext.toAbbreviatedAddress -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress +import cash.z.ecc.android.sdk.ext.twig import kotlinx.coroutines.launch import kotlin.math.roundToInt diff --git a/app/src/main/java/cash/z/ecc/android/ui/receive/ReceiveViewModel.kt b/app/src/main/java/cash/z/ecc/android/ui/receive/ReceiveViewModel.kt index 1b4cfbc..0eecafc 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/receive/ReceiveViewModel.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/receive/ReceiveViewModel.kt @@ -1,8 +1,8 @@ package cash.z.ecc.android.ui.receive import androidx.lifecycle.ViewModel -import cash.z.wallet.sdk.Synchronizer -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.Synchronizer +import cash.z.ecc.android.sdk.ext.twig import javax.inject.Inject class ReceiveViewModel @Inject constructor() : ViewModel() { diff --git a/app/src/main/java/cash/z/ecc/android/ui/scan/QrAnalyzer.kt b/app/src/main/java/cash/z/ecc/android/ui/scan/QrAnalyzer.kt index c13ab4e..fbb5de4 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/scan/QrAnalyzer.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/scan/QrAnalyzer.kt @@ -2,9 +2,9 @@ package cash.z.ecc.android.ui.scan import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy -import cash.z.wallet.sdk.ext.retrySimple -import cash.z.wallet.sdk.ext.retryUpTo -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.ext.retrySimple +import cash.z.ecc.android.sdk.ext.retryUpTo +import cash.z.ecc.android.sdk.ext.twig import com.google.android.gms.tasks.Task import com.google.firebase.ml.vision.FirebaseVision import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode diff --git a/app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt index 8317e1b..67d22c6 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt @@ -3,12 +3,10 @@ package cash.z.ecc.android.ui.scan import android.content.Context import android.content.pm.PackageManager import android.os.Bundle +import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View -import androidx.camera.core.CameraSelector -import androidx.camera.core.ImageAnalysis -import androidx.camera.core.ImageProxy -import androidx.camera.core.Preview +import androidx.camera.core.* import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.ContextCompat import cash.z.ecc.android.R @@ -18,11 +16,15 @@ import cash.z.ecc.android.di.viewmodel.viewModel import cash.z.ecc.android.ext.onClickNavBack import cash.z.ecc.android.ext.onClickNavTo import cash.z.ecc.android.feedback.Report -import cash.z.ecc.android.feedback.Report.Tap.* +import cash.z.ecc.android.feedback.Report.Tap.SCAN_BACK +import cash.z.ecc.android.feedback.Report.Tap.SCAN_RECEIVE +import cash.z.ecc.android.sdk.ext.twig import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.send.SendViewModel +import com.crashlytics.android.Crashlytics import com.google.common.util.concurrent.ListenableFuture import kotlinx.coroutines.launch +import java.util.concurrent.ExecutorService import java.util.concurrent.Executors class ScanFragment : BaseFragment() { @@ -33,11 +35,15 @@ class ScanFragment : BaseFragment() { private lateinit var cameraProviderFuture: ListenableFuture + private var cameraExecutor: ExecutorService? = null + override fun inflate(inflater: LayoutInflater): FragmentScanBinding = FragmentScanBinding.inflate(inflater) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + if (cameraExecutor != null) cameraExecutor?.shutdown() + cameraExecutor = Executors.newSingleThreadExecutor() binding.buttonReceive.onClickNavTo(R.id.action_nav_scan_to_nav_receive) { tapped(SCAN_RECEIVE) } binding.backButtonHitArea.onClickNavBack() { tapped(SCAN_BACK) } @@ -56,26 +62,67 @@ class ScanFragment : BaseFragment() { }, ContextCompat.getMainExecutor(context)) } + override fun onDestroyView() { + super.onDestroyView() + cameraExecutor?.shutdown() + cameraExecutor = null + } + private fun bindPreview(cameraProvider: ProcessCameraProvider) { - Preview.Builder().setTargetName("Preview").build().let { preview -> - preview.setSurfaceProvider(binding.preview.previewSurfaceProvider) + // Most of the code here is adapted from: https://github.com/android/camera-samples/blob/master/CameraXBasic/app/src/main/java/com/android/example/cameraxbasic/fragments/CameraFragment.kt + // it's worth keeping tabs on that implementation because they keep making breaking changes to these APIs! - val cameraSelector = CameraSelector.Builder() - .requireLensFacing(CameraSelector.LENS_FACING_BACK) - .build() + // Get screen metrics used to setup camera for full screen resolution + val metrics = DisplayMetrics().also { binding.preview.display.getRealMetrics(it) } + val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels) + val rotation = binding.preview.display.rotation - val imageAnalysis = ImageAnalysis.Builder() - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .build() + val preview = + Preview.Builder().setTargetName("Preview").setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(rotation).build() - imageAnalysis.setAnalyzer(Executors.newSingleThreadExecutor(), QrAnalyzer { q, i -> - onQrScanned(q, i) - }) + val cameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build() + + val imageAnalysis = ImageAnalysis.Builder().setTargetAspectRatio(screenAspectRatio) + .setTargetRotation(rotation) + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + + imageAnalysis.setAnalyzer(cameraExecutor!!, QrAnalyzer { q, i -> + onQrScanned(q, i) + }) + + // Must unbind the use-cases before rebinding them + cameraProvider.unbindAll() + + try { cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis) + preview.setSurfaceProvider(binding.preview.createSurfaceProvider()) + } catch (t: Throwable) { + // TODO: consider bubbling this up to the user + Crashlytics.logException(t) + twig("Error while opening the camera: $t") } } + /** + * Adapted from: https://github.com/android/camera-samples/blob/master/CameraXBasic/app/src/main/java/com/android/example/cameraxbasic/fragments/CameraFragment.kt#L350 + */ + private fun aspectRatio(width: Int, height: Int): Int { + val previewRatio = kotlin.math.max(width, height).toDouble() / kotlin.math.min( + width, + height + ) + if (kotlin.math.abs(previewRatio - (4.0 / 3.0)) + <= kotlin.math.abs(previewRatio - (16.0 / 9.0))) { + return AspectRatio.RATIO_4_3 + } + return AspectRatio.RATIO_16_9 + } + private fun onQrScanned(qrContent: String, image: ImageProxy) { resumedScope.launch { if (viewModel.isNotValid(qrContent)) image.close() // continue scanning diff --git a/app/src/main/java/cash/z/ecc/android/ui/scan/ScanViewModel.kt b/app/src/main/java/cash/z/ecc/android/ui/scan/ScanViewModel.kt index b6746d0..4fe988e 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/scan/ScanViewModel.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/scan/ScanViewModel.kt @@ -1,8 +1,8 @@ package cash.z.ecc.android.ui.scan import androidx.lifecycle.ViewModel -import cash.z.wallet.sdk.Synchronizer -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.Synchronizer +import cash.z.ecc.android.sdk.ext.twig import javax.inject.Inject class ScanViewModel @Inject constructor() : ViewModel() { diff --git a/app/src/main/java/cash/z/ecc/android/ui/send/SendAddressFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/send/SendAddressFragment.kt index d8a9526..db9c2a2 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/send/SendAddressFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/send/SendAddressFragment.kt @@ -16,9 +16,10 @@ import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report.Funnel.Send import cash.z.ecc.android.feedback.Report.Tap.* import cash.z.ecc.android.ui.base.BaseFragment -import cash.z.wallet.sdk.Synchronizer -import cash.z.wallet.sdk.block.CompactBlockProcessor.WalletBalance -import cash.z.wallet.sdk.ext.* +import cash.z.ecc.android.sdk.Synchronizer +import cash.z.ecc.android.sdk.block.CompactBlockProcessor.WalletBalance +import cash.z.ecc.android.sdk.ext.* +import cash.z.ecc.android.sdk.validate.AddressType import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -85,9 +86,9 @@ class SendAddressFragment : BaseFragment(), private fun onAddressChanged(address: String) { resumedScope.launch { var type = when (sendViewModel.validateAddress(address)) { - is Synchronizer.AddressType.Transparent -> "This is a valid transparent address" to R.color.zcashGreen - is Synchronizer.AddressType.Shielded -> "This is a valid shielded address" to R.color.zcashGreen - is Synchronizer.AddressType.Invalid -> "This address appears to be invalid" to R.color.zcashRed + is AddressType.Transparent -> "This is a valid transparent address" to R.color.zcashGreen + is AddressType.Shielded -> "This is a valid shielded address" to R.color.zcashGreen + is AddressType.Invalid -> "This address appears to be invalid" to R.color.zcashRed } if (address == sendViewModel.synchronizer.getAddress()) type = "Warning, this appears to be your address!" to R.color.zcashRed diff --git a/app/src/main/java/cash/z/ecc/android/ui/send/SendConfirmFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/send/SendConfirmFragment.kt index eca5e2d..73d6fea 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/send/SendConfirmFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/send/SendConfirmFragment.kt @@ -13,8 +13,8 @@ import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report.Funnel.Send import cash.z.ecc.android.feedback.Report.Tap.* import cash.z.ecc.android.ui.base.BaseFragment -import cash.z.wallet.sdk.ext.toAbbreviatedAddress -import cash.z.wallet.sdk.ext.convertZatoshiToZecString +import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress +import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString import kotlinx.coroutines.launch class SendConfirmFragment : BaseFragment() { diff --git a/app/src/main/java/cash/z/ecc/android/ui/send/SendFinalFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/send/SendFinalFragment.kt index f275f2d..814c6b1 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/send/SendFinalFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/send/SendFinalFragment.kt @@ -12,10 +12,10 @@ import cash.z.ecc.android.ext.goneIf import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report.Tap.* import cash.z.ecc.android.ui.base.BaseFragment -import cash.z.wallet.sdk.entity.* -import cash.z.wallet.sdk.ext.convertZatoshiToZecString -import cash.z.wallet.sdk.ext.toAbbreviatedAddress -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.db.entity.* +import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString +import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress +import cash.z.ecc.android.sdk.ext.twig import com.crashlytics.android.Crashlytics import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flow diff --git a/app/src/main/java/cash/z/ecc/android/ui/send/SendViewModel.kt b/app/src/main/java/cash/z/ecc/android/ui/send/SendViewModel.kt index f75cd51..9756f4b 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/send/SendViewModel.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/send/SendViewModel.kt @@ -14,12 +14,13 @@ import cash.z.ecc.android.feedback.Report.MetricType.* import cash.z.ecc.android.lockbox.LockBox import cash.z.ecc.android.ui.setup.WalletSetupViewModel import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX -import cash.z.wallet.sdk.Initializer -import cash.z.wallet.sdk.Synchronizer -import cash.z.wallet.sdk.entity.* -import cash.z.wallet.sdk.ext.ZcashSdk -import cash.z.wallet.sdk.ext.convertZatoshiToZecString -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.Initializer +import cash.z.ecc.android.sdk.Synchronizer +import cash.z.ecc.android.sdk.db.entity.* +import cash.z.ecc.android.sdk.ext.ZcashSdk +import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString +import cash.z.ecc.android.sdk.ext.twig +import cash.z.ecc.android.sdk.validate.AddressType import com.crashlytics.android.Crashlytics import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.flow.Flow @@ -94,7 +95,7 @@ class SendViewModel @Inject constructor() : ViewModel() { } } - suspend fun validateAddress(address: String): Synchronizer.AddressType = + suspend fun validateAddress(address: String): AddressType = synchronizer.validateAddress(address) fun validate(maxZatoshi: Long?) = flow { diff --git a/app/src/main/java/cash/z/ecc/android/ui/setup/LandingFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/setup/LandingFragment.kt index c3d743f..491486e 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/setup/LandingFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/setup/LandingFragment.kt @@ -18,7 +18,7 @@ import cash.z.ecc.android.feedback.Report.Tap.* import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITHOUT_BACKUP import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITH_BACKUP -import cash.z.wallet.sdk.Initializer +import cash.z.ecc.android.sdk.Initializer import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach diff --git a/app/src/main/java/cash/z/ecc/android/ui/setup/RestoreFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/setup/RestoreFragment.kt index 9011f97..389d4ad 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/setup/RestoreFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/setup/RestoreFragment.kt @@ -21,8 +21,8 @@ import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report.Funnel.Restore import cash.z.ecc.android.feedback.Report.Tap.* import cash.z.ecc.android.ui.base.BaseFragment -import cash.z.wallet.sdk.ext.ZcashSdk -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.ext.ZcashSdk +import cash.z.ecc.android.sdk.ext.twig import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tylersuehr.chips.Chip import com.tylersuehr.chips.ChipsAdapter diff --git a/app/src/main/java/cash/z/ecc/android/ui/setup/SeedWordAdapter.kt b/app/src/main/java/cash/z/ecc/android/ui/setup/SeedWordAdapter.kt index 26b0057..fe12c6a 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/setup/SeedWordAdapter.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/setup/SeedWordAdapter.kt @@ -11,7 +11,7 @@ import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report.Funnel.Restore import cash.z.ecc.android.ui.MainActivity import cash.z.ecc.android.ui.setup.SeedWordChip -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.ext.twig class SeedWordAdapter : ChipsAdapter { diff --git a/app/src/main/java/cash/z/ecc/android/ui/setup/WalletSetupViewModel.kt b/app/src/main/java/cash/z/ecc/android/ui/setup/WalletSetupViewModel.kt index a875919..8d8a626 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/setup/WalletSetupViewModel.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/setup/WalletSetupViewModel.kt @@ -8,11 +8,11 @@ import cash.z.ecc.android.feedback.measure import cash.z.ecc.android.lockbox.LockBox import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.* import cash.z.ecc.kotlin.mnemonic.Mnemonics -import cash.z.wallet.sdk.Initializer -import cash.z.wallet.sdk.Initializer.DefaultBirthdayStore -import cash.z.wallet.sdk.Initializer.DefaultBirthdayStore.Companion.ImportedWalletBirthdayStore -import cash.z.wallet.sdk.Initializer.DefaultBirthdayStore.Companion.NewWalletBirthdayStore -import cash.z.wallet.sdk.ext.twig +import cash.z.ecc.android.sdk.Initializer +import cash.z.ecc.android.sdk.Initializer.DefaultBirthdayStore +import cash.z.ecc.android.sdk.Initializer.DefaultBirthdayStore.Companion.ImportedWalletBirthdayStore +import cash.z.ecc.android.sdk.Initializer.DefaultBirthdayStore.Companion.NewWalletBirthdayStore +import cash.z.ecc.android.sdk.ext.twig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow diff --git a/app/src/test/java/cash/z/ecc/android/SendViewModelTest.kt b/app/src/test/java/cash/z/ecc/android/SendViewModelTest.kt index db6a34d..a6be8f8 100644 --- a/app/src/test/java/cash/z/ecc/android/SendViewModelTest.kt +++ b/app/src/test/java/cash/z/ecc/android/SendViewModelTest.kt @@ -2,7 +2,7 @@ package cash.z.ecc.android import cash.z.ecc.android.feedback.Feedback import cash.z.ecc.android.ui.send.SendViewModel -import cash.z.wallet.sdk.entity.* +import cash.z.ecc.android.sdk.entity.* import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verifyZeroInteractions import com.nhaarman.mockitokotlin2.whenever diff --git a/build.gradle b/build.gradle index 5385374..7584f5d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,6 @@ import cash.z.ecc.android.Deps buildscript { - ext { - kotlin_version = '1.3.61' - } repositories { google() jcenter() @@ -12,7 +9,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:3.6.0-rc02' + classpath 'com.android.tools.build:gradle:4.0.0' classpath 'com.google.gms:google-services:4.3.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${Deps.kotlinVersion}" classpath 'io.fabric.tools:gradle:1.31.2' diff --git a/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt b/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt index f21ec55..0642076 100644 --- a/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt +++ b/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt @@ -3,7 +3,7 @@ package cash.z.ecc.android object Deps { // For use in the top-level build.gradle which gives an error when provided // `Deps.Kotlin.version` directly - const val kotlinVersion = "1.3.61" + const val kotlinVersion = "1.3.72" const val compileSdkVersion = 29 const val buildToolsVersion = "29.0.2" @@ -11,18 +11,34 @@ object Deps { const val targetSdkVersion = 29 object AndroidX { + const val ANNOTATION = "androidx.annotation:annotation:1.1.0" const val APPCOMPAT = "androidx.appcompat:appcompat:1.1.0" - const val CORE_KTX = "androidx.core:core-ktx:1.1.0" const val CONSTRAINT_LAYOUT = "androidx.constraintlayout:constraintlayout:1.1.3" + const val CORE_KTX = "androidx.core:core-ktx:1.1.0" const val FRAGMENT_KTX = "androidx.fragment:fragment-ktx:1.1.0-beta01" + const val LEGACY = "androidx.legacy:legacy-support-v4:1.0.0" const val MULTIDEX = "androidx.multidex:multidex:2.0.1" + const val PAGING = "androidx.paging:paging-runtime-ktx:2.1.2" + object CameraX : Version("1.0.0-beta04") { + val CAMERA2 = "androidx.camera:camera-camera2:1.0.0-beta04" + val CORE = "androidx.camera:camera-core:1.0.0-beta04" + val LIFECYCLE = "androidx.camera:camera-lifecycle:1.0.0-beta04" + object View : Version("1.0.0-alpha11") { + val EXT = "androidx.camera:camera-extensions:1.0.0-alpha11" + val VIEW = "androidx.camera:camera-view:1.0.0-alpha11" + } + } + object Lifecycle : Version("2.2.0-rc02") { + val LIFECYCLE_RUNTIME_KTX = "androidx.lifecycle:lifecycle-runtime-ktx:$version" + val LIFECYCLE_EXTENSIONS = "androidx.lifecycle:lifecycle-extensions:$version" + } object Navigation : Version("2.2.0") { val FRAGMENT_KTX = "androidx.navigation:navigation-fragment-ktx:$version" val UI_KTX = "androidx.navigation:navigation-ui-ktx:$version" } - object Lifecycle: Version("2.2.0-rc02") { - val LIFECYCLE_RUNTIME_KTX = "androidx.lifecycle:lifecycle-runtime-ktx:$version" - val LIFECYCLE_EXTENSIONS = "androidx.lifecycle:lifecycle-extensions:$version" + object Room : Version("2.2.5") { + val ROOM_COMPILER = "androidx.room:room-compiler:$version" + val ROOM_KTX = "androidx.room:room-ktx:$version" } } object Dagger : Version("2.25.2") { @@ -31,35 +47,61 @@ object Deps { val COMPILER = "com.google.dagger:dagger-compiler:$version" } object Google { + // solves error: Duplicate class com.google.common.util.concurrent.ListenableFuture found in modules jetified-guava-26.0-android.jar (com.google.guava:guava:26.0-android) and listenablefuture-1.0.jar (com.google.guava:listenablefuture:1.0) + // per this recommendation from Chris Povirk, given guava's decision to split ListenableFuture away from Guava: https://groups.google.com/d/msg/guava-discuss/GghaKwusjcY/bCIAKfzOEwAJ + const val GUAVA = "com.google.guava:guava:27.0.1-android" const val MATERIAL = "com.google.android.material:material:1.1.0-beta01" + // QR Scanner + const val ML_VISION = "com.google.firebase:firebase-ml-vision:24.0.3" + } + object Grpc : Version("1.25.0") { + val ANDROID = "io.grpc:grpc-android:$version" + val OKHTTP = "io.grpc:grpc-okhttp:$version" + val PROTOBUG = "io.grpc:grpc-protobuf-lite:$version" + val STUB = "io.grpc:grpc-stub:$version" + } + object Analytics { // for dogfooding/crash-reporting/feedback only on internal team builds + val CRASHLYTICS = "com.crashlytics.sdk.android:crashlytics:2.10.1" + val CRASHLYTICS_NDK = "com.crashlytics.sdk.android:crashlytics-ndk:2.1.1" + val FIREBASE = "com.google.firebase:firebase-analytics:17.4.3" + val FIREBASE_PERF = "com.google.firebase:firebase-perf:19.0.7" + val MIXPANEL = "com.mixpanel.android:mixpanel-android:5.6.3" } object JavaX { - const val INJECT = "javax.inject:javax.inject:1" + const val INJECT = "javax.inject:javax.inject:1" + const val JAVA_ANNOTATION = "javax.annotation:javax.annotation-api:1.3.2" } object Kotlin : Version(kotlinVersion) { - val STDLIB = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version" + val STDLIB = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version" object Coroutines : Version("1.3.2") { - val ANDROID = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version" - val CORE = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version" - val TEST = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$version" + val ANDROID = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version" + val CORE = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version" + val TEST = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$version" } } object Zcash { - val ANDROID_WALLET_PLUGINS = "com.github.zcash:zcash-android-wallet-plugins:1.0.1" + const val ANDROID_WALLET_PLUGINS = "cash.z.ecc.android:zcash-android-wallet-plugins:1.0.0" + const val KOTLIN_BIP39 = "cash.z.ecc.android:kotlin-bip39:1.0.0-beta08" + object Sdk : Version("1.1.0-beta01") { + val MAINNET = "cash.z.ecc.android:sdk-mainnet:$version" + val TESTNET = "cash.z.ecc.android:sdk-testnet:$version" + } } object Misc { + const val LOTTIE = "com.airbnb.android:lottie:3.1.0" object Plugins { - val SECURE_STORAGE = "de.adorsys.android:securestoragelibrary:1.2.2" - val ANDROID_BIP39 = "cash.z.ecc.android:android-bip39:1.0.0-beta07" - val QR_SCANNER = "com.google.zxing:core:3.2.1" + const val SECURE_STORAGE = "de.adorsys.android:securestoragelibrary:1.2.2" + const val QR_SCANNER = "com.google.zxing:core:3.2.1" } } object Test { - const val JUNIT = "junit:junit:4.12" + const val JUNIT = "junit:junit:4.12" + const val MOKITO = "junit:junit:4.12" + const val COROUTINES_TEST = "junit:junit:4.12" object Android { - const val JUNIT = "androidx.test.ext:junit:1.1.1" - const val ESPRESSO = "androidx.test.espresso:espresso-core:3.2.0" + const val JUNIT = "androidx.test.ext:junit:1.1.1" + const val ESPRESSO = "androidx.test.espresso:espresso-core:3.2.0" } } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3556659..c0375fc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Jan 07 12:00:21 EST 2020 +#Fri May 29 19:00:53 EDT 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/mnemonic/build.gradle b/mnemonic/build.gradle index b3d61be..9cb2b03 100644 --- a/mnemonic/build.gradle +++ b/mnemonic/build.gradle @@ -8,7 +8,7 @@ dependencies { // Zcash implementation Deps.Zcash.ANDROID_WALLET_PLUGINS - implementation Deps.Misc.Plugins.ANDROID_BIP39 + implementation Deps.Zcash.KOTLIN_BIP39 testImplementation Deps.Test.JUNIT } diff --git a/mnemonic/src/main/java/cash/z/ecc/kotlin/mnemonic/Mnemonics.kt b/mnemonic/src/main/java/cash/z/ecc/kotlin/mnemonic/Mnemonics.kt index 28dfa81..0ffbd86 100644 --- a/mnemonic/src/main/java/cash/z/ecc/kotlin/mnemonic/Mnemonics.kt +++ b/mnemonic/src/main/java/cash/z/ecc/kotlin/mnemonic/Mnemonics.kt @@ -1,57 +1,22 @@ package cash.z.ecc.kotlin.mnemonic import cash.z.android.plugin.MnemonicPlugin +import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode import cash.z.ecc.android.bip39.Mnemonics.WordCount import cash.z.ecc.android.bip39.toEntropy import cash.z.ecc.android.bip39.toSeed import java.util.* +import java.util.Locale.ENGLISH import javax.inject.Inject -class Mnemonics @Inject constructor(): MnemonicPlugin { - override fun fullWordList(languageCode: String): List { - return cash.z.ecc.android.bip39.Mnemonics.getCachedWords(Locale.ENGLISH.language) - } - - override fun nextEntropy(): ByteArray { - return WordCount.COUNT_24.toEntropy() - } - - override fun nextMnemonic(): CharArray { - return nextMnemonic(nextEntropy()) - } - - override fun nextMnemonic(entropy: ByteArray): CharArray { - return MnemonicCode(entropy).chars - } - - override fun nextMnemonicList(): List { - return nextMnemonicList(nextEntropy()) - } - - override fun nextMnemonicList(entropy: ByteArray): List { - return MnemonicCode(entropy).map { it.toCharArray() } - } - - override fun toSeed(mnemonic: CharArray): ByteArray { - return MnemonicCode(mnemonic).toSeed() - } - - override fun toWordList(mnemonic: CharArray): List { - val wordList = mutableListOf() - var cursor = 0 - repeat(mnemonic.size) { i -> - val isSpace = mnemonic[i] == ' ' - if (isSpace || i == (mnemonic.size - 1)) { - val wordSize = i - cursor + if (isSpace) 0 else 1 - wordList.add(CharArray(wordSize).apply { - repeat(wordSize) { - this[it] = mnemonic[cursor + it] - } - }) - cursor = i + 1 - } - } - return wordList - } +class Mnemonics @Inject constructor() : MnemonicPlugin { + override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language) + override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy() + override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars + override fun nextMnemonic(entropy: ByteArray): CharArray = MnemonicCode(entropy).chars + override fun nextMnemonicList(): List = MnemonicCode(WordCount.COUNT_24).words + override fun nextMnemonicList(entropy: ByteArray): List = MnemonicCode(entropy).words + override fun toSeed(mnemonic: CharArray): ByteArray = MnemonicCode(mnemonic).toSeed() + override fun toWordList(mnemonic: CharArray): List = MnemonicCode(mnemonic).words } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 7d5ccd7..7878096 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,3 @@ rootProject.name='Zcash Wallet' -include ':app', ':qrecycler', ':feedback', ':mnemonic', ':lockbox', ':sdk', ':chipsinputlayout' -project(":sdk").projectDir = file("../zcash-android-wallet-sdk") +include ':app', ':qrecycler', ':feedback', ':mnemonic', ':lockbox', ':chipsinputlayout' project(":chipsinputlayout").projectDir = file("../../clones/chips-input-layout/library") From ebbe69125c21dd693f8a0035d3ea5b3cd5fcc58c Mon Sep 17 00:00:00 2001 From: Kevin Gorham Date: Wed, 10 Jun 2020 08:26:21 -0400 Subject: [PATCH 2/4] New: Provide checksum warning to user. If the user enters an invalid seed phrase, let them know immediately, per #120. --- app/src/main/java/cash/z/ecc/android/ext/Dialogs.kt | 12 ++++++++++++ .../cash/z/ecc/android/ui/setup/RestoreFragment.kt | 8 +++++++- .../z/ecc/android/ui/setup/WalletSetupViewModel.kt | 7 +++++++ .../java/cash/z/ecc/kotlin/mnemonic/Mnemonics.kt | 4 ++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/cash/z/ecc/android/ext/Dialogs.kt b/app/src/main/java/cash/z/ecc/android/ext/Dialogs.kt index 27f6f84..3d8f0ef 100644 --- a/app/src/main/java/cash/z/ecc/android/ext/Dialogs.kt +++ b/app/src/main/java/cash/z/ecc/android/ext/Dialogs.kt @@ -46,6 +46,18 @@ fun Context.showUninitializedError(error: Throwable? = null, onDismiss: () -> Un .show() } +fun Context.showInvalidSeedPhraseError(error: Throwable? = null, onDismiss: () -> Unit = {}): Dialog { + return MaterialAlertDialogBuilder(this) + .setTitle("Oops! Invalid Seed Phrase") + .setMessage("That seed phrase appears to be invalid! Please double-check it and try again.\n\n${error?.message ?: ""}") + .setCancelable(false) + .setPositiveButton("Retry") { dialog, _ -> + dialog.dismiss() + onDismiss() + } + .show() +} + fun Context.showScanFailure(error: Throwable?, onCancel: () -> Unit = {}, onDismiss: () -> Unit = {}): Dialog { val message = if (error == null) { "Unknown error" diff --git a/app/src/main/java/cash/z/ecc/android/ui/setup/RestoreFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/setup/RestoreFragment.kt index 389d4ad..40320e8 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/setup/RestoreFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/setup/RestoreFragment.kt @@ -17,6 +17,7 @@ import cash.z.ecc.android.R import cash.z.ecc.android.databinding.FragmentRestoreBinding import cash.z.ecc.android.di.viewmodel.activityViewModel import cash.z.ecc.android.ext.goneIf +import cash.z.ecc.android.ext.showInvalidSeedPhraseError import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report.Funnel.Restore import cash.z.ecc.android.feedback.Report.Tap.* @@ -118,7 +119,12 @@ class RestoreFragment : BaseFragment(), View.OnKeyListen if (birthdateString.isNullOrEmpty()) ZcashSdk.SAPLING_ACTIVATION_HEIGHT else birthdateString.toInt() }.coerceAtLeast(ZcashSdk.SAPLING_ACTIVATION_HEIGHT) - importWallet(seedPhrase, birthday) + try { + walletSetup.validatePhrase(seedPhrase) + importWallet(seedPhrase, birthday) + } catch (t: Throwable) { + mainActivity?.showInvalidSeedPhraseError(t) + } } private fun importWallet(seedPhrase: String, birthday: Int) { diff --git a/app/src/main/java/cash/z/ecc/android/ui/setup/WalletSetupViewModel.kt b/app/src/main/java/cash/z/ecc/android/ui/setup/WalletSetupViewModel.kt index 8d8a626..95c9a89 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/setup/WalletSetupViewModel.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/setup/WalletSetupViewModel.kt @@ -142,6 +142,13 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() { } } + /** + * Throw an exception if the seed phrase is bad. + */ + fun validatePhrase(seedPhrase: String) { + mnemonics.validate(seedPhrase.toCharArray()) + } + object LockBoxKey { const val SEED = "cash.z.ecc.android.SEED" const val SEED_PHRASE = "cash.z.ecc.android.SEED_PHRASE" diff --git a/mnemonic/src/main/java/cash/z/ecc/kotlin/mnemonic/Mnemonics.kt b/mnemonic/src/main/java/cash/z/ecc/kotlin/mnemonic/Mnemonics.kt index 0ffbd86..2d8e10b 100644 --- a/mnemonic/src/main/java/cash/z/ecc/kotlin/mnemonic/Mnemonics.kt +++ b/mnemonic/src/main/java/cash/z/ecc/kotlin/mnemonic/Mnemonics.kt @@ -19,4 +19,8 @@ class Mnemonics @Inject constructor() : MnemonicPlugin { override fun nextMnemonicList(entropy: ByteArray): List = MnemonicCode(entropy).words override fun toSeed(mnemonic: CharArray): ByteArray = MnemonicCode(mnemonic).toSeed() override fun toWordList(mnemonic: CharArray): List = MnemonicCode(mnemonic).words + + fun validate(mnemonic: CharArray) { + MnemonicCode(mnemonic).validate() + } } \ No newline at end of file From 340fb8c9938302cb7ff0ed02e54bb96df0ce55e3 Mon Sep 17 00:00:00 2001 From: Kevin Gorham Date: Wed, 10 Jun 2020 08:50:52 -0400 Subject: [PATCH 3/4] Address security finding #127 by validating address. This just needs to be tested on detail views with a lot of transactions to be sure that rapid scrolling doesn't cause too much backpressure. --- .../java/cash/z/ecc/android/ui/MainActivity.kt | 7 +++++++ .../android/ui/detail/TransactionViewHolder.kt | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt b/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt index 617a007..55f049b 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt @@ -224,6 +224,13 @@ class MainActivity : AppCompatActivity() { } } + suspend fun isValidAddress(address: String): Boolean { + try { + return !synchronizerComponent.synchronizer().validateAddress(address).isNotValid + } catch (t: Throwable) { } + return false + } + fun copyText(textToCopy: String, label: String = "zECC Wallet Text") { clipboard.setPrimaryClip( ClipData.newPlainText(label, textToCopy) diff --git a/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt b/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt index 9494555..2389a0c 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt @@ -3,6 +3,7 @@ package cash.z.ecc.android.ui.detail import android.view.View import android.widget.TextView import android.widget.Toast +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import cash.z.ecc.android.R import cash.z.ecc.android.ext.goneIf @@ -14,6 +15,7 @@ import cash.z.ecc.android.ui.util.toUtf8Memo import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.ext.* import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.launch import java.nio.charset.Charset import java.text.SimpleDateFormat import java.util.* @@ -27,8 +29,7 @@ class TransactionViewHolder(itemView: View) : Recycler private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault()) private val addressRegex = """zs\d\w{65,}""".toRegex() - fun bindTo(transaction: T?) { - + fun bindTo(transaction: T?) = (itemView.context as MainActivity).lifecycleScope.launch { // update view var lineOne: String = "" var lineTwo: String = "" @@ -97,19 +98,19 @@ class TransactionViewHolder(itemView: View) : Recycler shieldIcon.goneIf((transaction?.raw != null || transaction?.expiryHeight != null) && !transaction?.toAddress.isShielded()) } - private fun getSender(transaction: ConfirmedTransaction): String { + private suspend fun getSender(transaction: ConfirmedTransaction): String { val memo = transaction.memo.toUtf8Memo() return when { memo.contains(INCLUDE_MEMO_PREFIX) -> { - val address = memo.split(INCLUDE_MEMO_PREFIX)[1].trim() + val address = memo.split(INCLUDE_MEMO_PREFIX)[1].trim().validateAddress() ?: "Unknown" "${address.toAbbreviatedAddress()} paid you" } memo.contains("eply to:") -> { - val address = memo.split("eply to:")[1].trim() + val address = memo.split("eply to:")[1].trim().validateAddress() ?: "Unknown" "${address.toAbbreviatedAddress()} paid you" } memo.contains("zs") -> { - val who = extractAddress(memo)?.toAbbreviatedAddress() ?: "Unknown" + val who = extractAddress(memo).validateAddress()?.toAbbreviatedAddress() ?: "Unknown" "$who paid you" } else -> "Unknown paid you" @@ -145,6 +146,11 @@ class TransactionViewHolder(itemView: View) : Recycler (itemView.context as MainActivity).copyText(it, "Transaction Address") } } + + private suspend fun String?.validateAddress(): String? { + if (this == null) return null + return if ((itemView.context as MainActivity).isValidAddress(this)) this else null + } } private fun ByteArray.toTxId(): String { From de695678129c94373e91e028279aa049a718bc5b Mon Sep 17 00:00:00 2001 From: Kevin Gorham Date: Wed, 10 Jun 2020 10:01:58 -0400 Subject: [PATCH 4/4] Explicitly disable all feedback, dev info and crash reporting. Addresses https://github.com/zcash/zcash-android-wallet/issues/143 by placing everything behind a user setting that can be enabled in the future by users who want to continue helping us improve the user experience. For the most part, this will just be turned on for internal company releases in order to continue learning and improving the app. --- app/build.gradle | 10 +- app/src/main/AndroidManifest.xml | 2 + .../java/cash/z/ecc/android/ZcashWalletApp.kt | 1 - .../cash/z/ecc/android/di/module/AppModule.kt | 20 ++- .../android/feedback/FeedbackCrashlytics.kt | 4 +- .../cash/z/ecc/android/feedback/Report.kt | 1 + .../ui/detail/TransactionViewHolder.kt | 124 +++++++++--------- .../z/ecc/android/ui/scan/ScanFragment.kt | 4 +- .../ecc/android/ui/send/SendFinalFragment.kt | 5 +- .../z/ecc/android/ui/send/SendViewModel.kt | 3 +- .../z/ecc/android/ui/setup/BackupFragment.kt | 2 - build.gradle | 2 +- .../java/cash/z/ecc/android/Dependencies.kt | 5 +- .../android/feedback/FeedbackCoordinator.kt | 1 + 14 files changed, 96 insertions(+), 88 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1c44b4a..2293a2d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,8 +5,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' apply plugin: 'com.google.gms.google-services' -apply plugin: 'io.fabric' -apply plugin: 'com.google.firebase.firebase-perf' +apply plugin: 'com.google.firebase.crashlytics' //apply plugin: 'com.github.ben-manes.versions' archivesBaseName = 'zcash-android-wallet' @@ -102,10 +101,6 @@ android { } } -crashlytics { - enableNdk true -} - dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':qrecycler') @@ -161,9 +156,6 @@ dependencies { // Analytics (for dogfooding/crash-reporting/feedback only on internal team builds) implementation Deps.Analytics.CRASHLYTICS - implementation Deps.Analytics.CRASHLYTICS_NDK - implementation Deps.Analytics.FIREBASE - implementation Deps.Analytics.FIREBASE_PERF implementation Deps.Analytics.MIXPANEL // Misc. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7b5fa4f..099b1b7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,6 +33,8 @@ + + diff --git a/app/src/main/java/cash/z/ecc/android/ZcashWalletApp.kt b/app/src/main/java/cash/z/ecc/android/ZcashWalletApp.kt index fc3ab61..44ee9f9 100644 --- a/app/src/main/java/cash/z/ecc/android/ZcashWalletApp.kt +++ b/app/src/main/java/cash/z/ecc/android/ZcashWalletApp.kt @@ -37,7 +37,6 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider { override fun onCreate() { Thread.setDefaultUncaughtExceptionHandler(ExceptionReporter(Thread.getDefaultUncaughtExceptionHandler())) - Twig.plant(TroubleshootingTwig()) creationTime = System.currentTimeMillis() instance = this // Setup handler for uncaught exceptions. diff --git a/app/src/main/java/cash/z/ecc/android/di/module/AppModule.kt b/app/src/main/java/cash/z/ecc/android/di/module/AppModule.kt index 92870ce..948eabd 100644 --- a/app/src/main/java/cash/z/ecc/android/di/module/AppModule.kt +++ b/app/src/main/java/cash/z/ecc/android/di/module/AppModule.kt @@ -2,9 +2,14 @@ package cash.z.ecc.android.di.module import android.content.ClipboardManager import android.content.Context +import android.content.SharedPreferences import cash.z.ecc.android.ZcashWalletApp import cash.z.ecc.android.di.component.MainActivitySubcomponent import cash.z.ecc.android.feedback.* +import cash.z.ecc.android.sdk.ext.SilentTwig +import cash.z.ecc.android.sdk.ext.TroubleshootingTwig +import cash.z.ecc.android.sdk.ext.Twig +import com.google.firebase.crashlytics.FirebaseCrashlytics import dagger.Module import dagger.Provides import dagger.multibindings.IntoSet @@ -27,6 +32,11 @@ class AppModule { // Feedback // + @Provides + @Singleton + fun providePreferences(context: Context): SharedPreferences + = context.getSharedPreferences("Application", Context.MODE_PRIVATE) + @Provides @Singleton fun provideFeedback(): Feedback = Feedback() @@ -35,8 +45,16 @@ class AppModule { @Singleton fun provideFeedbackCoordinator( feedback: Feedback, + preferences: SharedPreferences, defaultObservers: Set<@JvmSuppressWildcards FeedbackCoordinator.FeedbackObserver> - ): FeedbackCoordinator = FeedbackCoordinator(feedback, defaultObservers) + ): FeedbackCoordinator { + return preferences.getBoolean(FeedbackCoordinator.ENABLED, false).let { isEnabled -> + // observe nothing unless feedback is enabled + FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(isEnabled) + Twig.plant(if (isEnabled) TroubleshootingTwig() else SilentTwig()) + FeedbackCoordinator(feedback, if (isEnabled) defaultObservers else setOf()) + } + } // diff --git a/app/src/main/java/cash/z/ecc/android/feedback/FeedbackCrashlytics.kt b/app/src/main/java/cash/z/ecc/android/feedback/FeedbackCrashlytics.kt index d3e5d30..ba146fb 100644 --- a/app/src/main/java/cash/z/ecc/android/feedback/FeedbackCrashlytics.kt +++ b/app/src/main/java/cash/z/ecc/android/feedback/FeedbackCrashlytics.kt @@ -1,6 +1,6 @@ package cash.z.ecc.android.feedback -import com.crashlytics.android.Crashlytics +import com.google.firebase.crashlytics.FirebaseCrashlytics class FeedbackCrashlytics : FeedbackCoordinator.FeedbackObserver { /** @@ -18,7 +18,7 @@ class FeedbackCrashlytics : FeedbackCoordinator.FeedbackObserver { ) else -> null } - exception?.let { Crashlytics.logException(it) } + exception?.let { FirebaseCrashlytics.getInstance().recordException(it) } } private class ReorgException(errorHeight: Int, rewindHeight: Int, reorgMesssage: String) : diff --git a/app/src/main/java/cash/z/ecc/android/feedback/Report.kt b/app/src/main/java/cash/z/ecc/android/feedback/Report.kt index b6899b3..f43f7a9 100644 --- a/app/src/main/java/cash/z/ecc/android/feedback/Report.kt +++ b/app/src/main/java/cash/z/ecc/android/feedback/Report.kt @@ -63,6 +63,7 @@ object Report { val errorHeight: Int by propertyMap val rewindHeight: Int by propertyMap } + class TxUpdateFailed(t: Throwable) : Feedback.AppError("txupdate", t, false) } } diff --git a/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt b/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt index 2389a0c..d0adffe 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/detail/TransactionViewHolder.kt @@ -29,73 +29,75 @@ class TransactionViewHolder(itemView: View) : Recycler private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault()) private val addressRegex = """zs\d\w{65,}""".toRegex() - fun bindTo(transaction: T?) = (itemView.context as MainActivity).lifecycleScope.launch { - // update view - var lineOne: String = "" - var lineTwo: String = "" - var amountZec: String = "" - var amountDisplay: String = "" - var amountColor: Int = R.color.text_light_dimmed - var lineOneColor: Int = R.color.text_light - var lineTwoColor: Int = R.color.text_light_dimmed - var indicatorBackground: Int = R.drawable.background_indicator_unknown + fun bindTo(transaction: T?) { + (itemView.context as MainActivity).lifecycleScope.launch { + // update view + var lineOne: String = "" + var lineTwo: String = "" + var amountZec: String = "" + var amountDisplay: String = "" + var amountColor: Int = R.color.text_light_dimmed + var lineOneColor: Int = R.color.text_light + var lineTwoColor: Int = R.color.text_light_dimmed + var indicatorBackground: Int = R.drawable.background_indicator_unknown - transaction?.apply { - itemView.setOnClickListener { - onTransactionClicked(this) - } - itemView.setOnLongClickListener { - onTransactionLongPressed(this) - true - } - amountZec = value.convertZatoshiToZecString() - // TODO: these might be good extension functions - val timestamp = formatter.format(blockTimeInSeconds * 1000L) - val isMined = blockTimeInSeconds != 0L - when { - !toAddress.isNullOrEmpty() -> { - lineOne = "You paid ${toAddress?.toAbbreviatedAddress()}" - lineTwo = if (isMined) "Sent $timestamp" else "Pending confirmation" - amountDisplay = "- $amountZec" - if (isMined) { - amountColor = R.color.zcashRed - indicatorBackground = R.drawable.background_indicator_outbound - } else { - lineOneColor = R.color.text_light_dimmed - lineTwoColor = R.color.text_light + transaction?.apply { + itemView.setOnClickListener { + onTransactionClicked(this) + } + itemView.setOnLongClickListener { + onTransactionLongPressed(this) + true + } + amountZec = value.convertZatoshiToZecString() + // TODO: these might be good extension functions + val timestamp = formatter.format(blockTimeInSeconds * 1000L) + val isMined = blockTimeInSeconds != 0L + when { + !toAddress.isNullOrEmpty() -> { + lineOne = "You paid ${toAddress?.toAbbreviatedAddress()}" + lineTwo = if (isMined) "Sent $timestamp" else "Pending confirmation" + amountDisplay = "- $amountZec" + if (isMined) { + amountColor = R.color.zcashRed + indicatorBackground = R.drawable.background_indicator_outbound + } else { + lineOneColor = R.color.text_light_dimmed + lineTwoColor = R.color.text_light + } + } + toAddress.isNullOrEmpty() && value > 0L && minedHeight > 0 -> { + lineOne = getSender(transaction) + lineTwo = "Received $timestamp" + amountDisplay = "+ $amountZec" + amountColor = R.color.zcashGreen + indicatorBackground = R.drawable.background_indicator_inbound + } + else -> { + lineOne = "Unknown" + lineTwo = "Unknown" + amountDisplay = "$amountZec" + amountColor = R.color.text_light } } - toAddress.isNullOrEmpty() && value > 0L && minedHeight > 0 -> { - lineOne = getSender(transaction) - lineTwo = "Received $timestamp" - amountDisplay = "+ $amountZec" - amountColor = R.color.zcashGreen - indicatorBackground = R.drawable.background_indicator_inbound - } - else -> { - lineOne = "Unknown" - lineTwo = "Unknown" - amountDisplay = "$amountZec" - amountColor = R.color.text_light + // sanitize amount + if (value < ZcashSdk.MINERS_FEE_ZATOSHI) amountDisplay = "< 0.001" + else if (amountZec.length > 10) { // 10 allows 3 digits to the left and 6 to the right of the decimal + amountDisplay = "tap to view" } } - // sanitize amount - if (value < ZcashSdk.MINERS_FEE_ZATOSHI) amountDisplay = "< 0.001" - else if (amountZec.length > 10) { // 10 allows 3 digits to the left and 6 to the right of the decimal - amountDisplay = "tap to view" - } + + + topText.text = lineOne + bottomText.text = lineTwo + amountText.text = amountDisplay + amountText.setTextColor(amountColor.toAppColor()) + topText.setTextColor(lineOneColor.toAppColor()) + bottomText.setTextColor(lineTwoColor.toAppColor()) + val context = itemView.context + indicator.background = context.resources.getDrawable(indicatorBackground) + shieldIcon.goneIf((transaction?.raw != null || transaction?.expiryHeight != null) && !transaction?.toAddress.isShielded()) } - - - topText.text = lineOne - bottomText.text = lineTwo - amountText.text = amountDisplay - amountText.setTextColor(amountColor.toAppColor()) - topText.setTextColor(lineOneColor.toAppColor()) - bottomText.setTextColor(lineTwoColor.toAppColor()) - val context = itemView.context - indicator.background = context.resources.getDrawable(indicatorBackground) - shieldIcon.goneIf((transaction?.raw != null || transaction?.expiryHeight != null) && !transaction?.toAddress.isShielded()) } private suspend fun getSender(transaction: ConfirmedTransaction): String { diff --git a/app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt index 67d22c6..07f152f 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt @@ -21,8 +21,8 @@ import cash.z.ecc.android.feedback.Report.Tap.SCAN_RECEIVE import cash.z.ecc.android.sdk.ext.twig import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.send.SendViewModel -import com.crashlytics.android.Crashlytics import com.google.common.util.concurrent.ListenableFuture +import com.google.firebase.crashlytics.FirebaseCrashlytics import kotlinx.coroutines.launch import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -102,7 +102,7 @@ class ScanFragment : BaseFragment() { preview.setSurfaceProvider(binding.preview.createSurfaceProvider()) } catch (t: Throwable) { // TODO: consider bubbling this up to the user - Crashlytics.logException(t) + mainActivity?.feedback?.report(t) twig("Error while opening the camera: $t") } diff --git a/app/src/main/java/cash/z/ecc/android/ui/send/SendFinalFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/send/SendFinalFragment.kt index 814c6b1..c4c11ea 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/send/SendFinalFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/send/SendFinalFragment.kt @@ -16,7 +16,6 @@ import cash.z.ecc.android.sdk.db.entity.* import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress import cash.z.ecc.android.sdk.ext.twig -import com.crashlytics.android.Crashlytics import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn @@ -112,8 +111,8 @@ class SendFinalFragment : BaseFragment() { } catch(t: Throwable) { val message = "ERROR: error while handling pending transaction update! $t" twig(message) - Crashlytics.log(message) - Crashlytics.logException(t) + mainActivity?.feedback?.report(Report.Error.NonFatal.TxUpdateFailed(t)) + mainActivity?.feedback?.report(t) } } diff --git a/app/src/main/java/cash/z/ecc/android/ui/send/SendViewModel.kt b/app/src/main/java/cash/z/ecc/android/ui/send/SendViewModel.kt index 9756f4b..5a01a9f 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/send/SendViewModel.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/send/SendViewModel.kt @@ -21,7 +21,6 @@ import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString import cash.z.ecc.android.sdk.ext.twig import cash.z.ecc.android.sdk.validate.AddressType -import com.crashlytics.android.Crashlytics import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -144,7 +143,7 @@ class SendViewModel @Inject constructor() : ViewModel() { report(metricId) } } catch (t: Throwable) { - Crashlytics.logException(RuntimeException("Error while updating Metrics", t)) + feedback.report(t) } } diff --git a/app/src/main/java/cash/z/ecc/android/ui/setup/BackupFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/setup/BackupFragment.kt index 9d5952b..7990411 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/setup/BackupFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/setup/BackupFragment.kt @@ -10,11 +10,9 @@ import android.widget.TextView import android.widget.Toast import androidx.activity.addCallback import androidx.lifecycle.lifecycleScope -import cash.z.ecc.android.R import cash.z.ecc.android.ZcashWalletApp import cash.z.ecc.android.databinding.FragmentBackupBinding import cash.z.ecc.android.di.viewmodel.activityViewModel -import cash.z.ecc.android.di.viewmodel.viewModel import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report.MetricType.SEED_PHRASE_LOADED import cash.z.ecc.android.feedback.Report.Tap.BACKUP_DONE diff --git a/build.gradle b/build.gradle index 7584f5d..a335c0f 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { classpath 'com.google.gms:google-services:4.3.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${Deps.kotlinVersion}" classpath 'io.fabric.tools:gradle:1.31.2' - classpath 'com.google.firebase:perf-plugin:1.3.1' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.1.1' } } diff --git a/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt b/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt index 0642076..d091f1b 100644 --- a/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt +++ b/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt @@ -61,10 +61,7 @@ object Deps { val STUB = "io.grpc:grpc-stub:$version" } object Analytics { // for dogfooding/crash-reporting/feedback only on internal team builds - val CRASHLYTICS = "com.crashlytics.sdk.android:crashlytics:2.10.1" - val CRASHLYTICS_NDK = "com.crashlytics.sdk.android:crashlytics-ndk:2.1.1" - val FIREBASE = "com.google.firebase:firebase-analytics:17.4.3" - val FIREBASE_PERF = "com.google.firebase:firebase-perf:19.0.7" + val CRASHLYTICS = "com.google.firebase:firebase-crashlytics:17.0.1" val MIXPANEL = "com.mixpanel.android:mixpanel-android:5.6.3" } object JavaX { diff --git a/feedback/src/main/java/cash/z/ecc/android/feedback/FeedbackCoordinator.kt b/feedback/src/main/java/cash/z/ecc/android/feedback/FeedbackCoordinator.kt index 133f1f1..36f1359 100644 --- a/feedback/src/main/java/cash/z/ecc/android/feedback/FeedbackCoordinator.kt +++ b/feedback/src/main/java/cash/z/ecc/android/feedback/FeedbackCoordinator.kt @@ -125,6 +125,7 @@ class FeedbackCoordinator(val feedback: Feedback, defaultObservers: Set