Merge pull request #1 from zcash/feature/scan-blocks-integration
Librustzcash integration
|
@ -0,0 +1,22 @@
|
||||||
|
Change Log
|
||||||
|
==========
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
- stop squishing the view when the keyboard opens
|
||||||
|
- dismiss keyboard on background tap
|
||||||
|
- display TAZ when on testnet
|
||||||
|
- added currency abbreviation after subheader text
|
||||||
|
- added error detection when address is too short
|
||||||
|
- disabled send button when errors exist
|
||||||
|
- updated dialog to reflect actual user input
|
||||||
|
- corrected toggle arrows from gray to black
|
||||||
|
- corrected corner radii from 8 to 6
|
||||||
|
- added paste button to apply default sample address for testing
|
||||||
|
- corrected address line color from black to 12% black
|
||||||
|
- corrected memo line color
|
||||||
|
- corrected memo 0 / 512 text color
|
||||||
|
- corrected memo background color
|
||||||
|
- corrected amount box background color
|
||||||
|
- updated send button to display TAZ (after toggling to "sending...")
|
||||||
|
- fixed a lot of edge cases around formatting numerical number entry and toggling
|
44
README.md
|
@ -1,2 +1,42 @@
|
||||||
# zcash-android-wallet-poc [![Build Status](https://app.bitrise.io/app/3f9040b242d98534/status.svg?token=AxoSmdULfUeBgW_GpS6VWg&branch=feature/revert-gradle-kotlin-dsl)](https://app.bitrise.io/app/3f9040b242d98534) [<img src="https://dply.me/c99ve9/button/small" alt="Download to device">](https://dply.me/c99ve9#install)
|
# Security Disclaimer
|
||||||
Proof of concept for a zcash wallet
|
|
||||||
|
#### :warning: WARNING: This is an *early preview* under active development and *anything may change at anytime!*
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
In the spirit of transparency, we provide this as a window into what we are actively developing. This is an alpha build, not yet intended for 3rd party use. Please be advised of the following:
|
||||||
|
|
||||||
|
* 🛑 This code currently is not audited 🛑
|
||||||
|
* ❌ This is a public, active branch with **no support**
|
||||||
|
* ❌ The code **does not have** documentation that is reviewed and approved by our Documentation team
|
||||||
|
* ❌ The code **does not have** adequate unit tests, acceptance tests and stress tests
|
||||||
|
* ❌ The code **does not have** automated tests that use the officially supported CI system
|
||||||
|
* ❌ The code **has not been subjected to thorough review** by engineers at the Electric Coin Company
|
||||||
|
* ❌ This product **does not run** compatibly with the latest version of zcashd
|
||||||
|
* ❌ The product **may be** majorly broken in several ways
|
||||||
|
* ❌ The app **only runs** on testnet
|
||||||
|
* ❌ The app **does not run** on mainnet and **cannot** run on regtest
|
||||||
|
* ❌ We **are actively changing** the codebase and adding features where/when needed
|
||||||
|
* ❌ We **do not** undertake appropriate security coverage (threat models, review, response, etc.)
|
||||||
|
* :heavy_check_mark: There is a product manager for this app
|
||||||
|
* :heavy_check_mark: Electric Coin Company maintains the app as we discover bugs and do network upgrades/minor releases
|
||||||
|
* :heavy_check_mark: Users can expect to get a response within a few weeks after submitting an issue
|
||||||
|
* ❌ The User Support team **had not yet been briefed** on the features provided to users and the functionality of the associated test-framework
|
||||||
|
* ❌ The code is **unpolished**
|
||||||
|
* ❌ The code is **not documented**
|
||||||
|
* ❌ The code **is not yet published** (to Bintray/Maven Central)
|
||||||
|
* ❌ Requires external lightwalletd server
|
||||||
|
|
||||||
|
|
||||||
|
### 🛑 Use of this code may lead to a loss of funds 🛑
|
||||||
|
|
||||||
|
Use of this code in its current form or with modifications may lead to loss of funds, loss of "expected" privacy, or denial of service for a large portion of users, or a bug which could leverage any of those kinds of attacks (especially a "0 day" where we suspect few people know about the vulnerability).
|
||||||
|
|
||||||
|
### :eyes: At this time, this is for preview purposes only. :eyes:
|
||||||
|
|
||||||
|
# Zcash Android Reference Wallet - Proof of Concept
|
||||||
|
[![Build Status](https://app.bitrise.io/app/3f9040b242d98534/status.svg?token=AxoSmdULfUeBgW_GpS6VWg&branch=feature/revert-gradle-kotlin-dsl)](https://app.bitrise.io/app/3f9040b242d98534) [<img src="https://dply.me/n4br57/button/small" alt="Download to device">](https://dply.me/n4br57#install)
|
||||||
|
|
||||||
|
Proof of concept for shielded-only Zcash wallet. Additional documentation will be added in a future milestone.
|
||||||
|
|
||||||
|
[![Alt text](https://img.youtube.com/vi/BgNO5Wn-9r0/0.jpg)](https://www.youtube.com/watch?v=BgNO5Wn-9r0)
|
||||||
|
|
|
@ -6,6 +6,7 @@ apply plugin: 'kotlin-android-extensions'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'deploygate'
|
apply plugin: 'deploygate'
|
||||||
apply plugin: 'com.github.ben-manes.versions'
|
apply plugin: 'com.github.ben-manes.versions'
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion buildConfig.compileSdkVersion
|
compileSdkVersion buildConfig.compileSdkVersion
|
||||||
|
@ -13,21 +14,53 @@ android {
|
||||||
applicationId "cash.z.android.wallet"
|
applicationId "cash.z.android.wallet"
|
||||||
minSdkVersion buildConfig.minSdkVersion
|
minSdkVersion buildConfig.minSdkVersion
|
||||||
targetSdkVersion buildConfig.targetSdkVersion
|
targetSdkVersion buildConfig.targetSdkVersion
|
||||||
versionCode 17 // todo: change this to 1_00_04 format, once we graduate beyond zero for the major version number because leading zeros indicate on octal number.
|
versionCode 20 // todo: change this to 1_00_04 format, once we graduate beyond zero for the major version number because leading zeros indicate on octal number.
|
||||||
versionName "0.2.5-alpha"
|
versionName "0.6.1-alpha"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
multiDexEnabled true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dataBinding {
|
||||||
|
enabled true
|
||||||
|
}
|
||||||
|
|
||||||
|
flavorDimensions 'network'
|
||||||
|
productFlavors {
|
||||||
|
// product flavor names cannot start with the word "test" because they would clash with other targets
|
||||||
|
zcashtestnet {
|
||||||
|
dimension 'network'
|
||||||
|
applicationId 'cash.z.android.wallet.testnet'
|
||||||
|
matchingFallbacks = ['debug', 'zcashtestnet']
|
||||||
|
}
|
||||||
|
|
||||||
|
zcashmainnet {
|
||||||
|
dimension 'network'
|
||||||
|
applicationId 'cash.z.android.wallet.mainnet'
|
||||||
|
matchingFallbacks = ['release', 'zcashmainnet']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
|
mock {
|
||||||
|
initWith debug
|
||||||
|
matchingFallbacks = ['debug', 'release', 'zcashtestnet']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
lintConfig file("zcash-lint-options.xml")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -41,11 +74,28 @@ dependencies {
|
||||||
implementation deps.androidx.navigation.fragmentKtx
|
implementation deps.androidx.navigation.fragmentKtx
|
||||||
implementation deps.androidx.navigation.ui
|
implementation deps.androidx.navigation.ui
|
||||||
implementation deps.androidx.navigation.uiKtx
|
implementation deps.androidx.navigation.uiKtx
|
||||||
implementation deps.kotlin.stdlib
|
|
||||||
implementation deps.material
|
implementation deps.material
|
||||||
|
implementation deps.androidx.multidex
|
||||||
|
|
||||||
|
// Kotlin
|
||||||
|
implementation deps.kotlin.stdlib
|
||||||
|
implementation deps.kotlin.reflect
|
||||||
|
implementation deps.kotlin.coroutines.core
|
||||||
|
implementation deps.kotlin.coroutines.android
|
||||||
|
|
||||||
// Zcash
|
// Zcash
|
||||||
implementation deps.zcash.walletSdk
|
implementation deps.zcash.walletSdk
|
||||||
|
implementation project(path: ':qrecycler')
|
||||||
|
|
||||||
|
// TODO: get the AAR to provide these
|
||||||
|
implementation "io.grpc:grpc-okhttp:1.19.0"
|
||||||
|
implementation "io.grpc:grpc-protobuf-lite:1.19.0"
|
||||||
|
implementation "io.grpc:grpc-stub:1.19.0"
|
||||||
|
implementation 'javax.annotation:javax.annotation-api:1.3.2'
|
||||||
|
implementation "androidx.room:room-runtime:2.1.0-alpha06"
|
||||||
|
implementation "androidx.room:room-common:2.1.0-alpha06"
|
||||||
|
implementation 'com.google.guava:guava:27.0.1-android'
|
||||||
|
kapt "androidx.room:room-compiler:2.1.0-alpha06"
|
||||||
|
|
||||||
// Dagger
|
// Dagger
|
||||||
implementation deps.dagger.android.support
|
implementation deps.dagger.android.support
|
||||||
|
@ -54,9 +104,17 @@ dependencies {
|
||||||
|
|
||||||
// Other
|
// Other
|
||||||
implementation deps.speeddial
|
implementation deps.speeddial
|
||||||
|
implementation deps.lottie
|
||||||
|
debugImplementation deps.stetho
|
||||||
|
mockImplementation deps.stetho
|
||||||
|
|
||||||
|
testImplementation deps.mockito.jupiter
|
||||||
|
testImplementation deps.mockito.kotlin
|
||||||
|
testImplementation deps.junit5.api
|
||||||
|
testImplementation deps.junit5.engine
|
||||||
|
testImplementation deps.junit5.migrationsupport
|
||||||
|
|
||||||
testImplementation deps.junit
|
|
||||||
androidTestImplementation deps.androidx.test.runner
|
androidTestImplementation deps.androidx.test.runner
|
||||||
androidTestImplementation deps.androidx.test.espresso
|
androidTestImplementation deps.androidx.test.espresso
|
||||||
compile project(path: ':qrecycler')
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
package cash.z.android.wallet.di.module
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.preference.PreferenceManager
|
||||||
|
import cash.z.android.wallet.BuildConfig
|
||||||
|
import cash.z.android.wallet.ZcashWalletApplication
|
||||||
|
import cash.z.android.wallet.sample.*
|
||||||
|
import cash.z.android.wallet.sample.SampleProperties.COMPACT_BLOCK_PORT
|
||||||
|
import cash.z.android.wallet.sample.SampleProperties.COMPACT_BLOCK_SERVER
|
||||||
|
import cash.z.android.wallet.sample.SampleProperties.PREFS_SERVER_NAME
|
||||||
|
import cash.z.android.wallet.sample.SampleProperties.PREFS_WALLET_DISPLAY_NAME
|
||||||
|
import cash.z.wallet.sdk.data.*
|
||||||
|
import cash.z.wallet.sdk.jni.JniConverter
|
||||||
|
import cash.z.wallet.sdk.secure.Wallet
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import javax.inject.Named
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module that contributes all the objects necessary for the synchronizer, which is basically everything that has
|
||||||
|
* application scope.
|
||||||
|
*/
|
||||||
|
@Module
|
||||||
|
internal object SynchronizerModule {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providePrefs(): SharedPreferences {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(ZcashWalletApplication.instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideWalletConfig(prefs: SharedPreferences): WalletConfig {
|
||||||
|
val walletName = prefs.getString(PREFS_WALLET_DISPLAY_NAME, null)
|
||||||
|
twig("FOUND WALLET DISPLAY NAME : $walletName")
|
||||||
|
return when(walletName) {
|
||||||
|
AliceWallet.displayName -> AliceWallet
|
||||||
|
BobWallet.displayName, null -> BobWallet // Default wallet
|
||||||
|
CarolWallet.displayName -> CarolWallet
|
||||||
|
DaveWallet.displayName -> DaveWallet
|
||||||
|
else -> WalletConfig.create(walletName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@Named(PREFS_SERVER_NAME)
|
||||||
|
fun provideServer(prefs: SharedPreferences): String {
|
||||||
|
val serverName = prefs.getString(PREFS_SERVER_NAME, null)
|
||||||
|
// in theory, the actual stored value itself could be null so provide the default this way to be safe
|
||||||
|
val server = Servers.values().firstOrNull { it.displayName == serverName }?.host ?: COMPACT_BLOCK_SERVER //TODO: validate that this is a hostname or IP. For now use default, instead
|
||||||
|
twig("FOUND SERVER DISPLAY NAME : $serverName ($server)")
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideTwig(): Twig = TroubleshootingTwig() // troubleshoot on debug, silent on release
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideDownloader(@Named(PREFS_SERVER_NAME) server: String, twigger: Twig): CompactBlockStream {
|
||||||
|
return CompactBlockStream(server, COMPACT_BLOCK_PORT, twigger)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideProcessor(application: ZcashWalletApplication, converter: JniConverter, walletConfig: WalletConfig, twigger: Twig): CompactBlockProcessor {
|
||||||
|
return CompactBlockProcessor(application, converter, walletConfig.cacheDbName, walletConfig.dataDbName, logger = twigger)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideRepository(application: ZcashWalletApplication, walletConfig: WalletConfig, converter: JniConverter): TransactionRepository {
|
||||||
|
return PollingTransactionRepository(application, walletConfig.dataDbName, 10_000L)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideWallet(application: ZcashWalletApplication, walletConfig: WalletConfig, converter: JniConverter): Wallet {
|
||||||
|
return Wallet(
|
||||||
|
context = application,
|
||||||
|
converter = converter,
|
||||||
|
dataDbPath = application.getDatabasePath(walletConfig.dataDbName).absolutePath,
|
||||||
|
paramDestinationDir = "${application.cacheDir.absolutePath}/params",
|
||||||
|
seedProvider = walletConfig.seedProvider,
|
||||||
|
spendingKeyProvider = walletConfig.spendingKeyProvider
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideManager(wallet: Wallet, repository: TransactionRepository, downloader: CompactBlockStream, twigger: Twig): ActiveTransactionManager {
|
||||||
|
return ActiveTransactionManager(repository, downloader.connection, wallet, twigger)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideJniConverter(): JniConverter {
|
||||||
|
return JniConverter().also {
|
||||||
|
if (BuildConfig.DEBUG) it.initLogs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideSynchronizer(
|
||||||
|
downloader: CompactBlockStream,
|
||||||
|
processor: CompactBlockProcessor,
|
||||||
|
repository: TransactionRepository,
|
||||||
|
manager: ActiveTransactionManager,
|
||||||
|
wallet: Wallet
|
||||||
|
): Synchronizer {
|
||||||
|
return SdkSynchronizer(
|
||||||
|
downloader,
|
||||||
|
processor,
|
||||||
|
repository,
|
||||||
|
manager,
|
||||||
|
wallet,
|
||||||
|
batchSize = 100,
|
||||||
|
blockPollFrequency = 50_000L
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,8 +1,14 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:dist="http://schemas.android.com/apk/distribution"
|
xmlns:dist="http://schemas.android.com/apk/distribution"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="cash.z.android.wallet">
|
package="cash.z.android.wallet">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<!--<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />-->
|
||||||
|
<!--<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />-->
|
||||||
|
|
||||||
<dist:module dist:instant="true" />
|
<dist:module dist:instant="true" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
@ -12,11 +18,13 @@
|
||||||
android:roundIcon="@mipmap/ic_launcher_shield_round"
|
android:roundIcon="@mipmap/ic_launcher_shield_round"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/ZcashTheme">
|
android:theme="@style/ZcashTheme"
|
||||||
|
tools:replace="android:name">
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.activity.MainActivity"
|
android:name=".ui.activity.MainActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/ZcashTheme.NoActionBar"
|
android:theme="@style/ZcashTheme.NoActionBar"
|
||||||
|
android:windowSoftInputMode="adjustPan"
|
||||||
android:screenOrientation="portrait">
|
android:screenOrientation="portrait">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
@ -25,6 +33,8 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".ui.util.CameraQrScanner" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -1,15 +1,22 @@
|
||||||
package cash.z.android.wallet
|
package cash.z.android.wallet
|
||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.Context
|
||||||
|
import androidx.multidex.MultiDex
|
||||||
import cash.z.android.wallet.di.component.DaggerApplicationComponent
|
import cash.z.android.wallet.di.component.DaggerApplicationComponent
|
||||||
|
import cash.z.wallet.sdk.data.TroubleshootingTwig
|
||||||
|
import cash.z.wallet.sdk.data.Twig
|
||||||
|
import com.facebook.stetho.Stetho
|
||||||
import dagger.android.AndroidInjector
|
import dagger.android.AndroidInjector
|
||||||
import dagger.android.DaggerApplication
|
import dagger.android.DaggerApplication
|
||||||
|
|
||||||
|
|
||||||
class ZcashWalletApplication : DaggerApplication() {
|
class ZcashWalletApplication : DaggerApplication() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
instance = this
|
instance = this
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
Stetho.initializeWithDefaults(this)
|
||||||
|
Twig.plant(TroubleshootingTwig())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -19,6 +26,11 @@ class ZcashWalletApplication : DaggerApplication() {
|
||||||
return DaggerApplicationComponent.builder().create(this)
|
return DaggerApplicationComponent.builder().create(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
MultiDex.install(this)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var instance: ZcashWalletApplication
|
lateinit var instance: ZcashWalletApplication
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package cash.z.android.wallet.data
|
||||||
|
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import cash.z.wallet.sdk.dao.WalletTransaction
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
|
import kotlinx.coroutines.channels.produce
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import kotlin.math.roundToLong
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.random.nextInt
|
||||||
|
import kotlin.random.nextLong
|
||||||
|
|
||||||
|
class SampleTransactionRepository(val scope: CoroutineScope) : TransactionRepository {
|
||||||
|
/**
|
||||||
|
* Just send a sample stream of balances, every so often
|
||||||
|
*/
|
||||||
|
override fun balance() = scope.produce {
|
||||||
|
var currentBalance = 0.0
|
||||||
|
while (isActive) {
|
||||||
|
send(BigDecimal(currentBalance))
|
||||||
|
delay(500)
|
||||||
|
currentBalance += 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just send a sample stream of transactions, every so often
|
||||||
|
*/
|
||||||
|
override fun transactions(): ReceiveChannel<WalletTransaction> = scope.produce {
|
||||||
|
var oldestTimestamp = System.currentTimeMillis() - (4 * DateUtils.WEEK_IN_MILLIS)
|
||||||
|
while (isActive) {
|
||||||
|
delay(1500L)
|
||||||
|
send(createSampleTransaction(oldestTimestamp).also { oldestTimestamp = it.timeInSeconds * 1000 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSampleTransaction(oldestTimestamp: Long): WalletTransaction {
|
||||||
|
// up to 20% of the delta
|
||||||
|
val upperBound = System.currentTimeMillis() + Math.round(0.2 * (System.currentTimeMillis() - oldestTimestamp))
|
||||||
|
val txId = Random.nextLong(0L..(Long.MAX_VALUE - 1L))
|
||||||
|
val value = Random.nextLong(1L..1_500_000_000L) - 750_000_000L
|
||||||
|
val height = Random.nextInt(0..(Int.MAX_VALUE - 1))
|
||||||
|
val isSend = value > 0L
|
||||||
|
val time = Random.nextLong(oldestTimestamp..upperBound)
|
||||||
|
val isMined = Random.nextBoolean()
|
||||||
|
return WalletTransaction(
|
||||||
|
txId = txId,
|
||||||
|
value = value,
|
||||||
|
height = height,
|
||||||
|
isSend = isSend,
|
||||||
|
timeInSeconds = time/1000,
|
||||||
|
isMined = isMined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package cash.z.android.wallet.data
|
||||||
|
|
||||||
|
import cash.z.wallet.sdk.dao.WalletTransaction
|
||||||
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
interface TransactionRepository {
|
||||||
|
fun balance(): ReceiveChannel<BigDecimal>
|
||||||
|
fun transactions(): ReceiveChannel<WalletTransaction>
|
||||||
|
}
|
|
@ -3,4 +3,5 @@ package cash.z.android.wallet.di.annotation
|
||||||
import javax.inject.Scope
|
import javax.inject.Scope
|
||||||
|
|
||||||
@Scope
|
@Scope
|
||||||
annotation class ActivityScoped
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
annotation class ActivityScope
|
|
@ -0,0 +1,7 @@
|
||||||
|
package cash.z.android.wallet.di.annotation
|
||||||
|
|
||||||
|
import javax.inject.Scope
|
||||||
|
|
||||||
|
@Scope
|
||||||
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
annotation class ApplicationScope
|
|
@ -3,4 +3,5 @@ package cash.z.android.wallet.di.annotation
|
||||||
import javax.inject.Scope
|
import javax.inject.Scope
|
||||||
|
|
||||||
@Scope
|
@Scope
|
||||||
annotation class ApplicationScoped
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
annotation class FragmentScope
|
|
@ -3,8 +3,8 @@ package cash.z.android.wallet.di.component
|
||||||
import cash.z.android.wallet.ui.activity.MainActivityModule
|
import cash.z.android.wallet.ui.activity.MainActivityModule
|
||||||
import cash.z.android.wallet.ZcashWalletApplication
|
import cash.z.android.wallet.ZcashWalletApplication
|
||||||
import cash.z.android.wallet.di.module.ApplicationModule
|
import cash.z.android.wallet.di.module.ApplicationModule
|
||||||
import cash.z.android.wallet.ui.fragment.HomeFragmentModule
|
import cash.z.android.wallet.di.module.SynchronizerModule
|
||||||
import cash.z.android.wallet.ui.fragment.ReceiveFragmentModule
|
import cash.z.android.wallet.ui.fragment.*
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
import dagger.android.AndroidInjector
|
import dagger.android.AndroidInjector
|
||||||
import dagger.android.support.AndroidSupportInjectionModule
|
import dagger.android.support.AndroidSupportInjectionModule
|
||||||
|
@ -19,9 +19,22 @@ import javax.inject.Singleton
|
||||||
modules = [
|
modules = [
|
||||||
AndroidSupportInjectionModule::class,
|
AndroidSupportInjectionModule::class,
|
||||||
ApplicationModule::class,
|
ApplicationModule::class,
|
||||||
|
SynchronizerModule::class,
|
||||||
MainActivityModule::class,
|
MainActivityModule::class,
|
||||||
|
|
||||||
|
// Injected Fragments
|
||||||
HomeFragmentModule::class,
|
HomeFragmentModule::class,
|
||||||
ReceiveFragmentModule::class
|
AboutFragmentModule::class,
|
||||||
|
HistoryFragmentModule::class,
|
||||||
|
WelcomeFragmentModule::class,
|
||||||
|
ReceiveFragmentModule::class,
|
||||||
|
RequestFragmentModule::class,
|
||||||
|
SendFragmentModule::class,
|
||||||
|
ScanFragmentModule::class,
|
||||||
|
SettingsFragmentModule::class,
|
||||||
|
WelcomeFragmentModule::class,
|
||||||
|
FirstrunFragmentModule::class,
|
||||||
|
SyncFragmentModule::class
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
interface ApplicationComponent : AndroidInjector<ZcashWalletApplication> {
|
interface ApplicationComponent : AndroidInjector<ZcashWalletApplication> {
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
package cash.z.android.wallet.di.module
|
package cash.z.android.wallet.di.module
|
||||||
|
|
||||||
import cash.z.android.qrecycler.QRecycler
|
import cash.z.android.qrecycler.QRecycler
|
||||||
|
import cash.z.android.wallet.ui.fragment.HomeFragment
|
||||||
|
import cash.z.android.wallet.ui.presenter.HomePresenter
|
||||||
|
import cash.z.wallet.sdk.data.Synchronizer
|
||||||
import cash.z.wallet.sdk.jni.JniConverter
|
import cash.z.wallet.sdk.jni.JniConverter
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module that contributes all the objects with application scope. Anything that should live globally belongs here.
|
* Module that contributes all the objects with application scope. Anything that should live globally belongs here.
|
||||||
|
@ -18,7 +22,4 @@ internal object ApplicationModule {
|
||||||
@Provides
|
@Provides
|
||||||
fun provideQRecycler(): QRecycler = QRecycler()
|
fun provideQRecycler(): QRecycler = QRecycler()
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@Provides
|
|
||||||
fun provideJniConverter(): JniConverter = JniConverter()
|
|
||||||
}
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package cash.z.android.wallet.extention
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
|
||||||
|
internal val NO_ACTION = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls context.alert with the given string.
|
||||||
|
*/
|
||||||
|
internal fun Context.alert(
|
||||||
|
@StringRes messageResId: Int,
|
||||||
|
@StringRes positiveButtonResId: Int = android.R.string.ok,
|
||||||
|
@StringRes negativeButtonResId: Int = android.R.string.cancel,
|
||||||
|
negativeAction: () -> Unit = NO_ACTION,
|
||||||
|
positiveAction: () -> Unit = NO_ACTION
|
||||||
|
) {
|
||||||
|
alert(
|
||||||
|
message = getString(messageResId),
|
||||||
|
positiveButtonResId = positiveButtonResId,
|
||||||
|
negativeButtonResId = negativeButtonResId,
|
||||||
|
negativeAction = negativeAction,
|
||||||
|
positiveAction = positiveAction
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an alert with the given message, if the block exists, it will execute after the user clicks the positive button,
|
||||||
|
* while clicking the negative button will abort the block. If no block exists, there will only be a positive button.
|
||||||
|
*/
|
||||||
|
internal fun Context.alert(
|
||||||
|
message: String,
|
||||||
|
@StringRes positiveButtonResId: Int = android.R.string.ok,
|
||||||
|
@StringRes negativeButtonResId: Int = android.R.string.cancel,
|
||||||
|
negativeAction: (() -> Unit) = NO_ACTION,
|
||||||
|
positiveAction: (() -> Unit) = NO_ACTION
|
||||||
|
) {
|
||||||
|
val builder = AlertDialog.Builder(this)
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton(positiveButtonResId) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
positiveAction()
|
||||||
|
}
|
||||||
|
if (positiveAction !== NO_ACTION || negativeAction !== NO_ACTION) {
|
||||||
|
builder.setNegativeButton(negativeButtonResId) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
negativeAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.show()
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package cash.z.android.wallet.extention
|
||||||
|
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.EditText
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
|
||||||
|
inline fun EditText.afterTextChanged(crossinline block: (String) -> Unit) {
|
||||||
|
this.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
block.invoke(s.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun EditText.doOnDone(crossinline block: (String) -> Unit) {
|
||||||
|
setOnEditorActionListener { v, actionId, _ ->
|
||||||
|
return@setOnEditorActionListener if ((actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||||
|
v.clearFocus()
|
||||||
|
// v.clearComposingText()
|
||||||
|
v.context.getSystemService<InputMethodManager>()
|
||||||
|
?.hideSoftInputFromWindow(v.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||||
|
block(this.text.toString())
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun EditText.doOnFocusLost(crossinline block: (String) -> Unit) {
|
||||||
|
setOnFocusChangeListener { _, hasFocus ->
|
||||||
|
if (!hasFocus) block(this.text.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun EditText.doOnDoneOrFocusLost(crossinline block: (String) -> Unit) {
|
||||||
|
doOnDone(block)
|
||||||
|
doOnFocusLost(block)
|
||||||
|
}
|
|
@ -6,4 +6,8 @@ internal inline fun tryIgnore(block: () -> Unit) {
|
||||||
|
|
||||||
internal inline fun <T> tryNull(block: () -> T): T? {
|
internal inline fun <T> tryNull(block: () -> T): T? {
|
||||||
return try { block() } catch(ignored: Throwable) { null }
|
return try { block() } catch(ignored: Throwable) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline fun String.truncate(): String {
|
||||||
|
return "${substring(0..4)}...${substring(length-5, length)}"
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ package cash.z.android.wallet.extention
|
||||||
|
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.annotation.IntegerRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import cash.z.android.wallet.ZcashWalletApplication
|
import cash.z.android.wallet.ZcashWalletApplication
|
||||||
|
@ -20,3 +21,10 @@ internal inline fun @receiver:ColorRes Int.toAppColor(): Int {
|
||||||
internal inline fun @receiver:StringRes Int.toAppString(): String {
|
internal inline fun @receiver:StringRes Int.toAppString(): String {
|
||||||
return ZcashWalletApplication.instance.getString(this)}
|
return ZcashWalletApplication.instance.getString(this)}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grab an integer from the application resources
|
||||||
|
*/
|
||||||
|
internal inline fun @receiver:IntegerRes Int.toAppInt(): Int {
|
||||||
|
return ZcashWalletApplication.instance.resources.getInteger(this)}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package cash.z.android.wallet.extention
|
||||||
|
|
||||||
|
import android.text.format.DateUtils.SECOND_IN_MILLIS
|
||||||
|
import android.text.format.DateUtils.getRelativeTimeSpanString
|
||||||
|
|
||||||
|
internal inline fun Long.toRelativeTimeString(): CharSequence {
|
||||||
|
return getRelativeTimeSpanString(
|
||||||
|
this,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
SECOND_IN_MILLIS
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package cash.z.android.wallet.extention
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import cash.z.android.wallet.R
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a snackbar with an "OK" button
|
||||||
|
*/
|
||||||
|
internal inline fun Snackbar?.showOk(view: View, message: String): Snackbar {
|
||||||
|
return if (this == null) {
|
||||||
|
Snackbar.make(view, "$message", Snackbar.LENGTH_INDEFINITE)
|
||||||
|
.setAction(view.context.getString(R.string.ok_allcaps)){/*auto-close*/}
|
||||||
|
} else {
|
||||||
|
setText(message)
|
||||||
|
}.also {
|
||||||
|
if (!it.isShownOrQueued) it.show()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package cash.z.android.wallet.sample
|
||||||
|
|
||||||
|
import android.provider.Settings
|
||||||
|
import cash.z.wallet.sdk.data.SampleSeedProvider
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.math.MathContext
|
||||||
|
import kotlin.properties.ReadOnlyProperty
|
||||||
|
import kotlin.properties.ReadWriteProperty
|
||||||
|
|
||||||
|
interface WalletConfig {
|
||||||
|
val displayName: String
|
||||||
|
val seedName: String
|
||||||
|
val seedProvider: ReadOnlyProperty<kotlin.Any?, kotlin.ByteArray>
|
||||||
|
val spendingKeyProvider: ReadWriteProperty<Any?, String>
|
||||||
|
val cacheDbName: String
|
||||||
|
val dataDbName: String
|
||||||
|
val defaultSendAddress: String
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun create(name: String): WalletConfig {
|
||||||
|
return object : WalletConfig {
|
||||||
|
override val displayName = name
|
||||||
|
override val seedName = "test.reference.${name}_${Settings.Secure.ANDROID_ID}".sanitizeName()
|
||||||
|
override val seedProvider = SampleSeedProvider(seedName)
|
||||||
|
override val spendingKeyProvider = SampleSpendingKeySharedPref(seedName)
|
||||||
|
override val cacheDbName = "test_cache_${name.sanitizeName()}.db"
|
||||||
|
override val dataDbName = "test_data_${name.sanitizeName()}.db"
|
||||||
|
override val defaultSendAddress = BobWallet.defaultSendAddress // send to Alice by default, in other words, behave like Bob, which is the default wallet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline fun String.sanitizeName(): String {
|
||||||
|
return this.toLowerCase().filter {
|
||||||
|
it in 'a'..'z' || it in '0'..'9'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object AliceWallet : WalletConfig {
|
||||||
|
override val displayName = "Alice"
|
||||||
|
override val seedName = "test.reference.$displayName".sanitizeName()
|
||||||
|
override val seedProvider = SampleSeedProvider(seedName)
|
||||||
|
override val spendingKeyProvider = SampleSpendingKeySharedPref(seedName)
|
||||||
|
override val cacheDbName = "test_cache_${displayName.sanitizeName()}.db"
|
||||||
|
override val dataDbName = "test_data_${displayName.sanitizeName()}.db"
|
||||||
|
override val defaultSendAddress =
|
||||||
|
"ztestsapling1wrjqt8et9elq7p0ejlgfpt4j9m7r7d4qlt7cke7ppp7dwrpev3yln30c37mrnzzekceajk66h0n" // bob's address
|
||||||
|
}
|
||||||
|
|
||||||
|
object BobWallet : WalletConfig {
|
||||||
|
override val displayName = "Bob"
|
||||||
|
override val seedName = "test.reference.$displayName".sanitizeName()
|
||||||
|
override val seedProvider = SampleSeedProvider(seedName)
|
||||||
|
override val spendingKeyProvider = SampleSpendingKeySharedPref(seedName)
|
||||||
|
override val cacheDbName = "test_cache_${displayName.sanitizeName()}.db"
|
||||||
|
override val dataDbName = "test_data_${displayName.sanitizeName()}.db"
|
||||||
|
override val defaultSendAddress =
|
||||||
|
"ztestsapling12pxv67r0kdw58q8tcn8kxhfy9n4vgaa7q8vp0dg24aueuz2mpgv2x7mw95yetcc37efc6q3hewn" // alice's address
|
||||||
|
}
|
||||||
|
|
||||||
|
object CarolWallet : WalletConfig {
|
||||||
|
override val displayName = "Carol"
|
||||||
|
override val seedName = "test.reference.$displayName".sanitizeName()
|
||||||
|
override val seedProvider = SampleSeedProvider(seedName)
|
||||||
|
override val spendingKeyProvider = SampleSpendingKeySharedPref(seedName)
|
||||||
|
override val cacheDbName = "test_cache_${displayName.sanitizeName()}.db"
|
||||||
|
override val dataDbName = "test_data_${displayName.sanitizeName()}.db"
|
||||||
|
override val defaultSendAddress =
|
||||||
|
"ztestsapling1y480yqw6h7lwmvw9wsn3h2xxg0np93cv8nq0j3m6g8edc79faevq5adrtzyxgsmu9jfc2hdf6al" // dave's address
|
||||||
|
}
|
||||||
|
|
||||||
|
object DaveWallet : WalletConfig {
|
||||||
|
override val displayName = "Dave"
|
||||||
|
override val seedName = "test.reference.$displayName".sanitizeName()
|
||||||
|
override val seedProvider = SampleSeedProvider(seedName)
|
||||||
|
override val spendingKeyProvider = SampleSpendingKeySharedPref(seedName)
|
||||||
|
override val cacheDbName = "test_cache_${displayName.sanitizeName()}.db"
|
||||||
|
override val dataDbName = "test_data_${displayName.sanitizeName()}.db"
|
||||||
|
override val defaultSendAddress =
|
||||||
|
"ztestsapling1efxqj5256ywqdc3zntfa0hw6yn4f83k2h7fgngwmxr3h3w7zydlencvh30730ez6p8fwg56htgz" // carol's address
|
||||||
|
}
|
||||||
|
|
||||||
|
object MyWallet : WalletConfig {
|
||||||
|
override val displayName = "MyWallet"
|
||||||
|
override val seedName = "test.reference.$displayName".sanitizeName()
|
||||||
|
override val seedProvider = SampleImportedSeedProvider("295761fce7fdc89fa1095259f5be6375c4a36f7a214767d668f9ef6e17aa6314")
|
||||||
|
override val spendingKeyProvider = SampleSpendingKeySharedPref(seedName)
|
||||||
|
override val cacheDbName = "test_cache_${displayName.sanitizeName()}.db"
|
||||||
|
override val dataDbName = "test_data_${displayName.sanitizeName()}.db"
|
||||||
|
override val defaultSendAddress =
|
||||||
|
"ztestsapling1snmqdnfqnc407pvqw7sld8w5zxx6nd0523kvlj4jf39uvxvh7vn0hs3q38n07806dwwecqwke3t" // dummyseed
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Servers(val host: String, val displayName: String) {
|
||||||
|
LOCALHOST("10.0.0.191", "Localhost"),
|
||||||
|
// WLAN("10.0.0.26"),
|
||||||
|
WLAN1("10.0.2.24", "WLAN Conference"),
|
||||||
|
WLAN2("192.168.1.235", "WLAN Office"),
|
||||||
|
BOLT_TESTNET("ec2-34-228-10-162.compute-1.amazonaws.com", "Bolt Labs Testnet"),
|
||||||
|
ZCASH_TESTNET("lightwalletd.z.cash", "Zcash Testnet")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: load most of these properties in later, perhaps from settings
|
||||||
|
object SampleProperties {
|
||||||
|
|
||||||
|
const val PREFS_WALLET_DISPLAY_NAME = "prefs_wallet_name"
|
||||||
|
const val PREFS_SERVER_NAME = "prefs_server_name"
|
||||||
|
|
||||||
|
val COMPACT_BLOCK_SERVER = Servers.ZCASH_TESTNET.host
|
||||||
|
const val COMPACT_BLOCK_PORT = 9067
|
||||||
|
val wallet = DaveWallet
|
||||||
|
// TODO: placeholder until we have a network service for this
|
||||||
|
val USD_PER_ZEC = BigDecimal("52.86", MathContext.DECIMAL128)
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package cash.z.android.wallet.sample
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import cash.z.android.wallet.ZcashWalletApplication
|
||||||
|
import okio.ByteString
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import kotlin.properties.ReadOnlyProperty
|
||||||
|
import kotlin.properties.ReadWriteProperty
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
@Deprecated(message = InsecureWarning.message)
|
||||||
|
class SampleImportedSeedProvider(private val seedHex: String) : ReadOnlyProperty<Any?, ByteArray> {
|
||||||
|
override fun getValue(thisRef: Any?, property: KProperty<*>): ByteArray {
|
||||||
|
val bytes = ByteString.decodeHex(seedHex).toByteArray()
|
||||||
|
val stringBytes = String(bytes, Charset.forName("UTF-8"))
|
||||||
|
Log.e("TWIG-x", "byteString: $stringBytes")
|
||||||
|
return decodeHex(seedHex).also { Log.e("TWIG-x", "$it") }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeHex(hex: String): ByteArray {
|
||||||
|
val result = ByteArray(hex.length / 2)
|
||||||
|
for (i in result.indices) {
|
||||||
|
val d1 = decodeHexDigit(hex[i * 2]) shl 4
|
||||||
|
val d2 = decodeHexDigit(hex[i * 2 + 1])
|
||||||
|
result[i] = (d1 + d2).toByte()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decodeHexDigit(c: Char): Int {
|
||||||
|
if (c in '0'..'9') return c - '0'
|
||||||
|
if (c in 'a'..'f') return c - 'a' + 10
|
||||||
|
if (c in 'A'..'F') return c - 'A' + 10
|
||||||
|
throw IllegalArgumentException("Unexpected hex digit: $c")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated(message = InsecureWarning.message)
|
||||||
|
class SampleSpendingKeySharedPref(private val fileName: String) : ReadWriteProperty<Any?, String> {
|
||||||
|
|
||||||
|
private fun getPrefs() = ZcashWalletApplication.instance
|
||||||
|
.getSharedPreferences(fileName, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
|
||||||
|
val preferences = getPrefs()
|
||||||
|
return preferences.getString("spending", null)
|
||||||
|
?: throw IllegalStateException(
|
||||||
|
"Spending key was not there when we needed it! Make sure it was saved " +
|
||||||
|
"during the first run of the app, when accounts were created!"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
|
||||||
|
Log.e("TWIG", "Spending key is being stored")
|
||||||
|
val preferences = getPrefs()
|
||||||
|
val editor = preferences.edit()
|
||||||
|
editor.putString("spending", value)
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object InsecureWarning {
|
||||||
|
const val message = "Do not use this because it is insecure and only intended for test code and samples. " +
|
||||||
|
"Instead, use the Android Keystore system or a 3rd party library that leverages it."
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package cash.z.android.wallet.sample
|
||||||
|
|
||||||
|
import cash.z.android.qrecycler.QScanner
|
||||||
|
|
||||||
|
class SampleQrScanner : QScanner {
|
||||||
|
override fun scanBarcode(callback: (Result<String>) -> Unit) {
|
||||||
|
callback(Result.success("sampleqrcode_scan_success"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package cash.z.android.wallet.ui.activity
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import dagger.android.support.DaggerAppCompatActivity
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
abstract class BaseActivity : DaggerAppCompatActivity(), CoroutineScope {
|
||||||
|
private lateinit var job: Job
|
||||||
|
|
||||||
|
override val coroutineContext: CoroutineContext
|
||||||
|
get() = job + Dispatchers.Main
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
job = Job()
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package cash.z.android.wallet.ui.activity
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import dagger.android.AndroidInjection
|
||||||
|
import dagger.android.AndroidInjector
|
||||||
|
import dagger.android.DispatchingAndroidInjector
|
||||||
|
import dagger.android.HasFragmentInjector
|
||||||
|
import dagger.android.support.DaggerAppCompatActivity
|
||||||
|
import dagger.android.support.HasSupportFragmentInjector
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
abstract class BaseMainActivity : DaggerAppCompatActivity(), CoroutineScope, HasFragmentInjector,
|
||||||
|
HasSupportFragmentInjector {
|
||||||
|
private lateinit var job: Job
|
||||||
|
|
||||||
|
override val coroutineContext: CoroutineContext
|
||||||
|
get() = job + Dispatchers.Main
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var supportFragmentInjector: DispatchingAndroidInjector<Fragment>
|
||||||
|
@Inject
|
||||||
|
lateinit var frameworkFragmentInjector: DispatchingAndroidInjector<android.app.Fragment>
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
job = Job()
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun supportFragmentInjector(): AndroidInjector<androidx.fragment.app.Fragment>? {
|
||||||
|
return supportFragmentInjector
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fragmentInjector(): AndroidInjector<android.app.Fragment>? {
|
||||||
|
return frameworkFragmentInjector
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,16 @@
|
||||||
package cash.z.android.wallet.ui.activity
|
package cash.z.android.wallet.ui.activity
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.view.inputmethod.InputMethodManager.HIDE_NOT_ALWAYS
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.core.view.doOnLayout
|
import androidx.core.view.doOnLayout
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.Navigation
|
import androidx.navigation.Navigation
|
||||||
import androidx.navigation.ui.AppBarConfiguration
|
import androidx.navigation.ui.AppBarConfiguration
|
||||||
|
@ -12,26 +19,48 @@ import androidx.navigation.ui.setupActionBarWithNavController
|
||||||
import androidx.navigation.ui.setupWithNavController
|
import androidx.navigation.ui.setupWithNavController
|
||||||
import cash.z.android.wallet.BuildConfig
|
import cash.z.android.wallet.BuildConfig
|
||||||
import cash.z.android.wallet.R
|
import cash.z.android.wallet.R
|
||||||
|
import cash.z.android.wallet.databinding.ActivityMainBinding
|
||||||
|
import cash.z.android.wallet.sample.WalletConfig
|
||||||
|
import cash.z.wallet.sdk.data.Synchronizer
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
import dagger.android.support.DaggerAppCompatActivity
|
import javax.inject.Inject
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
|
||||||
import kotlinx.android.synthetic.main.nav_header_main.*
|
|
||||||
|
|
||||||
class MainActivity : DaggerAppCompatActivity() {
|
class MainActivity : BaseActivity() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var synchronizer: Synchronizer
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var walletConfig: WalletConfig
|
||||||
|
|
||||||
|
lateinit var binding: ActivityMainBinding
|
||||||
|
lateinit var loadMessages: List<String>
|
||||||
|
|
||||||
// used to manage the drawer and drawerToggle interactions
|
|
||||||
private lateinit var appBarConfiguration: AppBarConfiguration
|
private lateinit var appBarConfiguration: AppBarConfiguration
|
||||||
lateinit var navController: NavController
|
lateinit var navController: NavController
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
|
||||||
|
initAppBar()
|
||||||
|
loadMessages = generateFunLoadMessages().shuffled()
|
||||||
|
synchronizer.start(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initAppBar() {
|
||||||
|
setSupportActionBar(findViewById(R.id.main_toolbar))
|
||||||
|
setupNavigation()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
synchronizer.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (drawer_layout.isDrawerOpen(GravityCompat.START)) {
|
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||||
drawer_layout.closeDrawer(GravityCompat.START)
|
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
} else {
|
} else {
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
|
@ -46,28 +75,71 @@ class MainActivity : DaggerAppCompatActivity() {
|
||||||
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
|
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setDrawerLocked(isLocked: Boolean) {
|
||||||
|
binding.drawerLayout.setDrawerLockMode(if (isLocked) DrawerLayout.LOCK_MODE_LOCKED_CLOSED else DrawerLayout.LOCK_MODE_UNLOCKED)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openDrawer(view: View) {
|
||||||
|
binding.drawerLayout.openDrawer(GravityCompat.START)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setToolbarShown(isShown: Boolean) {
|
||||||
|
binding.mainAppBar.visibility = if (isShown) View.VISIBLE else View.INVISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
fun setupNavigation() {
|
fun setupNavigation() {
|
||||||
// create and setup the navController and appbarConfiguration
|
// create and setup the navController and appbarConfiguration
|
||||||
navController = Navigation.findNavController(this, R.id.nav_host_fragment).also { n ->
|
navController = Navigation.findNavController(this, R.id.nav_host_fragment).also { n ->
|
||||||
appBarConfiguration = AppBarConfiguration(n.graph, drawer_layout).also { a ->
|
appBarConfiguration = AppBarConfiguration(n.graph, binding.drawerLayout).also { a ->
|
||||||
nav_view.setupWithNavController(n)
|
binding.navView.setupWithNavController(n)
|
||||||
setupActionBarWithNavController(n, a)
|
setupActionBarWithNavController(n, binding.drawerLayout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
navController.addOnDestinationChangedListener { _, _, _ ->
|
||||||
|
// hide the keyboard anytime we change destinations
|
||||||
|
getSystemService<InputMethodManager>()?.hideSoftInputFromWindow(binding.navView.windowToken, HIDE_NOT_ALWAYS)
|
||||||
|
}
|
||||||
|
|
||||||
// remove icon tint so that our colored nav icons show through
|
// remove icon tint so that our colored nav icons show through
|
||||||
nav_view.itemIconTintList = null
|
binding.navView.itemIconTintList = null
|
||||||
|
|
||||||
nav_view.doOnLayout {
|
binding.navView.doOnLayout {
|
||||||
text_nav_header_subtitle.text = "Version ${BuildConfig.VERSION_NAME}"
|
binding.navView.findViewById<TextView>(R.id.text_nav_header_subtitle).text = "Version ${BuildConfig.VERSION_NAME} (${walletConfig.displayName})"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun nextLoadMessage(index: Int = -1): String {
|
||||||
|
return if (index < 0) loadMessages.random() else loadMessages[index]
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
init {
|
init {
|
||||||
// Enable vector drawable magic
|
// Enable vector drawable magic
|
||||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: move these lists, once approved
|
||||||
|
fun generateSeriousLoadMessages(): List<String> {
|
||||||
|
return listOf(
|
||||||
|
"Initializing your shielded address",
|
||||||
|
"Connecting to testnet",
|
||||||
|
"Downloading historical blocks",
|
||||||
|
"Synchronizing to current blockchain",
|
||||||
|
"Searching for past transactions",
|
||||||
|
"Validating your balance"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateFunLoadMessages(): List<String> {
|
||||||
|
return listOf(
|
||||||
|
"Reticulating splines",
|
||||||
|
"Making the sausage",
|
||||||
|
"Drinking the kool-aid",
|
||||||
|
"Learning to spell Lamborghini",
|
||||||
|
"Asking Zooko, \"when moon?!\"",
|
||||||
|
"Pretending to look busy"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,4 +147,4 @@ class MainActivity : DaggerAppCompatActivity() {
|
||||||
abstract class MainActivityModule {
|
abstract class MainActivityModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributeMainActivity(): MainActivity
|
abstract fun contributeMainActivity(): MainActivity
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,60 +1,65 @@
|
||||||
package cash.z.android.wallet.ui.adapter
|
package cash.z.android.wallet.ui.adapter
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.LayoutRes
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import cash.z.android.wallet.R
|
import cash.z.android.wallet.R
|
||||||
import cash.z.android.wallet.extention.toAppColor
|
import cash.z.android.wallet.extention.toAppColor
|
||||||
import cash.z.android.wallet.vo.WalletTransaction
|
import cash.z.android.wallet.extention.toRelativeTimeString
|
||||||
import java.math.BigDecimal
|
import cash.z.android.wallet.extention.truncate
|
||||||
import java.math.MathContext
|
import cash.z.wallet.sdk.ext.convertZatoshiToZec
|
||||||
import java.math.RoundingMode
|
import cash.z.wallet.sdk.ext.toZec
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import cash.z.wallet.sdk.dao.WalletTransaction
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
|
||||||
class TransactionAdapter(private val transactions: MutableList<WalletTransaction>) :
|
class TransactionAdapter(@LayoutRes val itemResId: Int = R.layout.item_transaction) : ListAdapter<WalletTransaction, TransactionViewHolder>(DIFF_CALLBACK) {
|
||||||
RecyclerView.Adapter<TransactionViewHolder>() {
|
|
||||||
|
|
||||||
init {
|
|
||||||
transactions.sortBy { it.timestamp * -1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransactionViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransactionViewHolder {
|
||||||
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_transaction, parent, false)
|
val itemView = LayoutInflater.from(parent.context).inflate(itemResId, parent, false)
|
||||||
return TransactionViewHolder(itemView)
|
return TransactionViewHolder(itemView)
|
||||||
}
|
}
|
||||||
|
override fun onBindViewHolder(holder: TransactionViewHolder, position: Int) = holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = transactions.size
|
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<WalletTransaction>() {
|
||||||
|
override fun areItemsTheSame(oldItem: WalletTransaction, newItem: WalletTransaction) = oldItem.height == newItem.height
|
||||||
override fun onBindViewHolder(holder: TransactionViewHolder, position: Int) = holder.bind(transactions[position])
|
override fun areContentsTheSame(oldItem: WalletTransaction, newItem: WalletTransaction) = oldItem == newItem
|
||||||
|
|
||||||
fun setTransactions(txs: List<WalletTransaction>) {
|
|
||||||
transactions.clear()
|
|
||||||
transactions.addAll(txs)
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
private val status = itemView.findViewById<View>(R.id.view_transaction_status)
|
private val status = itemView.findViewById<View>(R.id.view_transaction_status)
|
||||||
|
private val icon = itemView.findViewById<ImageView>(R.id.image_transaction_type)
|
||||||
private val timestamp = itemView.findViewById<TextView>(R.id.text_transaction_timestamp)
|
private val timestamp = itemView.findViewById<TextView>(R.id.text_transaction_timestamp)
|
||||||
private val amount = itemView.findViewById<TextView>(R.id.text_transaction_amount)
|
private val amount = itemView.findViewById<TextView>(R.id.text_transaction_amount)
|
||||||
private val background = itemView.findViewById<View>(R.id.container_transaction)
|
private val address = itemView.findViewById<TextView>(R.id.text_transaction_address)
|
||||||
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
|
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
|
||||||
|
|
||||||
fun bind(tx: WalletTransaction) {
|
fun bind(tx: WalletTransaction) {
|
||||||
val sign = if(tx.amount > BigDecimal.ZERO) "+" else "-"
|
val isHistory = icon != null
|
||||||
val rowColor = if(adapterPosition.rem(2) == 0) R.color.zcashBlueGray else R.color.zcashWhite
|
val sign = if (tx.isSend) "-" else "+"
|
||||||
val amountColor = if(tx.amount > BigDecimal.ZERO) R.color.colorPrimary else R.color.text_dark_dimmed
|
val amountColor = if (tx.isSend) R.color.text_dark_dimmed else R.color.colorPrimary
|
||||||
background.setBackgroundColor(rowColor.toAppColor())
|
val transactionColor = if (tx.isSend) R.color.send_associated else R.color.receive_associated
|
||||||
status.setBackgroundColor(tx.status.color.toAppColor())
|
val transactionIcon = if (tx.isSend) R.drawable.ic_sent_transaction else R.drawable.ic_received_transaction
|
||||||
timestamp.text = formatter.format(tx.timestamp)
|
val zecAbsoluteValue = tx.value.absoluteValue.convertZatoshiToZec(6)
|
||||||
amount.text = String.format("$sign %,.3f", tx.amount.round(MathContext(3, RoundingMode.HALF_EVEN )).abs())
|
val toOrFrom = if (tx.isSend) "to" else "from"
|
||||||
|
val srcOrDestination = tx.address?.truncate() ?: "shielded mystery person"
|
||||||
|
timestamp.text = if (!tx.isMined || tx.timeInSeconds == 0L) "Pending"
|
||||||
|
else (if (isHistory) formatter.format(tx.timeInSeconds * 1000) else (tx.timeInSeconds * 1000L).toRelativeTimeString())
|
||||||
|
amount.text = "$sign$zecAbsoluteValue"
|
||||||
amount.setTextColor(amountColor.toAppColor())
|
amount.setTextColor(amountColor.toAppColor())
|
||||||
|
|
||||||
|
// maybes - and if this gets to be too much, then pass in a custom holder when constructing the adapter, instead
|
||||||
|
status?.setBackgroundColor(transactionColor.toAppColor())
|
||||||
|
address?.text = "$toOrFrom $srcOrDestination"
|
||||||
|
icon?.setImageResource(transactionIcon)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,56 @@
|
||||||
package cash.z.android.wallet.ui.fragment
|
package cash.z.android.wallet.ui.fragment
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import cash.z.android.wallet.BuildConfig
|
||||||
|
import cash.z.android.wallet.R
|
||||||
|
import cash.z.android.wallet.databinding.FragmentAboutBinding
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
|
||||||
class AboutFragment : PlaceholderFragment()
|
|
||||||
|
class AboutFragment : BaseFragment() {
|
||||||
|
lateinit var binding: cash.z.android.wallet.databinding.FragmentAboutBinding
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return DataBindingUtil
|
||||||
|
.inflate<FragmentAboutBinding>(inflater, R.layout.fragment_about, container, false)
|
||||||
|
.also { binding = it }
|
||||||
|
.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
binding.textAboutVersionValue.text = BuildConfig.VERSION_NAME
|
||||||
|
binding.textAboutLicensingValue.setOnClickListener {
|
||||||
|
openUrl("https://z.cash/trademark-policy/")
|
||||||
|
}
|
||||||
|
binding.textAboutWhatsNewValue.setOnClickListener {
|
||||||
|
openUrl("https://github.com/gmale/zcash-android-wallet-poc/blob/feature/scan-blocks-integration/CHANGELOG.md")
|
||||||
|
}
|
||||||
|
binding.textAboutZcashBlogValue.setOnClickListener {
|
||||||
|
openUrl("https://z.cash/blog/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openUrl(url: String) {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
mainActivity?.setToolbarShown(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
@Module
|
||||||
|
abstract class AboutFragmentModule {
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun contributeAboutFragment(): AboutFragment
|
||||||
|
}
|
|
@ -1,24 +1,32 @@
|
||||||
package cash.z.android.wallet.ui.fragment
|
package cash.z.android.wallet.ui.fragment
|
||||||
|
|
||||||
import android.content.Context
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.Fragment
|
import cash.z.android.wallet.ui.activity.MainActivity
|
||||||
import dagger.android.AndroidInjector
|
import dagger.android.support.DaggerFragment
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import dagger.android.support.AndroidSupportInjection
|
import kotlinx.coroutines.Dispatchers
|
||||||
import dagger.android.support.HasSupportFragmentInjector
|
import kotlinx.coroutines.Job
|
||||||
import javax.inject.Inject
|
import kotlin.contracts.ExperimentalContracts
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
abstract class BaseFragment : Fragment(), HasSupportFragmentInjector {
|
abstract class BaseFragment : DaggerFragment(), CoroutineScope {
|
||||||
|
|
||||||
@Inject
|
private lateinit var job: Job
|
||||||
internal lateinit var childFragmentInjector: DispatchingAndroidInjector<Fragment>
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
val mainActivity: MainActivity? get() = activity as MainActivity?
|
||||||
AndroidSupportInjection.inject(this)
|
|
||||||
super.onAttach(context)
|
override val coroutineContext: CoroutineContext
|
||||||
|
get() = job + Dispatchers.Main
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
job = Job()
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun supportFragmentInjector(): AndroidInjector<Fragment>? {
|
override fun onDestroy() {
|
||||||
return childFragmentInjector
|
super.onDestroy()
|
||||||
|
job.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
package cash.z.android.wallet.ui.fragment
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.doOnPreDraw
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.transition.Transition
|
||||||
|
import androidx.transition.TransitionInflater
|
||||||
|
import cash.z.android.wallet.R
|
||||||
|
import cash.z.android.wallet.databinding.FragmentFirstrunBinding
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
|
||||||
|
class FirstrunFragment : ProgressFragment(R.id.progress_firstrun), Transition.TransitionListener {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentFirstrunBinding
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
//
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
setupSharedElementTransitions()
|
||||||
|
return DataBindingUtil.inflate<FragmentFirstrunBinding>(
|
||||||
|
inflater, R.layout.fragment_firstrun, container, false
|
||||||
|
).let {
|
||||||
|
binding = it
|
||||||
|
it.root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
postponeEnterTransition()
|
||||||
|
binding.buttonNext.setOnClickListener {
|
||||||
|
mainActivity?.navController?.navigate(
|
||||||
|
R.id.action_firstrun_fragment_to_sync_fragment,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
(view?.parent as? ViewGroup)?.doOnPreDraw {
|
||||||
|
startPostponedEnterTransition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
mainActivity?.setDrawerLocked(true)
|
||||||
|
mainActivity?.setToolbarShown(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSharedElementTransitions() {
|
||||||
|
TransitionInflater.from(mainActivity).inflateTransition(R.transition.transition_zec_sent).apply {
|
||||||
|
duration = 250L
|
||||||
|
addListener(this@FirstrunFragment)
|
||||||
|
this@FirstrunFragment.sharedElementEnterTransition = this
|
||||||
|
this@FirstrunFragment.sharedElementReturnTransition = this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showProgress(progress: Int) {
|
||||||
|
super.showProgress(progress)
|
||||||
|
binding.textProgressFirstrun.text = getProgressText(progress)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProgressComplete() {
|
||||||
|
super.onProgressComplete()
|
||||||
|
binding.textProgressFirstrun.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTransitionStart(transition: Transition) {
|
||||||
|
binding.buttonNext.alpha = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTransitionEnd(transition: Transition) {
|
||||||
|
binding.buttonNext.animate().apply {
|
||||||
|
duration = 300L
|
||||||
|
}.alpha(1.0f)
|
||||||
|
binding.textProgressFirstrun.animate().apply {
|
||||||
|
duration = 300L
|
||||||
|
}.alpha(1.0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTransitionResume(transition: Transition) {}
|
||||||
|
override fun onTransitionPause(transition: Transition) {}
|
||||||
|
override fun onTransitionCancel(transition: Transition) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
abstract class FirstrunFragmentModule {
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun contributeFirstrunFragment(): FirstrunFragment
|
||||||
|
}
|
|
@ -1,4 +1,76 @@
|
||||||
package cash.z.android.wallet.ui.fragment
|
package cash.z.android.wallet.ui.fragment
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import cash.z.android.wallet.R
|
||||||
|
import cash.z.android.wallet.databinding.FragmentHistoryBinding
|
||||||
|
import cash.z.android.wallet.ui.adapter.TransactionAdapter
|
||||||
|
import cash.z.android.wallet.ui.presenter.HistoryPresenter
|
||||||
|
import cash.z.android.wallet.ui.presenter.HistoryPresenterModule
|
||||||
|
import cash.z.android.wallet.ui.util.AlternatingRowColorDecoration
|
||||||
|
import cash.z.wallet.sdk.dao.WalletTransaction
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class HistoryFragment : PlaceholderFragment()
|
|
||||||
|
class HistoryFragment : BaseFragment(), HistoryPresenter.HistoryView {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var historyPresenter: HistoryPresenter
|
||||||
|
private lateinit var binding: FragmentHistoryBinding
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return DataBindingUtil
|
||||||
|
.inflate<FragmentHistoryBinding>(inflater, R.layout.fragment_history, container, false)
|
||||||
|
.also { binding = it }
|
||||||
|
.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
if (mainActivity != null) {
|
||||||
|
mainActivity?.setToolbarShown(true)
|
||||||
|
binding.recyclerTransactionsHistory.apply {
|
||||||
|
layoutManager = LinearLayoutManager(mainActivity, RecyclerView.VERTICAL, false)
|
||||||
|
adapter = TransactionAdapter(R.layout.item_transaction_history)
|
||||||
|
addItemDecoration(AlternatingRowColorDecoration())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
launch {
|
||||||
|
historyPresenter.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
historyPresenter.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setTransactions(transactions: List<WalletTransaction>) {
|
||||||
|
mainActivity?.supportActionBar?.title = resources.getQuantityString(R.plurals.history_transaction_count_title,
|
||||||
|
transactions.size, transactions.size)
|
||||||
|
with (binding.recyclerTransactionsHistory) {
|
||||||
|
(adapter as TransactionAdapter).submitList(transactions)
|
||||||
|
postDelayed({
|
||||||
|
smoothScrollToPosition(0)
|
||||||
|
}, 100L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
abstract class HistoryFragmentModule {
|
||||||
|
@ContributesAndroidInjector(modules = [HistoryPresenterModule::class])
|
||||||
|
abstract fun contributeHistoryFragment(): HistoryFragment
|
||||||
|
}
|
|
@ -1,99 +1,406 @@
|
||||||
package cash.z.android.wallet.ui.fragment
|
package cash.z.android.wallet.ui.fragment
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.text.format.DateUtils
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.AccelerateInterpolator
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import androidx.transition.Transition
|
||||||
|
import androidx.transition.TransitionInflater
|
||||||
import cash.z.android.wallet.R
|
import cash.z.android.wallet.R
|
||||||
import cash.z.android.wallet.extention.toAppColor
|
import cash.z.android.wallet.databinding.FragmentHomeBinding
|
||||||
import cash.z.android.wallet.extention.toAppString
|
import cash.z.android.wallet.di.annotation.FragmentScope
|
||||||
import cash.z.android.wallet.extention.tryIgnore
|
import cash.z.android.wallet.extention.*
|
||||||
import cash.z.android.wallet.ui.activity.MainActivity
|
import cash.z.android.wallet.sample.SampleProperties
|
||||||
import cash.z.android.wallet.ui.adapter.TransactionAdapter
|
import cash.z.android.wallet.ui.adapter.TransactionAdapter
|
||||||
|
import cash.z.android.wallet.ui.presenter.HomePresenter
|
||||||
|
import cash.z.android.wallet.ui.presenter.HomePresenterModule
|
||||||
|
import cash.z.android.wallet.ui.util.AlternatingRowColorDecoration
|
||||||
|
import cash.z.android.wallet.ui.util.LottieLooper
|
||||||
import cash.z.android.wallet.ui.util.TopAlignedSpan
|
import cash.z.android.wallet.ui.util.TopAlignedSpan
|
||||||
import cash.z.android.wallet.vo.WalletTransaction
|
import cash.z.wallet.sdk.dao.WalletTransaction
|
||||||
import cash.z.android.wallet.vo.WalletTransactionStatus
|
import cash.z.wallet.sdk.data.ActiveSendTransaction
|
||||||
|
import cash.z.wallet.sdk.data.ActiveTransaction
|
||||||
|
import cash.z.wallet.sdk.data.TransactionState
|
||||||
|
import cash.z.wallet.sdk.data.twig
|
||||||
|
import cash.z.wallet.sdk.ext.*
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.leinardi.android.speeddial.SpeedDialActionItem
|
import com.leinardi.android.speeddial.SpeedDialActionItem
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
import kotlinx.android.synthetic.main.fragment_home.*
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.android.synthetic.main.include_home_content.*
|
import javax.inject.Inject
|
||||||
import kotlinx.android.synthetic.main.include_home_header.*
|
|
||||||
import java.math.BigDecimal
|
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
import kotlin.random.nextLong
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment representing the home screen of the app. This is the screen most often seen by the user when launching the
|
* Fragment representing the home screen of the app. This is the screen most often seen by the user when launching the
|
||||||
* application.
|
* application.
|
||||||
*/
|
*/
|
||||||
class HomeFragment : BaseFragment() {
|
class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomePresenter.HomeView {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var homePresenter: HomePresenter
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentHomeBinding
|
||||||
|
private lateinit var zcashLogoAnimation: LottieLooper
|
||||||
|
private var maxTransactionsShown: Int = 12
|
||||||
|
private var snackbar: Snackbar? = null
|
||||||
|
private var viewsInitialized = false
|
||||||
|
|
||||||
|
//testing this
|
||||||
|
private var clock: Handler = Handler()
|
||||||
|
private val tickIfNeeded = Ticker()
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// LifeCycle
|
||||||
|
//
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
// Inflate the layout for this fragment
|
viewsInitialized = false
|
||||||
return inflater.inflate(R.layout.fragment_home, container, false)
|
// setupSharedElementTransitions()
|
||||||
|
return DataBindingUtil.inflate<FragmentHomeBinding>(
|
||||||
|
inflater, R.layout.fragment_home, container, false
|
||||||
|
).let {
|
||||||
|
binding = it
|
||||||
|
it.root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSharedElementTransitions() {
|
||||||
|
TransitionInflater.from(mainActivity).inflateTransition(R.transition.transition_zec_sent).apply {
|
||||||
|
duration = 3000L
|
||||||
|
addListener(HomeTransitionListener())
|
||||||
|
this@HomeFragment.sharedElementEnterTransition = this
|
||||||
|
this@HomeFragment.sharedElementReturnTransition = this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
(activity as MainActivity).let { mainActivity ->
|
initTemp()
|
||||||
mainActivity.setSupportActionBar(home_toolbar)
|
init()
|
||||||
mainActivity.setupNavigation()
|
}
|
||||||
mainActivity.supportActionBar?.setTitle(R.string.destination_title_home)
|
|
||||||
}
|
|
||||||
headerFullViews = arrayOf(text_balance_usd, text_balance_includes_info, text_balance_zec, image_zec_symbol_balance_shadow, image_zec_symbol_balance)
|
|
||||||
headerEmptyViews = arrayOf(text_balance_zec_info, text_balance_zec_empty, image_zec_symbol_balance_shadow_empty, image_zec_symbol_balance_empty)
|
|
||||||
|
|
||||||
// TODO: remove this test behavior
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
image_logo.setOnClickListener {
|
super.onActivityCreated(savedInstanceState)
|
||||||
toggle(!empty)
|
mainActivity?.setToolbarShown(false)
|
||||||
|
mainActivity?.setDrawerLocked(false)
|
||||||
|
initFab()
|
||||||
|
|
||||||
|
binding.includeContent.recyclerTransactions.apply {
|
||||||
|
layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
|
||||||
|
adapter = TransactionAdapter()
|
||||||
|
addItemDecoration(AlternatingRowColorDecoration())
|
||||||
|
}
|
||||||
|
binding.includeContent.textTransactionHeaderSeeAll.setOnClickListener {
|
||||||
|
mainActivity?.navController?.navigate(R.id.nav_history_fragment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
view!!.postDelayed( {toggle(false)}, delay * 2L)
|
launch {
|
||||||
|
homePresenter.start()
|
||||||
|
}
|
||||||
|
clock.postDelayed(tickIfNeeded, 1000L)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onPause() {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onPause()
|
||||||
initFab(activity!!)
|
clock.removeCallbacks(tickIfNeeded)
|
||||||
|
homePresenter.stop()
|
||||||
|
binding.lottieZcashBadge.cancelAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
recycler_transactions.layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
|
|
||||||
recycler_transactions.adapter = TransactionAdapter(createDummyTransactions(60))
|
//
|
||||||
|
// SendView Implementation
|
||||||
|
//
|
||||||
|
|
||||||
|
override fun setTransactions(transactions: List<WalletTransaction>) {
|
||||||
|
val recent = if(transactions.size > maxTransactionsShown) transactions.subList(0, maxTransactionsShown) else transactions
|
||||||
|
with (binding.includeContent.recyclerTransactions) {
|
||||||
|
(adapter as TransactionAdapter).submitList(recent)
|
||||||
|
postDelayed({
|
||||||
|
smoothScrollToPosition(0)
|
||||||
|
}, 100L)
|
||||||
|
}
|
||||||
|
// Show "See All" when we have a sublist on screen
|
||||||
|
if (recent.size != transactions.size) {
|
||||||
|
binding.includeContent.textTransactionHeaderSeeAll.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
onContentRefreshComplete(transactions.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: pull some of this logic into the presenter, particularly the part that deals with ZEC <-> USD price conversion
|
||||||
|
override fun updateBalance(old: Long, new: Long) {
|
||||||
|
val zecValue = new.convertZatoshiToZec()
|
||||||
|
setZecValue(zecValue.toZecString(3))
|
||||||
|
setUsdValue(zecValue.convertZecToUsd(SampleProperties.USD_PER_ZEC).toUsdString())
|
||||||
|
|
||||||
|
onContentRefreshComplete(new <= 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setActiveTransactions(activeTransactionMap: Map<ActiveTransaction, TransactionState>) {
|
||||||
|
if (activeTransactionMap.isEmpty()) {
|
||||||
|
twig("A.T.: setActiveTransactionsShown(false) because map is empty")
|
||||||
|
setActiveTransactionsShown(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val transactions = activeTransactionMap.entries.toTypedArray()
|
||||||
|
// primary is the last one that was inserted
|
||||||
|
val primaryEntry = transactions[transactions.size - 1]
|
||||||
|
updatePrimaryTransaction(primaryEntry.key, primaryEntry.value)
|
||||||
|
|
||||||
|
onContentRefreshComplete(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancelledTooLate() {
|
||||||
|
snackbar = snackbar.showOk(view!!, "Oops! It was too late to cancel!")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSynchronizerError(error: Throwable?): Boolean {
|
||||||
|
context?.alert(
|
||||||
|
message = "WARNING: A critical error has occurred and " +
|
||||||
|
"this app will not function properly until that is corrected!",
|
||||||
|
positiveButtonResId = R.string.ignore,
|
||||||
|
negativeButtonResId = R.string.details,
|
||||||
|
negativeAction = { context?.alert("Synchronization error:\n\n$error") }
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// View API
|
||||||
|
//
|
||||||
|
|
||||||
|
fun setContentViewShown(isShown: Boolean) {
|
||||||
|
// with(binding.includeContent) {
|
||||||
|
// groupEmptyViewItems.visibility = if (isShown) View.GONE else View.VISIBLE
|
||||||
|
// groupContentViewItems.visibility = if (isShown) View.VISIBLE else View.GONE
|
||||||
|
// }
|
||||||
|
toggleViews(!isShown)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val stopAnimation = Runnable {
|
||||||
|
setRefreshAnimationPlaying(false).also { twig("refresh false from onRefresh") }
|
||||||
|
}
|
||||||
|
override fun onRefresh() {
|
||||||
|
setRefreshAnimationPlaying(true).also { twig("refresh true from onRefresh") }
|
||||||
|
|
||||||
|
with(binding.includeContent.refreshLayout) {
|
||||||
|
isRefreshing = false
|
||||||
|
val fauxRefresh = Random.nextLong(750L..3000L)
|
||||||
|
postDelayed(stopAnimation, fauxRefresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun setRefreshAnimationPlaying(isPlaying: Boolean) {
|
||||||
|
twig("set refresh to: $isPlaying for $zcashLogoAnimation")
|
||||||
|
if (isPlaying) {
|
||||||
|
zcashLogoAnimation.start()
|
||||||
|
} else {
|
||||||
|
zcashLogoAnimation.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onInitialLoadComplete() {
|
||||||
|
val isEmpty = (binding.includeContent.recyclerTransactions?.adapter?.itemCount ?: 0).let { it == 0 }
|
||||||
|
twig("onInitialLoadComplete and isEmpty == $isEmpty")
|
||||||
|
setContentViewShown(!isEmpty)
|
||||||
|
if (isEmpty) {
|
||||||
|
binding.includeContent.textEmptyWalletMessage.setText(R.string.home_empty_wallet)
|
||||||
|
}
|
||||||
|
setRefreshAnimationPlaying(false).also { twig("refresh false from onInitialLoadComplete") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePrimaryTransaction(transaction: ActiveTransaction, transactionState: TransactionState) {
|
||||||
|
|
||||||
|
twig("setting transaction state to ${transactionState::class.simpleName}")
|
||||||
|
var title = binding.includeContent.textActiveTransactionTitle.text?.toString() ?: ""
|
||||||
|
var subtitle: CharSequence = binding.includeContent.textActiveTransactionSubtitle.text?.toString() ?: ""
|
||||||
|
var isShown = binding.includeContent.textActiveTransactionHeader.visibility == View.VISIBLE
|
||||||
|
var isShownDelay = 10L
|
||||||
|
when (transactionState) {
|
||||||
|
TransactionState.Creating -> {
|
||||||
|
binding.includeContent.headerActiveTransaction.visibility = View.VISIBLE
|
||||||
|
title = "Preparing ${transaction.value.convertZatoshiToZecString(3)} ZEC"
|
||||||
|
subtitle = "to ${(transaction as ActiveSendTransaction).toAddress.truncate()}"
|
||||||
|
setTransactionActive(transaction, true)
|
||||||
|
isShown = true
|
||||||
|
}
|
||||||
|
TransactionState.SendingToNetwork -> {
|
||||||
|
title = "Sending Transaction"
|
||||||
|
subtitle = "to ${(transaction as ActiveSendTransaction).toAddress.truncate()}"
|
||||||
|
binding.includeContent.textActiveTransactionValue.text = "${transaction.value.convertZatoshiToZecString(3)}"
|
||||||
|
binding.includeContent.textActiveTransactionValue.visibility = View.VISIBLE
|
||||||
|
binding.includeContent.buttonActiveTransactionCancel.visibility = View.GONE
|
||||||
|
setTransactionActive(transaction, true)
|
||||||
|
isShown = true
|
||||||
|
}
|
||||||
|
is TransactionState.Failure -> {
|
||||||
|
binding.includeContent.lottieActiveTransaction.setAnimation(R.raw.lottie_send_failure)
|
||||||
|
binding.includeContent.lottieActiveTransaction.playAnimation()
|
||||||
|
title = "Failed"
|
||||||
|
subtitle = when(transactionState.failedStep) {
|
||||||
|
TransactionState.Creating -> "Failed to create transaction"
|
||||||
|
TransactionState.SendingToNetwork -> "Failed to submit transaction to the network"
|
||||||
|
else -> "Unrecoginzed error"
|
||||||
|
}
|
||||||
|
binding.includeContent.buttonActiveTransactionCancel.visibility = View.GONE
|
||||||
|
binding.includeContent.textActiveTransactionValue.visibility = View.GONE
|
||||||
|
setTransactionActive(transaction, false)
|
||||||
|
isShown = false
|
||||||
|
isShownDelay = 10_000L
|
||||||
|
}
|
||||||
|
is TransactionState.AwaitingConfirmations -> {
|
||||||
|
if (transactionState.confirmationCount < 1) {
|
||||||
|
binding.includeContent.lottieActiveTransaction.setAnimation(R.raw.lottie_send_success)
|
||||||
|
binding.includeContent.lottieActiveTransaction.playAnimation()
|
||||||
|
title = "ZEC Sent"
|
||||||
|
subtitle = "Waiting to be mined..."
|
||||||
|
binding.includeContent.textActiveTransactionValue.text = transaction.value.convertZatoshiToZecString(3)
|
||||||
|
binding.includeContent.textActiveTransactionValue.visibility = View.VISIBLE
|
||||||
|
binding.includeContent.buttonActiveTransactionCancel.visibility = View.GONE
|
||||||
|
isShown = true
|
||||||
|
} else if (transactionState.confirmationCount > 1) {
|
||||||
|
isShown = false
|
||||||
|
} else {
|
||||||
|
title = "Confirmation Received"
|
||||||
|
subtitle = transactionState.timestamp.toRelativeTimeString()
|
||||||
|
isShown = false
|
||||||
|
isShownDelay = 5_000L
|
||||||
|
// take it out of the list in a bit and skip counting confirmation animation for now (i.e. one is enough)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TransactionState.Cancelled -> {
|
||||||
|
title = binding.includeContent.textActiveTransactionTitle.text.toString()
|
||||||
|
subtitle = binding.includeContent.textActiveTransactionSubtitle.text.toString()
|
||||||
|
setTransactionActive(transaction, false)
|
||||||
|
isShown = false
|
||||||
|
isShownDelay = 10_000L
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.e(javaClass.simpleName, "Warning: unrecognized transaction state $transactionState is being ignored")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.includeContent.textActiveTransactionTitle.text = title
|
||||||
|
binding.includeContent.textActiveTransactionSubtitle.text = subtitle
|
||||||
|
twig("A.T.: setActiveTransactionsShown($isShown, $isShownDelay) because ${transactionState}")
|
||||||
|
setActiveTransactionsShown(isShown, isShownDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Internal View Logic
|
||||||
|
//
|
||||||
|
|
||||||
|
private fun setActiveTransactionsShown(isShown: Boolean, delay: Long = 0L) {
|
||||||
|
binding.includeContent.headerActiveTransaction.postDelayed({
|
||||||
|
binding.includeContent.groupActiveTransactionItems.visibility = if (isShown) View.VISIBLE else View.GONE
|
||||||
|
// do not animate if visibility is already in the right state
|
||||||
|
// binding.includeContent.headerActiveTransaction.animate().alpha(if(isShown) 1f else 0f).setDuration(250).setListener(
|
||||||
|
// AnimatorCompleteListener{ }
|
||||||
|
// )
|
||||||
|
}, delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the Fab button and all its action items
|
* General initialization called during onViewCreated. Mostly responsible for applying the default empty state of
|
||||||
*
|
* the view, before any data or information is known.
|
||||||
* @param activity a helper parameter that forces this method to be called after the activity is created and not null
|
|
||||||
*/
|
*/
|
||||||
private fun initFab(activity: Activity) {
|
private fun init() {
|
||||||
val speedDial = sd_fab
|
zcashLogoAnimation = LottieLooper(binding.lottieZcashBadge, 20..47, 69)
|
||||||
val nav = (activity as MainActivity).navController
|
binding.includeContent.buttonActiveTransactionCancel.setOnClickListener {
|
||||||
|
val transaction = it.tag as? ActiveSendTransaction
|
||||||
|
if (transaction != null) {
|
||||||
|
homePresenter.onCancelActiveTransaction(transaction)
|
||||||
|
} else {
|
||||||
|
Toaster.short("Error: unable to find transaction to cancel!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.lottieZcashBadge.setOnClickListener {
|
||||||
|
binding.lottieZcashBadge.playAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.includeContent.refreshLayout.setProgressViewEndTarget(false, (38f * resources.displayMetrics.density).toInt())
|
||||||
|
|
||||||
|
with(binding.includeContent.refreshLayout) {
|
||||||
|
setOnRefreshListener(this@HomeFragment)
|
||||||
|
setColorSchemeColors(R.color.zcashBlack.toAppColor())
|
||||||
|
setProgressBackgroundColorSchemeColor(R.color.zcashYellow.toAppColor())
|
||||||
|
}
|
||||||
|
maxTransactionsShown = calculateMaxTransactions()
|
||||||
|
|
||||||
|
// hide content
|
||||||
|
setContentViewShown(false)
|
||||||
|
binding.includeContent.textEmptyWalletMessage.setText(R.string.home_empty_wallet_updating)
|
||||||
|
setRefreshAnimationPlaying(true).also { twig("refresh true from init") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateMaxTransactions(): Int {
|
||||||
|
return 12 //TODO: measure the screen and get optimal number for this device
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize the stuff that is temporary and needs to go ASAP
|
||||||
|
private fun initTemp() {
|
||||||
|
|
||||||
|
with(binding.includeHeader) {
|
||||||
|
headerFullViews = arrayOf(textBalanceUsd, textBalanceIncludesInfo, textBalanceZec, imageZecSymbolBalanceShadow, imageZecSymbolBalance)
|
||||||
|
headerEmptyViews = arrayOf(textBalanceZecInfo, textBalanceZecEmpty, imageZecSymbolBalanceShadowEmpty, imageZecSymbolBalanceEmpty)
|
||||||
|
headerFullViews.forEach { containerHomeHeader.removeView(it) }
|
||||||
|
headerEmptyViews.forEach { containerHomeHeader.removeView(it) }
|
||||||
|
binding.includeHeader.containerHomeHeader.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
// toggling determines visibility. hide it all.
|
||||||
|
binding.includeContent.groupEmptyViewItems.visibility = View.GONE
|
||||||
|
binding.includeContent.groupContentViewItems.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the Fab button and all its action items. Should be called during onActivityCreated.
|
||||||
|
*/
|
||||||
|
private fun initFab() {
|
||||||
|
val speedDial = binding.sdFab
|
||||||
|
val nav = mainActivity?.navController
|
||||||
|
|
||||||
HomeFab.values().forEach {
|
HomeFab.values().forEach {
|
||||||
speedDial.addActionItem(it.createItem())
|
speedDial.addActionItem(it.createItem())
|
||||||
}
|
}
|
||||||
|
|
||||||
speedDial.setOnActionSelectedListener { item ->
|
speedDial.setOnActionSelectedListener { item ->
|
||||||
HomeFab.fromId(item.id)?.destination?.apply { nav.navigate(this) }
|
HomeFab.fromId(item.id)?.destination?.apply { nav?.navigate(this) }
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for creating fablets--those little buttons that pop up when the fab is tapped.
|
||||||
|
*/
|
||||||
private val createItem: HomeFab.() -> SpeedDialActionItem = {
|
private val createItem: HomeFab.() -> SpeedDialActionItem = {
|
||||||
SpeedDialActionItem.Builder(id, icon)
|
SpeedDialActionItem.Builder(id, icon)
|
||||||
.setFabBackgroundColor(bgColor.toAppColor())
|
.setFabBackgroundColor(bgColor.toAppColor())
|
||||||
|
@ -103,24 +410,103 @@ class HomeFragment : BaseFragment() {
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setUsdValue(value: Double) {
|
private fun setUsdValue(value: String) {
|
||||||
val valueString = String.format("$ %,.2f",value)
|
val valueString = String.format("$ $value")
|
||||||
val hairSpace = "\u200A"
|
// val hairSpace = "\u200A"
|
||||||
// val adjustedValue = "$$hairSpace$valueString"
|
// val adjustedValue = "$$hairSpace$valueString"
|
||||||
val textSpan = SpannableString(valueString)
|
val textSpan = SpannableString(valueString)
|
||||||
textSpan.setSpan(TopAlignedSpan(), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
textSpan.setSpan(TopAlignedSpan(), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
textSpan.setSpan(TopAlignedSpan(), valueString.length - 3, valueString.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
textSpan.setSpan(TopAlignedSpan(), valueString.length - 3, valueString.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
text_balance_usd.text = textSpan
|
binding.includeHeader.textBalanceUsd.text = textSpan
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setZecValue(value: Double) {
|
private fun setZecValue(value: String) {
|
||||||
text_balance_zec.text = if(value == 0.0) "0" else String.format("%.3f",value)
|
binding.includeHeader.textBalanceZec.text = value
|
||||||
|
|
||||||
|
|
||||||
// // bugfix: there is a bug in motionlayout that causes text to flicker as it is resized because the last character doesn't fit. Padding both sides with a thin space works around this bug.
|
// // bugfix: there is a bug in motionlayout that causes text to flicker as it is resized because the last character doesn't fit. Padding both sides with a thin space works around this bug.
|
||||||
// val hairSpace = "\u200A"
|
// val hairSpace = "\u200A"
|
||||||
// val adjustedValue = "$hairSpace$valueString$hairSpace"
|
// val adjustedValue = "$hairSpace$valueString$hairSpace"
|
||||||
// text_balance_zec.text = adjustedValue
|
// text_balance_zec.text = adjustedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever the content has been refreshed on the screen. When it is time to show and hide things.
|
||||||
|
* If the balance goes to zero, the wallet is now empty so show the empty view.
|
||||||
|
* If the balance changes from zero, the wallet is no longer empty so hide the empty view.
|
||||||
|
* But don't do either of these things if the situation has not changed.
|
||||||
|
*/
|
||||||
|
private fun onContentRefreshComplete(isEmpty: Boolean) {
|
||||||
|
val isAdapterEmpty = (binding.includeContent.recyclerTransactions.adapter?.itemCount ?: 0) == 0
|
||||||
|
val isBalanceZero = binding.includeHeader.textBalanceZec.text.toString() == "0"
|
||||||
|
val isActiveHidden = binding.includeContent.groupActiveTransactionItems.visibility != View.VISIBLE
|
||||||
|
val isActuallyEmpty = isEmpty && isAdapterEmpty && isBalanceZero && isActiveHidden
|
||||||
|
|
||||||
|
// wasEmpty isn't enough info. it must be considered along with whether these views were ever initialized
|
||||||
|
val wasEmpty = binding.includeContent.groupEmptyViewItems.visibility == View.VISIBLE
|
||||||
|
// situation has changed when we weren't initialized but now we have a balance or emptiness has changed
|
||||||
|
val situationHasChanged = !viewsInitialized || (isActuallyEmpty != wasEmpty)
|
||||||
|
|
||||||
|
twig("onContentRefreshComplete called initialized: $viewsInitialized isEmpty: $isActuallyEmpty wasEmpty: $wasEmpty")
|
||||||
|
if (situationHasChanged) {
|
||||||
|
twig("The situation has changed! toggling views!")
|
||||||
|
setContentViewShown(!isActuallyEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
setRefreshAnimationPlaying(false).also { twig("refresh false from onContentRefreshComplete") }
|
||||||
|
binding.includeHeader.containerHomeHeader.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onActiveTransactionTransitionStart() {
|
||||||
|
binding.includeContent.buttonActiveTransactionCancel.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onActiveTransactionTransitionEnd() {
|
||||||
|
// TODO: investigate if this fix is still required after getting transition animation working again
|
||||||
|
// fixes a bug where the translation gets lost, during animation. As a nice side effect, visually, it makes the view appear to settle in to position
|
||||||
|
binding.includeContent.headerActiveTransaction.translationZ = 10.0f
|
||||||
|
binding.includeContent.buttonActiveTransactionCancel.apply {
|
||||||
|
postDelayed({text = "cancel"}, 50L)
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTransactionActive(transaction: ActiveTransaction, isActive: Boolean) {
|
||||||
|
// TODO: get view for transaction, mostly likely keep a sparse array of these or something
|
||||||
|
if (isActive) {
|
||||||
|
binding.includeContent.buttonActiveTransactionCancel.setText(R.string.cancel)
|
||||||
|
binding.includeContent.buttonActiveTransactionCancel.isEnabled = true
|
||||||
|
binding.includeContent.buttonActiveTransactionCancel.tag = transaction
|
||||||
|
binding.includeContent.headerActiveTransaction.animate().apply {
|
||||||
|
translationZ(10f)
|
||||||
|
duration = 200L
|
||||||
|
interpolator = DecelerateInterpolator()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.includeContent.buttonActiveTransactionCancel.setText(R.string.cancelled)
|
||||||
|
binding.includeContent.buttonActiveTransactionCancel.isEnabled = false
|
||||||
|
binding.includeContent.buttonActiveTransactionCancel.tag = null
|
||||||
|
binding.includeContent.headerActiveTransaction.animate().apply {
|
||||||
|
translationZ(2f)
|
||||||
|
duration = 300L
|
||||||
|
interpolator = AccelerateInterpolator()
|
||||||
|
}
|
||||||
|
binding.includeContent.lottieActiveTransaction.cancelAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class Ticker : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
if (mainActivity == null) return
|
||||||
|
binding.includeContent.recyclerTransactions.apply {
|
||||||
|
if ((adapter?.itemCount ?: 0) > 0) {
|
||||||
|
adapter?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
clock.postDelayed(this@Ticker, 1000L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the basic properties of each FAB button for use while initializing the FAB
|
* Defines the basic properties of each FAB button for use while initializing the FAB
|
||||||
*/
|
*/
|
||||||
|
@ -132,12 +518,12 @@ class HomeFragment : BaseFragment() {
|
||||||
@IdRes val destination:Int
|
@IdRes val destination:Int
|
||||||
) {
|
) {
|
||||||
/* ordered by when they need to be added to the speed dial (i.e. reverse display order) */
|
/* ordered by when they need to be added to the speed dial (i.e. reverse display order) */
|
||||||
REQUEST(
|
HISTORY(
|
||||||
R.id.fab_request,
|
R.id.fab_history,
|
||||||
R.drawable.ic_receipt_24dp,
|
R.drawable.ic_history_24dp,
|
||||||
R.color.icon_request,
|
R.color.icon_request,
|
||||||
R.string.destination_menu_label_request,
|
R.string.destination_menu_label_history,
|
||||||
R.id.nav_request_fragment
|
R.id.nav_history_fragment
|
||||||
),
|
),
|
||||||
RECEIVE(
|
RECEIVE(
|
||||||
R.id.fab_receive,
|
R.id.fab_receive,
|
||||||
|
@ -160,114 +546,75 @@ class HomeFragment : BaseFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
//// TODO: Delete these test functions
|
||||||
|
//// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
val delay = 50L
|
||||||
// TODO: Delete these test functions
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
var empty = false
|
|
||||||
val delay = 20L
|
|
||||||
lateinit var headerEmptyViews: Array<View>
|
lateinit var headerEmptyViews: Array<View>
|
||||||
lateinit var headerFullViews: Array<View>
|
lateinit var headerFullViews: Array<View>
|
||||||
|
|
||||||
fun shrink(): Double {
|
|
||||||
return text_balance_zec.text.toString().trim().toDouble() - Random.nextDouble(5.0)
|
|
||||||
}
|
|
||||||
fun grow(): Double {
|
|
||||||
return text_balance_zec.text.toString().trim().toDouble() + Random.nextDouble(5.0)
|
|
||||||
}
|
|
||||||
fun reduceValue() {
|
|
||||||
shrink().let {
|
|
||||||
if(it < 0) { setZecValue(0.0); toggleViews(empty); forceRedraw() }
|
|
||||||
else view?.postDelayed({
|
|
||||||
setZecValue(it)
|
|
||||||
setUsdValue(it*75.0)
|
|
||||||
reduceValue()
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun increaseValue(target: Double) {
|
|
||||||
grow().let {
|
|
||||||
if(it > target) { setZecValue(target); setUsdValue(target*75.0); toggleViews(empty) }
|
|
||||||
else view?.postDelayed({
|
|
||||||
setZecValue(it)
|
|
||||||
setUsdValue(it*75.0)
|
|
||||||
increaseValue(target)
|
|
||||||
if (headerFullViews[0].parent == null || headerEmptyViews[0].parent != null) toggleViews(false)
|
|
||||||
forceRedraw()
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun forceRedraw() {
|
fun forceRedraw() {
|
||||||
view?.postDelayed({
|
view?.postDelayed({
|
||||||
container_home_header.progress = container_home_header.progress - 0.1f
|
binding.includeHeader.containerHomeHeader.progress = binding.includeHeader.containerHomeHeader.progress - 0.1f
|
||||||
}, delay * 2)
|
}, delay * 2)
|
||||||
}
|
}
|
||||||
internal fun toggle(isEmpty: Boolean) {
|
|
||||||
toggleValues(isEmpty)
|
|
||||||
}
|
|
||||||
internal fun toggleViews(isEmpty: Boolean) {
|
internal fun toggleViews(isEmpty: Boolean) {
|
||||||
if(isEmpty) {
|
twig("toggling views to isEmpty == $isEmpty")
|
||||||
view?.postDelayed({
|
var action: () -> Unit
|
||||||
group_empty_view_items.visibility = View.VISIBLE
|
if (isEmpty) {
|
||||||
group_full_view_items.visibility = View.GONE
|
action = {
|
||||||
headerFullViews.forEach { container_home_header.removeView(it) }
|
binding.includeContent.groupEmptyViewItems.visibility = View.VISIBLE
|
||||||
|
binding.includeContent.groupContentViewItems.visibility = View.GONE
|
||||||
|
headerFullViews.forEach { binding.includeHeader.containerHomeHeader.removeView(it) }
|
||||||
headerEmptyViews.forEach {
|
headerEmptyViews.forEach {
|
||||||
tryIgnore {
|
tryIgnore {
|
||||||
container_home_header.addView(it)
|
binding.includeHeader.containerHomeHeader.addView(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, delay)
|
}
|
||||||
} else {
|
} else {
|
||||||
view?.postDelayed({
|
action = {
|
||||||
group_empty_view_items.visibility = View.GONE
|
binding.includeContent.groupEmptyViewItems.visibility = View.GONE
|
||||||
group_full_view_items.visibility = View.VISIBLE
|
binding.includeContent.groupContentViewItems.visibility = View.VISIBLE
|
||||||
headerEmptyViews.forEach { container_home_header.removeView(it) }
|
headerEmptyViews.forEach { binding.includeHeader.containerHomeHeader.removeView(it) }
|
||||||
headerFullViews.forEach {
|
headerFullViews.forEach {
|
||||||
tryIgnore {
|
tryIgnore {
|
||||||
container_home_header.addView(it)
|
binding.includeHeader.containerHomeHeader.addView(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, delay)
|
}
|
||||||
}
|
}
|
||||||
|
view?.postDelayed({
|
||||||
|
binding.includeHeader.containerHomeHeader.visibility = View.VISIBLE
|
||||||
|
action()
|
||||||
|
viewsInitialized = true
|
||||||
|
}, delay)
|
||||||
|
// TODO: the motion layout does not begin in the right state for some reason. Debug this later.
|
||||||
|
view?.postDelayed(::forceRedraw, delay * 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun toggleValues(isEmpty: Boolean) {
|
|
||||||
empty = isEmpty
|
inner class HomeTransitionListener : Transition.TransitionListener {
|
||||||
if(empty) {
|
override fun onTransitionStart(transition: Transition) {
|
||||||
reduceValue()
|
|
||||||
} else {
|
|
||||||
increaseValue(Random.nextDouble(20.0, 100.0))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTransitionEnd(transition: Transition) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTransitionResume(transition: Transition) {}
|
||||||
|
override fun onTransitionPause(transition: Transition) {}
|
||||||
|
override fun onTransitionCancel(transition: Transition) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
abstract class HomeFragmentModule {
|
abstract class HomeFragmentModule {
|
||||||
@ContributesAndroidInjector
|
@FragmentScope
|
||||||
|
@ContributesAndroidInjector(modules = [HomePresenterModule::class])
|
||||||
abstract fun contributeHomeFragment(): HomeFragment
|
abstract fun contributeHomeFragment(): HomeFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//TODO: delete this test code
|
|
||||||
|
|
||||||
internal fun createDummyTransactions(size: Int): MutableList<WalletTransaction> {
|
|
||||||
val transactions = mutableListOf<WalletTransaction>()
|
|
||||||
repeat(size) {
|
|
||||||
transactions.add(createDummyTransaction())
|
|
||||||
}
|
|
||||||
return transactions
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun createDummyTransaction(): WalletTransaction {
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
val before = now - (4 * DateUtils.WEEK_IN_MILLIS)
|
|
||||||
val amount = BigDecimal(Random.nextDouble(0.1, 15.0) * arrayOf(-1, 1).random())
|
|
||||||
val status = if(amount > BigDecimal.ZERO) WalletTransactionStatus.SENT else WalletTransactionStatus.RECEIVED
|
|
||||||
return WalletTransaction(
|
|
||||||
status,
|
|
||||||
Random.nextLong(before, now),
|
|
||||||
amount
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package cash.z.android.wallet.ui.fragment
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import cash.z.android.wallet.R
|
||||||
|
import cash.z.android.wallet.databinding.IncludeHomeHeaderBinding
|
||||||
|
|
||||||
|
class HomeHeaderEmptyFragment : Fragment() {
|
||||||
|
private lateinit var binding: IncludeHomeHeaderBinding
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, parent: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return DataBindingUtil
|
||||||
|
.inflate<IncludeHomeHeaderBinding>(inflater, R.layout.include_home_header, parent, false).let {
|
||||||
|
binding = it
|
||||||
|
it.root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
package cash.z.android.wallet.ui.fragment
|
|
||||||
|
|
||||||
|
|
||||||
class ImportFragment : PlaceholderFragment()
|
|
|
@ -13,7 +13,7 @@ import cash.z.android.wallet.ui.activity.MainActivity
|
||||||
* Fragment for sending Zcash.
|
* Fragment for sending Zcash.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
open class PlaceholderFragment : Fragment() {
|
open class PlaceholderFragment : BaseFragment() {
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
@ -23,15 +23,4 @@ open class PlaceholderFragment : Fragment() {
|
||||||
return inflater.inflate(R.layout.fragment_placeholder, container, false)
|
return inflater.inflate(R.layout.fragment_placeholder, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
(activity as MainActivity).let { mainActivity ->
|
|
||||||
mainActivity.setSupportActionBar(view.findViewById(R.id.toolbar))
|
|
||||||
mainActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
mainActivity.supportActionBar?.setTitle(R.string.destination_title_placeholder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
package cash.z.android.wallet.ui.fragment
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import cash.z.android.wallet.ui.presenter.ProgressPresenter
|
||||||
|
import cash.z.wallet.sdk.data.Synchronizer
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
abstract class ProgressFragment(
|
||||||
|
@IdRes private val progressBarId: Int
|
||||||
|
) : BaseFragment(),
|
||||||
|
ProgressPresenter.ProgressView {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
protected lateinit var synchronizer: Synchronizer
|
||||||
|
|
||||||
|
protected lateinit var progressPresenter: ProgressPresenter
|
||||||
|
private lateinit var progressBar: ProgressBar
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
progressBar = view.findViewById(progressBarId)
|
||||||
|
// progressBar.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
// construct here rather than inject to make scoping easier
|
||||||
|
// so that we reduce the chances of updating progress on the wrong fragment
|
||||||
|
progressPresenter = ProgressPresenter(this, synchronizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
launch {
|
||||||
|
progressPresenter.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
progressPresenter.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showProgress(progress: Int) {
|
||||||
|
if (progress >= 100) {
|
||||||
|
onProgressComplete()
|
||||||
|
// progressBar.animate().(0.0f).apply {
|
||||||
|
// duration = 250L
|
||||||
|
// setListener(AnimatorCompleteListener {
|
||||||
|
progressBar.visibility = View.GONE
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
} else if (progress > 0 && progressBar.visibility != View.VISIBLE) {
|
||||||
|
progressBar.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
progressBar.progress = progress
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: replace this quick and dirty logic with something permanent
|
||||||
|
open fun getProgressText(progress: Int): String {
|
||||||
|
if (mainActivity == null) return ""
|
||||||
|
// cycle twice
|
||||||
|
val factor = 100 / (mainActivity!!.loadMessages.size * 2)
|
||||||
|
val index = (progress/factor).rem(mainActivity!!.loadMessages.size)
|
||||||
|
var message = "$progress% ${mainActivity?.nextLoadMessage(index)}"
|
||||||
|
if (progress > 98) message = "Done!"
|
||||||
|
if (progress >= 50) message = message.replace("Zooko", "Zooko AGAIN", true).replace("Learning to spell", "Double-checking the spelling of").replace("the kool", "MORE kool", true).replace("Making the sausage", "Getting a little hangry by now!", true)
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onProgressComplete() {}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package cash.z.android.wallet.ui.fragment
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -11,7 +12,9 @@ import cash.z.android.qrecycler.QRecycler
|
||||||
import cash.z.android.wallet.R
|
import cash.z.android.wallet.R
|
||||||
import cash.z.android.wallet.ui.activity.MainActivity
|
import cash.z.android.wallet.ui.activity.MainActivity
|
||||||
import cash.z.android.wallet.ui.util.AddressPartNumberSpan
|
import cash.z.android.wallet.ui.util.AddressPartNumberSpan
|
||||||
|
import cash.z.wallet.sdk.data.Synchronizer
|
||||||
import cash.z.wallet.sdk.jni.JniConverter
|
import cash.z.wallet.sdk.jni.JniConverter
|
||||||
|
import cash.z.wallet.sdk.secure.Wallet
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
import kotlinx.android.synthetic.main.fragment_receive.*
|
import kotlinx.android.synthetic.main.fragment_receive.*
|
||||||
|
@ -26,7 +29,7 @@ class ReceiveFragment : BaseFragment() {
|
||||||
lateinit var qrecycler: QRecycler
|
lateinit var qrecycler: QRecycler
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var converter: JniConverter
|
lateinit var synchronizer: Synchronizer
|
||||||
|
|
||||||
lateinit var addressParts: Array<TextView>
|
lateinit var addressParts: Array<TextView>
|
||||||
|
|
||||||
|
@ -40,11 +43,6 @@ class ReceiveFragment : BaseFragment() {
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
(activity as MainActivity).let { mainActivity ->
|
|
||||||
mainActivity.setSupportActionBar(view.findViewById(R.id.toolbar))
|
|
||||||
mainActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
mainActivity.supportActionBar?.setTitle(R.string.destination_title_receive)
|
|
||||||
}
|
|
||||||
addressParts = arrayOf(
|
addressParts = arrayOf(
|
||||||
text_address_part_1,
|
text_address_part_1,
|
||||||
text_address_part_2,
|
text_address_part_2,
|
||||||
|
@ -55,15 +53,22 @@ class ReceiveFragment : BaseFragment() {
|
||||||
text_address_part_8
|
text_address_part_8
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
mainActivity?.setToolbarShown(true)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
// TODO: replace these with channels. For now just wire the logic together
|
// TODO: replace these with channels. For now just wire the logic together
|
||||||
onAddressLoaded(loadAddress())
|
onAddressLoaded(loadAddress())
|
||||||
|
// converter.scanBlocks()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onAddressLoaded(address: String) {
|
private fun onAddressLoaded(address: String) {
|
||||||
|
Log.e("TWIG", "onAddressLoaded: $address")
|
||||||
qrecycler.load(address)
|
qrecycler.load(address)
|
||||||
.withQuietZoneSize(3)
|
.withQuietZoneSize(3)
|
||||||
.withCorrectionLevel(QRecycler.CorrectionLevel.MEDIUM)
|
.withCorrectionLevel(QRecycler.CorrectionLevel.MEDIUM)
|
||||||
|
@ -85,7 +90,7 @@ class ReceiveFragment : BaseFragment() {
|
||||||
|
|
||||||
// TODO: replace with tiered load. First check memory reference (textview contents?) then check DB, then load from JNI and write to DB
|
// TODO: replace with tiered load. First check memory reference (textview contents?) then check DB, then load from JNI and write to DB
|
||||||
private fun loadAddress(): String {
|
private fun loadAddress(): String {
|
||||||
return converter.getAddress("dummyseed".toByteArray())
|
return synchronizer.getAddress()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import cash.z.android.wallet.R
|
import cash.z.android.wallet.R
|
||||||
import cash.z.android.wallet.ui.activity.MainActivity
|
import cash.z.android.wallet.ui.activity.MainActivity
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
|
||||||
|
|
||||||
// TODO: Rename parameter arguments, choose names that match
|
// TODO: Rename parameter arguments, choose names that match
|
||||||
|
@ -25,7 +27,7 @@ private const val ARG_PARAM2 = "param2"
|
||||||
* create an instance of this fragment.
|
* create an instance of this fragment.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
class RequestFragment : Fragment() {
|
class RequestFragment : BaseFragment() {
|
||||||
// TODO: Rename and change types of parameters
|
// TODO: Rename and change types of parameters
|
||||||
private var param1: String? = null
|
private var param1: String? = null
|
||||||
private var param2: String? = null
|
private var param2: String? = null
|
||||||
|
@ -47,12 +49,6 @@ class RequestFragment : Fragment() {
|
||||||
return inflater.inflate(R.layout.fragment_request, container, false)
|
return inflater.inflate(R.layout.fragment_request, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
(activity as MainActivity).setSupportActionBar(view.findViewById(R.id.toolbar))
|
|
||||||
(activity as MainActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Rename method, update argument and hook method into UI event
|
// TODO: Rename method, update argument and hook method into UI event
|
||||||
fun onButtonPressed(uri: Uri) {
|
fun onButtonPressed(uri: Uri) {
|
||||||
listener?.onFragmentInteraction(uri)
|
listener?.onFragmentInteraction(uri)
|
||||||
|
@ -67,6 +63,11 @@ class RequestFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
mainActivity?.setToolbarShown(true)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDetach() {
|
override fun onDetach() {
|
||||||
super.onDetach()
|
super.onDetach()
|
||||||
listener = null
|
listener = null
|
||||||
|
@ -108,3 +109,9 @@ class RequestFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
abstract class RequestFragmentModule {
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun contributeRequestFragment(): RequestFragment
|
||||||
|
}
|
|
@ -0,0 +1,280 @@
|
||||||
|
package cash.z.android.wallet.ui.fragment
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
|
import android.hardware.camera2.CameraManager
|
||||||
|
import android.media.Image
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewAnimationUtils
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import cash.z.android.cameraview.CameraView
|
||||||
|
import cash.z.android.wallet.R
|
||||||
|
import cash.z.android.wallet.databinding.FragmentScanBinding
|
||||||
|
import cash.z.android.wallet.extention.Toaster
|
||||||
|
import com.google.firebase.ml.vision.FirebaseVision
|
||||||
|
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
|
||||||
|
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcodeDetectorOptions
|
||||||
|
import com.google.firebase.ml.vision.common.FirebaseVisionImage
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragment for scanning addresss, hopefully.
|
||||||
|
*/
|
||||||
|
class ScanFragment : BaseFragment() {
|
||||||
|
|
||||||
|
lateinit var binding: FragmentScanBinding
|
||||||
|
var barcodeCallback: BarcodeCallback? = null
|
||||||
|
|
||||||
|
interface BarcodeCallback {
|
||||||
|
fun onBarcodeScanned(value: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val revealCamera = Runnable {
|
||||||
|
binding.overlayBarcodeScan.apply {
|
||||||
|
val cX = measuredWidth / 2
|
||||||
|
val cY = measuredHeight / 2
|
||||||
|
ViewAnimationUtils.createCircularReveal(this, cX, cY, 0.0f, cX.toFloat()).start()
|
||||||
|
postDelayed({
|
||||||
|
val v:View = this
|
||||||
|
v.animate().alpha(0.0f).apply { duration = 2400L }.setListener(object : Animator.AnimatorListener {
|
||||||
|
override fun onAnimationRepeat(animation: Animator?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationStart(animation: Animator?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
|
binding.overlayBarcodeScan.visibility = View.GONE
|
||||||
|
}
|
||||||
|
override fun onAnimationCancel(animation: Animator?) {
|
||||||
|
binding.overlayBarcodeScan.visibility = View.GONE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},500L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val requiredPermissions: Array<String?>
|
||||||
|
get() {
|
||||||
|
return try {
|
||||||
|
val info = mainActivity?.packageManager
|
||||||
|
?.getPackageInfo(mainActivity?.packageName, PackageManager.GET_PERMISSIONS)
|
||||||
|
val ps = info?.requestedPermissions
|
||||||
|
if (ps != null && ps.isNotEmpty()) {
|
||||||
|
ps
|
||||||
|
} else {
|
||||||
|
arrayOfNulls(0)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
arrayOfNulls(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
return DataBindingUtil.inflate<FragmentScanBinding>(
|
||||||
|
inflater, R.layout.fragment_scan, container, false
|
||||||
|
).let {
|
||||||
|
binding = it
|
||||||
|
it.root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
// binding.previewCameraSource.doOnLayout {
|
||||||
|
// if (allPermissionsGranted()) {
|
||||||
|
// createCameraSource(it.width, it.height)
|
||||||
|
// } else {
|
||||||
|
// getRuntimePermissions()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
if(!allPermissionsGranted()) getRuntimePermissions()
|
||||||
|
|
||||||
|
|
||||||
|
// sendPresenter = SendPresenter(this, mainActivity?.synchronizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
binding.overlayBarcodeScan.post(revealCamera)
|
||||||
|
System.err.println("camoorah : onResume ScanFragment")
|
||||||
|
if(allPermissionsGranted()) onStartCamera()
|
||||||
|
// launch {
|
||||||
|
// sendPresenter.start()
|
||||||
|
// }
|
||||||
|
// startCameraSource()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
binding.cameraView.stop()
|
||||||
|
super.onPause()
|
||||||
|
// sendPresenter.stop()
|
||||||
|
// binding.previewCameraSource?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
// cameraSource?.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Camera */
|
||||||
|
// private fun createCameraSource(width: Int, height: Int) {
|
||||||
|
// Toaster.short("w: $width h: $height")
|
||||||
|
// // If there's no existing cameraSource, create one.
|
||||||
|
// if (cameraSource == null) {
|
||||||
|
// cameraSource = CameraSource(mainActivity, binding.graphicOverlay)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// try {
|
||||||
|
// cameraSource?.setMachineLearningFrameProcessor(BarcodeScanningProcessor())
|
||||||
|
// } catch (e: FirebaseMLException) {
|
||||||
|
// Log.e("temporaryBehavior", "can not create camera source")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts or restarts the camera source, if it exists. If the camera source doesn't exist yet
|
||||||
|
* (e.g., because onResume was called before the camera source was created), this will be called
|
||||||
|
* again when the camera source is created.
|
||||||
|
*/
|
||||||
|
// private fun startCameraSource() {
|
||||||
|
// cameraSource?.let {
|
||||||
|
// try {
|
||||||
|
// binding.previewCameraSource?.start(cameraSource!!, binding.graphicOverlay)
|
||||||
|
// } catch (e: IOException) {
|
||||||
|
// Log.e("temporaryBehavior", "Unable to start camera source.", e)
|
||||||
|
// cameraSource?.release()
|
||||||
|
// cameraSource = null
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
/* Permissions */
|
||||||
|
|
||||||
|
private fun allPermissionsGranted(): Boolean {
|
||||||
|
for (permission in requiredPermissions) {
|
||||||
|
if (!isPermissionGranted(mainActivity!!, permission!!)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRuntimePermissions() {
|
||||||
|
val allNeededPermissions = arrayListOf<String>()
|
||||||
|
for (permission in requiredPermissions) {
|
||||||
|
if (!isPermissionGranted(mainActivity!!, permission!!)) {
|
||||||
|
allNeededPermissions.add(permission)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allNeededPermissions.isEmpty()) {
|
||||||
|
requestPermissions(allNeededPermissions.toTypedArray(), CAMERA_PERMISSION_REQUEST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
if (allPermissionsGranted()) {
|
||||||
|
view!!.postDelayed({
|
||||||
|
onStartCamera()
|
||||||
|
},2000L) // TODO: remove this temp hack to sidestep crash when permissions were not available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onStartCamera() {
|
||||||
|
with(binding.cameraView) {
|
||||||
|
// workaround race conditions with google play services downloading the binaries for Firebase Vision APIs
|
||||||
|
postDelayed({
|
||||||
|
firebaseCallback = PoCallback()
|
||||||
|
start()
|
||||||
|
}, 1000L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class PoCallback : CameraView.FirebaseCallback {
|
||||||
|
val options = FirebaseVisionBarcodeDetectorOptions.Builder()
|
||||||
|
.setBarcodeFormats(FirebaseVisionBarcode.FORMAT_QR_CODE)
|
||||||
|
.build()
|
||||||
|
val barcodeDetector = FirebaseVision.getInstance().getVisionBarcodeDetector(options)
|
||||||
|
var cameraId = getBackCameraId()
|
||||||
|
|
||||||
|
private fun getBackCameraId(): String {
|
||||||
|
val manager = mainActivity?.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||||
|
|
||||||
|
for (cameraId in manager.cameraIdList) {
|
||||||
|
val characteristics = manager.getCameraCharacteristics(cameraId)
|
||||||
|
val cOrientation = characteristics.get(CameraCharacteristics.LENS_FACING)!!
|
||||||
|
if (cOrientation == CameraCharacteristics.LENS_FACING_BACK) return cameraId
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("no rear-facing camera found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onImageAvailable(image: Image) {
|
||||||
|
if(mainActivity == null) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
System.err.println("camoorah : onImageAvailable: $image width: ${image.width} height: ${image.height}")
|
||||||
|
var firebaseImage =
|
||||||
|
FirebaseVisionImage.fromMediaImage(image, getRotationCompensation(cameraId, mainActivity!!))
|
||||||
|
barcodeDetector
|
||||||
|
.detectInImage(firebaseImage)
|
||||||
|
.addOnSuccessListener { results ->
|
||||||
|
if (results.isNotEmpty()) {
|
||||||
|
val barcode = results[0]
|
||||||
|
val value = barcode.rawValue
|
||||||
|
onScanSuccess(value!!)
|
||||||
|
// TODO: highlight the barcode
|
||||||
|
var bounds = barcode.boundingBox
|
||||||
|
var corners = barcode.cornerPoints
|
||||||
|
binding.cameraView.setBarcode(barcode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
System.err.println("camoorah : error while processing onImageAvailable: $t\n\tcaused by: ${t.cause}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var pendingSuccess = false
|
||||||
|
private fun onScanSuccess(value: String) {
|
||||||
|
binding.cameraView.stop()
|
||||||
|
if (!pendingSuccess) {
|
||||||
|
pendingSuccess = true
|
||||||
|
binding.cameraView.post {
|
||||||
|
barcodeCallback?.onBarcodeScanned(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// TODO: continue doing permissions here in a more specific, less general way
|
||||||
|
private const val CAMERA_PERMISSION_REQUEST = 1001
|
||||||
|
|
||||||
|
private fun isPermissionGranted(context: Context, permission: String): Boolean {
|
||||||
|
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
abstract class ScanFragmentModule {
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun contributeScanFragment(): ScanFragment
|
||||||
|
}
|
|
@ -1,37 +1,389 @@
|
||||||
package cash.z.android.wallet.ui.fragment
|
package cash.z.android.wallet.ui.fragment
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.graphics.Typeface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.text.style.StyleSpan
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.appcompat.widget.TooltipCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
|
import androidx.core.text.toSpannable
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import cash.z.android.wallet.BuildConfig
|
||||||
import cash.z.android.wallet.R
|
import cash.z.android.wallet.R
|
||||||
import cash.z.android.wallet.ui.activity.MainActivity
|
import cash.z.android.wallet.databinding.FragmentSendBinding
|
||||||
|
import cash.z.android.wallet.extention.*
|
||||||
|
import cash.z.android.wallet.sample.SampleProperties
|
||||||
|
import cash.z.android.wallet.ui.presenter.SendPresenter
|
||||||
|
import cash.z.android.wallet.ui.presenter.SendPresenterModule
|
||||||
|
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment for sending Zcash.
|
* Fragment for sending Zcash.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
class SendFragment : Fragment() {
|
class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.BarcodeCallback {
|
||||||
|
|
||||||
|
private val zec = R.string.zec_abbreviation.toAppString()
|
||||||
|
private val usd = R.string.usd_abbreviation.toAppString()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var sendPresenter: SendPresenter
|
||||||
|
private lateinit var binding: FragmentSendBinding
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
//
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
// Inflate the layout for this fragment
|
return DataBindingUtil.inflate<FragmentSendBinding>(
|
||||||
return inflater.inflate(R.layout.fragment_send, container, false)
|
inflater, R.layout.fragment_send, container, false
|
||||||
|
).let {
|
||||||
|
binding = it
|
||||||
|
it.root
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onAttachFragment(childFragment: Fragment) {
|
||||||
|
super.onAttachFragment(childFragment)
|
||||||
|
(childFragment as? ScanFragment)?.barcodeCallback = this
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
(activity as MainActivity).let { mainActivity ->
|
init()
|
||||||
mainActivity.setSupportActionBar(view.findViewById(R.id.toolbar))
|
}
|
||||||
mainActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
mainActivity.supportActionBar?.setTitle(R.string.destination_title_send)
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
mainActivity?.setToolbarShown(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
launch {
|
||||||
|
sendPresenter.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
sendPresenter.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// SendView Implementation
|
||||||
|
//
|
||||||
|
|
||||||
|
override fun exit() {
|
||||||
|
mainActivity?.navController?.navigate(R.id.nav_home_fragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setHeaders(isUsdSelected: Boolean, headerString: String, subheaderString: String) {
|
||||||
|
showCurrencySymbols(isUsdSelected)
|
||||||
|
setHeaderValue(headerString)
|
||||||
|
setSubheaderValue(subheaderString, isUsdSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setHeaderValue(value: String) {
|
||||||
|
binding.textValueHeader.setText(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n") // SetTextI18n lint logic has errors and does not recognize that the entire string contains variables, formatted per locale and loaded from string resources.
|
||||||
|
override fun setSubheaderValue(value: String, isUsdSelected: Boolean) {
|
||||||
|
val subheaderLabel = if (isUsdSelected) zec else usd
|
||||||
|
binding.textValueSubheader.text = "$value $subheaderLabel" //ignore SetTextI18n error here because it is invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showSendDialog(zecString: String, usdString: String, toAddress: String, hasMemo: Boolean) {
|
||||||
|
hideKeyboard()
|
||||||
|
setSendEnabled(false) // partially because we need to lower the button elevation
|
||||||
|
binding.dialogTextTitle.text = getString(R.string.send_dialog_title, zecString, zec, usdString)
|
||||||
|
binding.dialogTextAddress.text = toAddress
|
||||||
|
binding.dialogTextMemoIncluded.visibility = if(hasMemo) View.VISIBLE else View.GONE
|
||||||
|
binding.groupDialogSend.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateAvailableBalance(new: Long) {
|
||||||
|
// TODO: use a formatted string resource here
|
||||||
|
val availableTextSpan = "${new.convertZatoshiToZecString(8)} $zec Available".toSpannable()
|
||||||
|
availableTextSpan.setSpan(ForegroundColorSpan(R.color.colorPrimary.toAppColor()), availableTextSpan.length - "Available".length, availableTextSpan.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
availableTextSpan.setSpan(StyleSpan(Typeface.BOLD), 0, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
binding.textZecValueAvailable.text = availableTextSpan
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSendEnabled(isEnabled: Boolean) {
|
||||||
|
binding.buttonSendZec.isEnabled = isEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// ScanFragment.BarcodeCallback implemenation
|
||||||
|
//
|
||||||
|
|
||||||
|
override fun onBarcodeScanned(value: String) {
|
||||||
|
exitScanMode()
|
||||||
|
binding.inputZcashAddress.setText(value)
|
||||||
|
sendPresenter.inputAddressUpdated(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Internal View Logic
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize view logic only. Click listeners, text change handlers and tooltips.
|
||||||
|
*/
|
||||||
|
private fun init() {
|
||||||
|
|
||||||
|
/* Init - Text Input */
|
||||||
|
|
||||||
|
binding.textValueHeader.apply {
|
||||||
|
setSelectAllOnFocus(true)
|
||||||
|
afterTextChanged { if (it.isNotEmpty()) sendPresenter.inputHeaderUpdating(it) }
|
||||||
|
doOnDoneOrFocusLost { sendPresenter.inputHeaderUpdated(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.inputZcashAddress.apply {
|
||||||
|
afterTextChanged { if (it.isNotEmpty()) sendPresenter.inputAddressUpdating(it) }
|
||||||
|
doOnDoneOrFocusLost { sendPresenter.inputAddressUpdated(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.textAreaMemo.apply {
|
||||||
|
afterTextChanged {
|
||||||
|
if (it.isNotEmpty()) sendPresenter.inputMemoUpdating(it)
|
||||||
|
binding.textMemoCharCount.text = "${text.length} / ${resources.getInteger(R.integer.memo_max_length)}"
|
||||||
|
}
|
||||||
|
doOnDoneOrFocusLost { sendPresenter.inputMemoUpdated(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Init - Taps */
|
||||||
|
|
||||||
|
binding.imageSwapCurrency.setOnClickListener {
|
||||||
|
// validate the amount before we toggle (or else we lose their uncommitted change)
|
||||||
|
sendPresenter.inputHeaderUpdated(binding.textValueHeader.text.toString())
|
||||||
|
sendPresenter.inputToggleCurrency()
|
||||||
|
}
|
||||||
|
binding.buttonSendZec.setOnClickListener{
|
||||||
|
exitScanMode()
|
||||||
|
sendPresenter.inputSendPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow background taps to dismiss the keyboard and clear focus
|
||||||
|
binding.contentFragmentSend.setOnClickListener {
|
||||||
|
sendPresenter.invalidate()
|
||||||
|
hideKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Non-Presenter calls (UI-only logic) */
|
||||||
|
|
||||||
|
binding.imageScanQr.apply {
|
||||||
|
TooltipCompat.setTooltipText(this, context.getString(R.string.send_tooltip_scan_qr))
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.imageAddressShortcut?.apply {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
TooltipCompat.setTooltipText(this, context.getString(R.string.send_tooltip_address_shortcut))
|
||||||
|
setOnClickListener(::onPasteShortcutAddress)
|
||||||
|
} else {
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.dialogSendBackground.setOnClickListener { hideSendDialog() }
|
||||||
|
binding.dialogSubmitButton.setOnClickListener { onSendZec() }
|
||||||
|
binding.imageScanQr.setOnClickListener(::onScanQrCode)
|
||||||
|
binding.buttonSendZec.text = getString(R.string.send_button_label, zec)
|
||||||
|
setSendEnabled(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showCurrencySymbols(isUsdSelected: Boolean) {
|
||||||
|
// visibility has some kind of bug that appears to be related to layout groups. So using alpha instead since our API level is high enough to support that
|
||||||
|
if (isUsdSelected) {
|
||||||
|
binding.textDollarSymbolHeader.alpha = 1.0f
|
||||||
|
binding.imageZecSymbolSubheader.alpha = 1.0f
|
||||||
|
binding.imageZecSymbolHeader.alpha = 0.0f
|
||||||
|
binding.textDollarSymbolSubheader.alpha = 0.0f
|
||||||
|
} else {
|
||||||
|
binding.imageZecSymbolHeader.alpha = 1.0f
|
||||||
|
binding.textDollarSymbolSubheader.alpha = 1.0f
|
||||||
|
binding.textDollarSymbolHeader.alpha = 0.0f
|
||||||
|
binding.imageZecSymbolSubheader.alpha = 0.0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onScanQrCode(view: View) {
|
||||||
|
hideKeyboard()
|
||||||
|
val fragment = ScanFragment()
|
||||||
|
val ft = childFragmentManager.beginTransaction()
|
||||||
|
.add(R.id.camera_placeholder, fragment, "camera_fragment")
|
||||||
|
.addToBackStack("camera_fragment_scanning")
|
||||||
|
.commit()
|
||||||
|
|
||||||
|
binding.groupHiddenDuringScan.visibility = View.INVISIBLE
|
||||||
|
binding.buttonCancelScan.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
animate().alpha(1.0f).apply {
|
||||||
|
duration = 3000L
|
||||||
|
}
|
||||||
|
setOnClickListener {
|
||||||
|
exitScanMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: possibly move this behavior to only live in the debug build. Perhaps with a viewholder that I just delegate to. Then inject the holder in this class with production verstion getting an empty implementation that just hides the icon.
|
||||||
|
private fun onPasteShortcutAddress(view: View) {
|
||||||
|
view.context.alert(R.string.send_alert_shortcut_clicked) {
|
||||||
|
val address = SampleProperties.wallet.defaultSendAddress
|
||||||
|
binding.inputZcashAddress.setText(address)
|
||||||
|
sendPresenter.inputAddressUpdated(address)
|
||||||
|
hideKeyboard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after confirmation dialog is affirmed. Begins the process of actually sending ZEC.
|
||||||
|
*/
|
||||||
|
private fun onSendZec() {
|
||||||
|
setSendEnabled(false)
|
||||||
|
sendPresenter.sendFunds()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun exitScanMode() {
|
||||||
|
val cameraFragment = childFragmentManager.findFragmentByTag("camera_fragment")
|
||||||
|
if (cameraFragment != null) {
|
||||||
|
val ft = childFragmentManager.beginTransaction()
|
||||||
|
.remove(cameraFragment)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
binding.buttonCancelScan.visibility = View.GONE
|
||||||
|
binding.groupHiddenDuringScan.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideKeyboard() {
|
||||||
|
mainActivity?.getSystemService<InputMethodManager>()
|
||||||
|
?.hideSoftInputFromWindow(view?.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||||
|
checkAllInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideSendDialog() {
|
||||||
|
setSendEnabled(true)
|
||||||
|
binding.groupDialogSend.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAddressLineColor(@ColorRes colorRes: Int = R.color.zcashBlack_12) {
|
||||||
|
if (mainActivity != null) {
|
||||||
|
DrawableCompat.setTint(
|
||||||
|
binding.inputZcashAddress.background,
|
||||||
|
ContextCompat.getColor(mainActivity!!, colorRes)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Error handling */
|
||||||
|
|
||||||
|
override fun setAmountError(message: String?) {
|
||||||
|
if (message == null) {
|
||||||
|
binding.textValueError.visibility = View.GONE
|
||||||
|
binding.textValueError.text = null
|
||||||
|
} else {
|
||||||
|
binding.textValueError.text = message
|
||||||
|
binding.textValueError.visibility = View.VISIBLE
|
||||||
|
setSendEnabled(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAddressError(message: String?) {
|
||||||
|
if (message == null) {
|
||||||
|
setAddressLineColor()
|
||||||
|
binding.textAddressError.text = null
|
||||||
|
binding.textAddressError.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
setAddressLineColor(R.color.zcashRed)
|
||||||
|
binding.textAddressError.text = message
|
||||||
|
binding.textAddressError.visibility = View.VISIBLE
|
||||||
|
setSendEnabled(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMemoError(message: String?) {
|
||||||
|
val validColor = R.color.zcashBlack_12.toAppColor()
|
||||||
|
val errorColor = R.color.zcashRed.toAppColor()
|
||||||
|
if (message == null) {
|
||||||
|
binding.dividerMemo.setBackgroundColor(validColor)
|
||||||
|
binding.textMemoCharCount.setTextColor(validColor)
|
||||||
|
binding.textAreaMemo.setTextColor(R.color.text_dark.toAppColor())
|
||||||
|
} else {
|
||||||
|
binding.dividerMemo.setBackgroundColor(errorColor)
|
||||||
|
binding.textMemoCharCount.setTextColor(errorColor)
|
||||||
|
binding.textAreaMemo.setTextColor(errorColor)
|
||||||
|
setSendEnabled(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate all input. This is essentially the same as extracting a model out of the view and validating it with the
|
||||||
|
* presenter. Basically, this needs to happen anytime something is edited, in order to try and enable Send. Right
|
||||||
|
* now this method is called 1) any time the model is updated with valid input, 2) anytime the keyboard is hidden,
|
||||||
|
* and 3) anytime send is pressed. It also triggers the only logic that can set "requiresValidation" to false.
|
||||||
|
*/
|
||||||
|
override fun checkAllInput(): Boolean {
|
||||||
|
with(binding) {
|
||||||
|
return sendPresenter.inputHeaderUpdated(textValueHeader.text.toString())
|
||||||
|
&& sendPresenter.inputAddressUpdated(inputZcashAddress.text.toString())
|
||||||
|
&& sendPresenter.inputMemoUpdated(textAreaMemo.text.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: come back to this test code later and fix the shared element transitions
|
||||||
|
//
|
||||||
|
// fun submitWithSharedElements() {
|
||||||
|
// var extras = with(binding) {
|
||||||
|
// listOf(dialogSendBackground, dialogSendContents, dialogTextTitle, dialogTextAddress)
|
||||||
|
// .map{ it to it.transitionName }
|
||||||
|
// .let { FragmentNavigatorExtras(*it.toTypedArray()) }
|
||||||
|
// }
|
||||||
|
// val extras = FragmentNavigatorExtras(
|
||||||
|
// binding.dialogSendContents to binding.dialogSendContents.transitionName,
|
||||||
|
// binding.dialogTextTitle to getString(R.string.transition_active_transaction_title),
|
||||||
|
// binding.dialogTextAddress to getString(R.string.transition_active_transaction_address),
|
||||||
|
// binding.dialogSendBackground to getString(R.string.transition_active_transaction_background)
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// mainActivity?.navController.navigate(R.id.nav_home_fragment,
|
||||||
|
// null,
|
||||||
|
// null,
|
||||||
|
// extras)
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Module
|
||||||
|
abstract class SendFragmentModule {
|
||||||
|
@ContributesAndroidInjector(modules = [SendPresenterModule::class])
|
||||||
|
abstract fun contributeSendFragment(): SendFragment
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,118 @@
|
||||||
package cash.z.android.wallet.ui.fragment
|
package cash.z.android.wallet.ui.fragment
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import cash.z.android.wallet.R
|
||||||
|
import cash.z.android.wallet.databinding.FragmentSettingsBinding
|
||||||
|
import cash.z.android.wallet.extention.Toaster
|
||||||
|
import cash.z.android.wallet.extention.alert
|
||||||
|
import cash.z.android.wallet.sample.SampleProperties
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SettingsFragment : PlaceholderFragment()
|
|
||||||
|
class SettingsFragment : BaseFragment() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var prefs: SharedPreferences
|
||||||
|
lateinit var binding: FragmentSettingsBinding
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return DataBindingUtil
|
||||||
|
.inflate<FragmentSettingsBinding>(inflater, R.layout.fragment_settings, container, false)
|
||||||
|
.also { binding = it }
|
||||||
|
.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
mainActivity?.setToolbarShown(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
binding.buttonResetApp.setOnClickListener {
|
||||||
|
view.context.alert(R.string.settings_alert_reset_app) {
|
||||||
|
Toaster.short("Not Yet Implemented!")
|
||||||
|
mainActivity?.navController?.navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.includeToolbar.toolbarApplyOrClose.findViewById<ImageView>(R.id.image_close).apply {
|
||||||
|
setOnClickListener {
|
||||||
|
mainActivity?.navController?.navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.includeToolbar.toolbarApplyOrClose.findViewById<ImageView>(R.id.image_apply).apply {
|
||||||
|
setOnClickListener {
|
||||||
|
val userName = binding.spinnerDemoUser.selectedItem.toString()
|
||||||
|
val server = binding.spinnerServers.selectedItem.toString()
|
||||||
|
view.context.alert("Are you sure you want to apply these changes?\n\nUser: $userName\nServer: $server\n\nTHIS WILL EXIT THE APP!") {
|
||||||
|
onApplySettings(userName, server)
|
||||||
|
// TODO: handle this whole reset thing better. For now, just aggressively kill the app. A better
|
||||||
|
// approach is to create a custom scope for the synchronizer and then just manage that like any
|
||||||
|
// other subcomponent. In that scenario, we would simply navigate up from this fragment at this
|
||||||
|
// point (after installing a new synchronizer subcomponent)
|
||||||
|
view.postDelayed({
|
||||||
|
mainActivity?.finish()
|
||||||
|
Thread.sleep(1000L) // if you're going to cut a corner, lean into it! sleep FTW!
|
||||||
|
android.os.Process.killProcess(android.os.Process.myPid())
|
||||||
|
}, 2000L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
binding.spinnerServers.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||||
|
setCustomServerUiShown(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
val item = binding.spinnerDemoUser.selectedItem.toString()
|
||||||
|
setCustomServerUiShown(item.startsWith("Custom"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.spinnerDemoUser.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||||
|
setCustomUserUiShown(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
val item = binding.spinnerDemoUser.selectedItem.toString()
|
||||||
|
setCustomUserUiShown(item.startsWith("Custom"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setCustomServerUiShown(isShown: Boolean) {
|
||||||
|
if (isShown) Toaster.short("Custom servers are not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setCustomUserUiShown(isShown: Boolean) {
|
||||||
|
if (isShown) Toaster.short("Custom users are not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onApplySettings(userName: String, server: String) {
|
||||||
|
AlertDialog.Builder(mainActivity!!).setMessage("Changing everything...").show()
|
||||||
|
prefs.edit().apply {
|
||||||
|
putString(SampleProperties.PREFS_SERVER_NAME, server)
|
||||||
|
putString(SampleProperties.PREFS_WALLET_DISPLAY_NAME, userName)
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
abstract class SettingsFragmentModule {
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun contributeSettingsFragment(): SettingsFragment
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
package cash.z.android.wallet.ui.fragment
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.doOnPreDraw
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.transition.TransitionInflater
|
||||||
|
import cash.z.android.wallet.R
|
||||||
|
import cash.z.android.wallet.databinding.FragmentSyncBinding
|
||||||
|
import cash.z.android.wallet.extention.alert
|
||||||
|
import cash.z.android.wallet.extention.showOk
|
||||||
|
import cash.z.android.wallet.ui.presenter.Presenter
|
||||||
|
import cash.z.android.wallet.ui.presenter.ProgressPresenter
|
||||||
|
import cash.z.wallet.sdk.data.Synchronizer
|
||||||
|
import cash.z.wallet.sdk.data.twig
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
class SyncFragment : ProgressFragment(R.id.progress_sync) {
|
||||||
|
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentSyncBinding
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
//
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
setupSharedElementTransitions()
|
||||||
|
return DataBindingUtil.inflate<FragmentSyncBinding>(
|
||||||
|
inflater, R.layout.fragment_sync, container, false
|
||||||
|
).let {
|
||||||
|
binding = it
|
||||||
|
it.root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
postponeEnterTransition()
|
||||||
|
binding.buttonNext.setOnClickListener {
|
||||||
|
mainActivity?.navController?.navigate(R.id.action_sync_fragment_to_home_fragment,
|
||||||
|
null,
|
||||||
|
NavOptions.Builder().setPopUpTo(R.id.mobile_navigation, true).build(),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
binding.progressSync.visibility = View.INVISIBLE
|
||||||
|
binding.textProgressSync.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
(view?.parent as? ViewGroup)?.doOnPreDraw {
|
||||||
|
startPostponedEnterTransition()
|
||||||
|
}
|
||||||
|
synchronizer.onSynchronizerErrorListener = ::onSynchronizerError
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
mainActivity?.setDrawerLocked(true)
|
||||||
|
mainActivity?.setToolbarShown(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSharedElementTransitions() {
|
||||||
|
TransitionInflater.from(mainActivity).inflateTransition(R.transition.transition_zec_sent).apply {
|
||||||
|
duration = 250L
|
||||||
|
// addListener(this@SyncFragment)
|
||||||
|
this@SyncFragment.sharedElementEnterTransition = this
|
||||||
|
this@SyncFragment.sharedElementReturnTransition = this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showProgress(progress: Int) {
|
||||||
|
binding.textProgressSync.text = getProgressText(progress)
|
||||||
|
binding.textProgressSync.visibility = View.VISIBLE
|
||||||
|
super.showProgress(progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProgressComplete() {
|
||||||
|
super.onProgressComplete()
|
||||||
|
binding.textProgressSync.visibility = View.GONE
|
||||||
|
with (binding.buttonNext) {
|
||||||
|
isEnabled = true
|
||||||
|
alpha = 0.3f
|
||||||
|
animate().alpha(1.0f).duration = 300L
|
||||||
|
text = "Start"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSynchronizerError(error: Throwable?): Boolean {
|
||||||
|
context?.alert(
|
||||||
|
message = "WARNING: A critical error has occurred and " +
|
||||||
|
"this app will not function properly until that is corrected!",
|
||||||
|
positiveButtonResId = R.string.ignore,
|
||||||
|
negativeButtonResId = R.string.details,
|
||||||
|
negativeAction = { context?.alert("Synchronization error:\n\n$error") }
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
abstract class SyncFragmentModule {
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun contributeSyncFragment(): SyncFragment
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
package cash.z.android.wallet.ui.fragment
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.navigation.fragment.FragmentNavigatorExtras
|
||||||
|
import androidx.transition.TransitionInflater
|
||||||
|
import cash.z.android.wallet.BuildConfig
|
||||||
|
import cash.z.android.wallet.R
|
||||||
|
import cash.z.android.wallet.databinding.FragmentWelcomeBinding
|
||||||
|
import cash.z.android.wallet.sample.SampleProperties
|
||||||
|
import cash.z.android.wallet.sample.WalletConfig
|
||||||
|
import cash.z.wallet.sdk.data.twig
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
|
||||||
|
class WelcomeFragment : ProgressFragment(R.id.progress_welcome) {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var walletConfig: WalletConfig
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var prefs: SharedPreferences
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentWelcomeBinding
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
//
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
setupSharedElementTransitions()
|
||||||
|
return DataBindingUtil.inflate<FragmentWelcomeBinding>(
|
||||||
|
inflater, R.layout.fragment_welcome, container, false
|
||||||
|
).let {
|
||||||
|
binding = it
|
||||||
|
it.root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
val userName = walletConfig.displayName.substringAfterLast('.').capitalize()
|
||||||
|
val serverName = prefs.getString(SampleProperties.PREFS_SERVER_NAME, "Unknown")
|
||||||
|
val network = if (resources.getBoolean(R.bool.is_testnet)) "Testnet 2.0.1" else "Mainnet 2.0.1"
|
||||||
|
var buildInfo = "PoC v${BuildConfig.VERSION_NAME} $network\n" +
|
||||||
|
"Zcash Company - For demo purposes only\nUser: $userName\nServer: $serverName"
|
||||||
|
binding.textWelcomeBuildInfo.text = buildInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
view!!.postDelayed({
|
||||||
|
launch {
|
||||||
|
onNext()
|
||||||
|
}
|
||||||
|
}, 5000L)
|
||||||
|
|
||||||
|
// this.setExitSharedElementCallback(object : SharedElementCallback() {
|
||||||
|
// override fun onCaptureSharedElementSnapshot(
|
||||||
|
// sharedElement: View,
|
||||||
|
// viewToGlobalMatrix: Matrix,
|
||||||
|
// screenBounds: RectF
|
||||||
|
// ): Parcelable? {
|
||||||
|
// val width = Math.round(screenBounds.width())
|
||||||
|
// val height = Math.round(screenBounds.height())
|
||||||
|
// var bitmap: Bitmap? = null
|
||||||
|
// if (width > 0 && height > 0) {
|
||||||
|
// val matrix = Matrix()
|
||||||
|
// matrix.set(viewToGlobalMatrix)
|
||||||
|
// matrix.postTranslate(screenBounds.left, screenBounds.top)
|
||||||
|
// bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
|
// val canvas = Canvas(bitmap)
|
||||||
|
// canvas.concat(matrix)
|
||||||
|
// sharedElement.draw(canvas)
|
||||||
|
// }
|
||||||
|
// return bitmap
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
mainActivity?.setDrawerLocked(true)
|
||||||
|
mainActivity?.setToolbarShown(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSharedElementTransitions() {
|
||||||
|
TransitionInflater.from(mainActivity).inflateTransition(R.transition.transition_zec_sent).apply {
|
||||||
|
duration = 2500L
|
||||||
|
this@WelcomeFragment.sharedElementReturnTransition = this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private suspend fun onNext() = coroutineScope {
|
||||||
|
if (mainActivity != null) {
|
||||||
|
val isFirstRun = mainActivity!!.synchronizer.isFirstRun()
|
||||||
|
val destination =
|
||||||
|
if (isFirstRun) R.id.action_welcome_fragment_to_firstrun_fragment
|
||||||
|
else R.id.action_welcome_fragment_to_sync_fragment
|
||||||
|
|
||||||
|
// var extras = with(binding) {
|
||||||
|
// listOf(progressWelcome, textProgressWelcome)
|
||||||
|
// .map { it to it.transitionName }
|
||||||
|
// .let { FragmentNavigatorExtras(*it.toTypedArray()) }
|
||||||
|
// }
|
||||||
|
val extras = FragmentNavigatorExtras(
|
||||||
|
binding.progressWelcome to binding.progressWelcome.transitionName
|
||||||
|
)
|
||||||
|
|
||||||
|
mainActivity?.navController?.navigate(
|
||||||
|
destination,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
// NavOptions.Builder().setPopUpTo(R.id.mobile_navigation, true).build(),
|
||||||
|
extras
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
abstract class WelcomeFragmentModule {
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun contributeWelcomeFragment(): WelcomeFragment
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package cash.z.android.wallet.ui.presenter
|
||||||
|
|
||||||
|
import cash.z.android.wallet.di.annotation.FragmentScope
|
||||||
|
import cash.z.android.wallet.ui.fragment.HistoryFragment
|
||||||
|
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
|
||||||
|
import cash.z.wallet.sdk.dao.WalletTransaction
|
||||||
|
import cash.z.wallet.sdk.data.Synchronizer
|
||||||
|
import cash.z.wallet.sdk.data.twig
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
class HistoryPresenter @Inject constructor(
|
||||||
|
private val view: HistoryFragment,
|
||||||
|
private var synchronizer: Synchronizer
|
||||||
|
) : Presenter {
|
||||||
|
|
||||||
|
private var job: Job? = null
|
||||||
|
|
||||||
|
interface HistoryView : PresenterView {
|
||||||
|
fun setTransactions(transactions: List<WalletTransaction>)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun start() {
|
||||||
|
job?.cancel()
|
||||||
|
job = Job()
|
||||||
|
twig("historyPresenter starting!")
|
||||||
|
view.launchTransactionBinder(synchronizer.allTransactions())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
twig("historyPresenter stopping!")
|
||||||
|
job?.cancel()?.also { job = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.launchTransactionBinder(channel: ReceiveChannel<List<WalletTransaction>>) = launch {
|
||||||
|
twig("transaction binder starting!")
|
||||||
|
for (walletTransactionList in channel) {
|
||||||
|
twig("received ${walletTransactionList.size} transactions for presenting")
|
||||||
|
bind(walletTransactionList)
|
||||||
|
}
|
||||||
|
twig("transaction binder exiting!")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// View Callbacks on Main Thread
|
||||||
|
//
|
||||||
|
|
||||||
|
private fun bind(transactions: List<WalletTransaction>) {
|
||||||
|
twig("binding ${transactions.size} walletTransactions")
|
||||||
|
view.setTransactions(transactions.sortedByDescending {
|
||||||
|
if (!it.isMined && it.isSend) Long.MAX_VALUE else it.timeInSeconds
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Module
|
||||||
|
abstract class HistoryPresenterModule {
|
||||||
|
@Binds
|
||||||
|
@FragmentScope
|
||||||
|
abstract fun providePresenter(historyPresenter: HistoryPresenter): Presenter
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
package cash.z.android.wallet.ui.presenter
|
||||||
|
|
||||||
|
import cash.z.android.wallet.di.annotation.FragmentScope
|
||||||
|
import cash.z.android.wallet.extention.alert
|
||||||
|
import cash.z.android.wallet.ui.fragment.HomeFragment
|
||||||
|
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
|
||||||
|
import cash.z.wallet.sdk.dao.WalletTransaction
|
||||||
|
import cash.z.wallet.sdk.data.*
|
||||||
|
import cash.z.wallet.sdk.secure.Wallet
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class HomePresenter @Inject constructor(
|
||||||
|
private val view: HomeFragment,
|
||||||
|
private val synchronizer: Synchronizer
|
||||||
|
) : Presenter {
|
||||||
|
|
||||||
|
private var job: Job? = null
|
||||||
|
|
||||||
|
interface HomeView : PresenterView {
|
||||||
|
fun setTransactions(transactions: List<WalletTransaction>)
|
||||||
|
fun updateBalance(old: Long, new: Long)
|
||||||
|
fun setActiveTransactions(activeTransactionMap: Map<ActiveTransaction, TransactionState>)
|
||||||
|
fun onCancelledTooLate()
|
||||||
|
fun onSynchronizerError(error: Throwable?): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun start() {
|
||||||
|
job?.cancel()
|
||||||
|
job = Job()
|
||||||
|
twig("homePresenter starting! from ${this.hashCode()}")
|
||||||
|
with(view) {
|
||||||
|
launchBalanceBinder(synchronizer.balance())
|
||||||
|
launchTransactionBinder(synchronizer.allTransactions())
|
||||||
|
launchActiveTransactionMonitor(synchronizer.activeTransactions())
|
||||||
|
}
|
||||||
|
synchronizer.onSynchronizerErrorListener = view::onSynchronizerError
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
twig("homePresenter stopping!")
|
||||||
|
job?.cancel()?.also { job = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.launchBalanceBinder(channel: ReceiveChannel<Wallet.WalletBalance>) = launch {
|
||||||
|
var old: Long? = null
|
||||||
|
twig("balance binder starting!")
|
||||||
|
for (new in channel) {
|
||||||
|
twig("polled a balance item")
|
||||||
|
bind(old, new.total).also { old = new.total }
|
||||||
|
}
|
||||||
|
twig("balance binder exiting!")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.launchTransactionBinder(channel: ReceiveChannel<List<WalletTransaction>>) = launch {
|
||||||
|
twig("transaction binder starting!")
|
||||||
|
for (walletTransactionList in channel) {
|
||||||
|
twig("received ${walletTransactionList.size} transactions for presenting")
|
||||||
|
bind(walletTransactionList)
|
||||||
|
}
|
||||||
|
twig("transaction binder exiting!")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.launchActiveTransactionMonitor(channel: ReceiveChannel<Map<ActiveTransaction, TransactionState>>) = launch {
|
||||||
|
twig("active transaction monitor starting!")
|
||||||
|
for (i in channel) {
|
||||||
|
bind(i)
|
||||||
|
}
|
||||||
|
twig("active transaction monitor exiting!")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// View Callbacks on Main Thread
|
||||||
|
//
|
||||||
|
|
||||||
|
private fun bind(old: Long?, new: Long) {
|
||||||
|
twig("binding balance of $new")
|
||||||
|
view.updateBalance(old ?: 0L, new)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun bind(transactions: List<WalletTransaction>) {
|
||||||
|
twig("binding ${transactions.size} walletTransactions")
|
||||||
|
view.setTransactions(transactions.sortedByDescending {
|
||||||
|
if (!it.isMined && it.isSend) Long.MAX_VALUE else it.timeInSeconds
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bind(activeTransactionMap: Map<ActiveTransaction, TransactionState>) {
|
||||||
|
twig("binding a.t. map of size ${activeTransactionMap.size}")
|
||||||
|
if (activeTransactionMap.isNotEmpty()) view.setActiveTransactions(activeTransactionMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCancelActiveTransaction(transaction: ActiveSendTransaction) {
|
||||||
|
twig("requesting to cancel send for transaction ${transaction.internalId}")
|
||||||
|
val isTooLate = !synchronizer.cancelSend(transaction)
|
||||||
|
if (isTooLate) {
|
||||||
|
view.onCancelledTooLate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
abstract class HomePresenterModule {
|
||||||
|
@Binds
|
||||||
|
@FragmentScope
|
||||||
|
abstract fun providePresenter(homePresenter: HomePresenter): Presenter
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package cash.z.android.wallet.ui.presenter
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
interface Presenter {
|
||||||
|
suspend fun start()
|
||||||
|
fun stop()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A presenter collaborates with a scoped view. The presenter lives within that scope,
|
||||||
|
* meaning, when the view dies, the presenter dies, too. To achieve this, the presenter's
|
||||||
|
* [start] method should be launched from the view's scope via structured concurrency.
|
||||||
|
*/
|
||||||
|
interface PresenterView : CoroutineScope
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package cash.z.android.wallet.ui.presenter
|
||||||
|
|
||||||
|
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
|
||||||
|
import cash.z.wallet.sdk.data.Synchronizer
|
||||||
|
import cash.z.wallet.sdk.data.Twig
|
||||||
|
import cash.z.wallet.sdk.data.twig
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
class ProgressPresenter @Inject constructor(
|
||||||
|
private val view: ProgressView,
|
||||||
|
private var synchronizer: Synchronizer
|
||||||
|
) : Presenter {
|
||||||
|
|
||||||
|
private var job: Job? = null
|
||||||
|
|
||||||
|
interface ProgressView : PresenterView {
|
||||||
|
fun showProgress(progress: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// LifeCycle
|
||||||
|
//
|
||||||
|
|
||||||
|
override suspend fun start() {
|
||||||
|
job?.cancel()
|
||||||
|
job = Job()
|
||||||
|
Twig.sprout("ProgressPresenter")
|
||||||
|
twig("starting")
|
||||||
|
view.launchProgressMonitor(synchronizer.progress())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
Twig.clip("ProgressPresenter")
|
||||||
|
twig("stopping")
|
||||||
|
job?.cancel()?.also { job = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.launchProgressMonitor(channel: ReceiveChannel<Int>) = launch {
|
||||||
|
twig("progress monitor starting on thread ${Thread.currentThread().name}!")
|
||||||
|
for (i in channel) {
|
||||||
|
bind(i)
|
||||||
|
}
|
||||||
|
// "receive" and send 100, whenever the channel is closed for send
|
||||||
|
bind(100)
|
||||||
|
twig("progress monitor exiting!")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bind(progress: Int) = view.launch {
|
||||||
|
twig("binding progress of $progress on thread ${Thread.currentThread().name}!")
|
||||||
|
view.showProgress(progress)
|
||||||
|
twig("done binding progress of $progress on thread ${Thread.currentThread().name}!")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,379 @@
|
||||||
|
package cash.z.android.wallet.ui.presenter
|
||||||
|
|
||||||
|
import cash.z.android.wallet.R
|
||||||
|
import cash.z.android.wallet.di.annotation.FragmentScope
|
||||||
|
import cash.z.android.wallet.extention.toAppString
|
||||||
|
import cash.z.android.wallet.sample.SampleProperties
|
||||||
|
import cash.z.android.wallet.ui.fragment.SendFragment
|
||||||
|
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
|
||||||
|
import cash.z.wallet.sdk.data.Synchronizer
|
||||||
|
import cash.z.wallet.sdk.data.Twig
|
||||||
|
import cash.z.wallet.sdk.data.twig
|
||||||
|
import cash.z.wallet.sdk.ext.*
|
||||||
|
import cash.z.wallet.sdk.secure.Wallet
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class SendPresenter @Inject constructor(
|
||||||
|
private val view: SendFragment,
|
||||||
|
private val synchronizer: Synchronizer
|
||||||
|
) : Presenter {
|
||||||
|
|
||||||
|
interface SendView : PresenterView {
|
||||||
|
fun updateAvailableBalance(new: Long)
|
||||||
|
fun setHeaders(isUsdSelected: Boolean, headerString: String, subheaderString: String)
|
||||||
|
fun setHeaderValue(usdString: String)
|
||||||
|
fun setSubheaderValue(usdString: String, isUsdSelected: Boolean)
|
||||||
|
fun showSendDialog(zecString: String, usdString: String, toAddress: String, hasMemo: Boolean)
|
||||||
|
fun exit()
|
||||||
|
|
||||||
|
// error handling
|
||||||
|
fun setAmountError(message: String?)
|
||||||
|
fun setAddressError(message: String?)
|
||||||
|
fun setMemoError(message: String?)
|
||||||
|
fun setSendEnabled(isEnabled: Boolean)
|
||||||
|
fun checkAllInput(): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We require the user to send more than this amount. Right now, we just use the miner's fee as a minimum but other
|
||||||
|
* lower bounds may also be useful for validation.
|
||||||
|
*/
|
||||||
|
private val minersFee = 10_000L
|
||||||
|
private var balanceJob: Job? = null
|
||||||
|
private var requiresValidation = false
|
||||||
|
var sendUiModel = SendUiModel()
|
||||||
|
|
||||||
|
// TODO: find the best set of characters here. Possibly add something to the rust layer to help with this.
|
||||||
|
private val validMemoChars = " \t\n\r.?!,\"':;-_=+@#%*"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// LifeCycle
|
||||||
|
//
|
||||||
|
|
||||||
|
override suspend fun start() {
|
||||||
|
Twig.sprout("SendPresenter")
|
||||||
|
twig("sendPresenter starting!")
|
||||||
|
// set the currency to zec and update the view, initializing everything to zero
|
||||||
|
inputToggleCurrency()
|
||||||
|
balanceJob?.cancel()
|
||||||
|
balanceJob = Job()
|
||||||
|
balanceJob = view.launchBalanceBinder(synchronizer.balance())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
twig("sendPresenter stopping!")
|
||||||
|
Twig.clip("SendPresenter")
|
||||||
|
balanceJob?.cancel()?.also { balanceJob = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CoroutineScope.launchBalanceBinder(channel: ReceiveChannel<Wallet.WalletBalance>) = launch {
|
||||||
|
twig("send balance binder starting!")
|
||||||
|
for (new in channel) {
|
||||||
|
twig("send polled a balance item")
|
||||||
|
bind(new)
|
||||||
|
}
|
||||||
|
twig("send balance binder exiting!")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Public API
|
||||||
|
//
|
||||||
|
|
||||||
|
fun sendFunds() {
|
||||||
|
//TODO: prehaps grab the activity scope or let the sycnchronizer have scope and make that function not suspend
|
||||||
|
// also, we need to handle cancellations. So yeah, definitely do this differently
|
||||||
|
GlobalScope.launch {
|
||||||
|
twig("Process: cash.z.android.wallet. checking....")
|
||||||
|
twig("Process: cash.z.android.wallet. is it null??? $sendUiModel")
|
||||||
|
synchronizer.sendToAddress(sendUiModel.zatoshiValue!!, sendUiModel.toAddress)
|
||||||
|
}
|
||||||
|
view.exit()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// User Input
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user has tapped on the button for toggling currency, swapping zec for usd
|
||||||
|
*/
|
||||||
|
fun inputToggleCurrency() {
|
||||||
|
// tricky: this is not really a model update, instead it is a byproduct of using `isUsdSelected` for the
|
||||||
|
// currency instead of strong types. There are several todo's to fix that. if we update the model here then
|
||||||
|
// the UI will think the user took action and display errors prematurely.
|
||||||
|
sendUiModel = sendUiModel.copy(isUsdSelected = !sendUiModel.isUsdSelected)
|
||||||
|
with(sendUiModel) {
|
||||||
|
view.setHeaders(
|
||||||
|
isUsdSelected = isUsdSelected,
|
||||||
|
headerString = if (isUsdSelected) usdValue.toUsdString() else zatoshiValue.convertZatoshiToZecString(),
|
||||||
|
subheaderString = if (isUsdSelected) zatoshiValue.convertZatoshiToZecString() else usdValue.toUsdString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As the user is typing the header string, update the subheader string. Do not modify our own internal model yet.
|
||||||
|
* Internal model is only modified after [headerUpdated] is called (with valid data).
|
||||||
|
*/
|
||||||
|
fun inputHeaderUpdating(headerValue: String) {
|
||||||
|
headerValue.safelyConvertToBigDecimal()?.let { headerValueAsDecimal ->
|
||||||
|
val subheaderValue = headerValueAsDecimal.convertCurrency(SampleProperties.USD_PER_ZEC, sendUiModel.isUsdSelected)
|
||||||
|
|
||||||
|
// subheader string contains opposite currency of the selected one. so if usd is selected, format the subheader as zec
|
||||||
|
val subheaderString = if(sendUiModel.isUsdSelected) subheaderValue.toZecString() else subheaderValue.toUsdString()
|
||||||
|
|
||||||
|
view.setSubheaderValue(subheaderString, sendUiModel.isUsdSelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As the user updates the address, update the error that gets displayed in real-time
|
||||||
|
*
|
||||||
|
* @param addressValue the address that the user has typed, so far
|
||||||
|
*/
|
||||||
|
fun inputAddressUpdating(addressValue: String) {
|
||||||
|
validateAddress(addressValue, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As the user updates the memo, update the error that gets displayed in real-time
|
||||||
|
*
|
||||||
|
* @param memoValue the memo that the user has typed, so far
|
||||||
|
*/
|
||||||
|
fun inputMemoUpdating(memoValue: String) {
|
||||||
|
// treat the memo a little differently because it is more likely for the user to go back and edit invalid chars
|
||||||
|
// and we want the send button to be active the moment that happens
|
||||||
|
if(validateMemo(memoValue)) {
|
||||||
|
updateModel(sendUiModel.copy(memo = memoValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user has completed their update to the header value, typically on focus change.
|
||||||
|
*
|
||||||
|
* @return true when the given amount is parsable, positive and less than the available amount.
|
||||||
|
*/
|
||||||
|
fun inputHeaderUpdated(amountString: String): Boolean {
|
||||||
|
if (!validateAmount(amountString)) return false
|
||||||
|
|
||||||
|
// either USD or ZEC -- TODO: use strong typing (and polymorphism) instead of isUsdSelected checks
|
||||||
|
val amount = amountString.safelyConvertToBigDecimal()!! // we've already validated this as not null and it's immutable
|
||||||
|
with(sendUiModel) {
|
||||||
|
if (isUsdSelected) {
|
||||||
|
// amount represents USD
|
||||||
|
val headerString = amount.toUsdString()
|
||||||
|
val zatoshiValue = amount.convertUsdToZec(SampleProperties.USD_PER_ZEC).convertZecToZatoshi()
|
||||||
|
val subheaderString = amount.convertUsdToZec(SampleProperties.USD_PER_ZEC).toUsdString()
|
||||||
|
updateModel(sendUiModel.copy(zatoshiValue = zatoshiValue, usdValue = amount))
|
||||||
|
view.setHeaders(sendUiModel.isUsdSelected, headerString, subheaderString)
|
||||||
|
} else {
|
||||||
|
// amount represents ZEC
|
||||||
|
val headerString = amount.toZecString()
|
||||||
|
val usdValue = amount.convertZecToUsd(SampleProperties.USD_PER_ZEC)
|
||||||
|
val subheaderString = usdValue.toUsdString()
|
||||||
|
updateModel(sendUiModel.copy(zatoshiValue = amount.convertZecToZatoshi(), usdValue = usdValue))
|
||||||
|
twig("calling setHeaders with $headerString $subheaderString")
|
||||||
|
view.setHeaders(sendUiModel.isUsdSelected, headerString, subheaderString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user has updated the toAddress, typically on focus change.
|
||||||
|
*
|
||||||
|
* @return true when the given address' length and content are valid
|
||||||
|
*/
|
||||||
|
fun inputAddressUpdated(newAddress: String): Boolean {
|
||||||
|
if (!validateAddress(newAddress)) return false
|
||||||
|
updateModel(sendUiModel.copy(toAddress = newAddress))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user has updated the memo field, typically after pressing the 'done' key.
|
||||||
|
*
|
||||||
|
* @return true when the given memo's content does not contain invalid characters
|
||||||
|
*/
|
||||||
|
fun inputMemoUpdated(newMemo: String): Boolean {
|
||||||
|
if (!validateMemo(newMemo)) return false
|
||||||
|
updateModel(sendUiModel.copy(memo = newMemo))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after the user has pressed the send button and should be shown a confirmation dialog, next.
|
||||||
|
*
|
||||||
|
* @return true when all input fields contained valid data
|
||||||
|
*/
|
||||||
|
fun inputSendPressed(): Boolean {
|
||||||
|
// double sanity check. Make sure view and model agree and are each valid and if not, highlight the error.
|
||||||
|
if (!view.checkAllInput() || !validateAll()) return false
|
||||||
|
|
||||||
|
with(sendUiModel) {
|
||||||
|
view.showSendDialog(
|
||||||
|
zecString = zatoshiValue.convertZatoshiToZecString(),
|
||||||
|
usdString = usdValue.toUsdString(),
|
||||||
|
toAddress = toAddress,
|
||||||
|
hasMemo = !memo.isBlank()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(balanceInfo: Wallet.WalletBalance) {
|
||||||
|
val available = balanceInfo.available
|
||||||
|
if (available >= 0) {
|
||||||
|
twig("binding balance of $available")
|
||||||
|
view.updateAvailableBalance(available)
|
||||||
|
updateModel(sendUiModel.copy(availableBalance = available))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateModel(newModel: SendUiModel) {
|
||||||
|
sendUiModel = newModel.apply { hasBeenUpdated = true }
|
||||||
|
// now that we have new data, check and see if we can clear errors and re-enable the send button
|
||||||
|
if (requiresValidation) validateAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Validation
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after any user interaction. This is a potential time that errors should be shown, but only if data has
|
||||||
|
* already been entered. The view should call this method on focus change.
|
||||||
|
*/
|
||||||
|
fun invalidate() {
|
||||||
|
requiresValidation = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the given memo, ensuring that it does not contain unsupported characters. For now, we're very
|
||||||
|
* restrictive until we define more clear requirements for the values that can safely be entered in this field
|
||||||
|
* without introducing security risks.
|
||||||
|
*
|
||||||
|
* @param memo the memo to consider for validation
|
||||||
|
*
|
||||||
|
* @return true when the memo contains valid characters, which includes being blank
|
||||||
|
*/
|
||||||
|
private fun validateMemo(memo: String): Boolean {
|
||||||
|
return if (memo.all { it.isLetterOrDigit() || it in validMemoChars }) {
|
||||||
|
view.setMemoError(null)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
view.setMemoError("Only letters and numbers are allowed in memo at this time")
|
||||||
|
requiresValidation = true
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the given address
|
||||||
|
*
|
||||||
|
* @param toAddress the address to consider for validation
|
||||||
|
* @param ignoreLength whether to ignore the length while validating, this is helpful when the user is still
|
||||||
|
* actively typing the address
|
||||||
|
*/
|
||||||
|
private fun validateAddress(toAddress: String, ignoreLength: Boolean = false): Boolean {
|
||||||
|
// TODO: later expose a method in the synchronizer for validating addresses.
|
||||||
|
// Right now it's not available so we do it ourselves
|
||||||
|
return if (!ignoreLength && sendUiModel.hasBeenUpdated && toAddress.length < 20) {// arbitrary length for now
|
||||||
|
view.setAddressError(R.string.send_error_address_too_short.toAppString())
|
||||||
|
requiresValidation = true
|
||||||
|
false
|
||||||
|
} else if (!toAddress.startsWith("zt") && !toAddress.startsWith("zs")) {
|
||||||
|
view.setAddressError(R.string.send_error_address_invalid_contents.toAppString())
|
||||||
|
requiresValidation = true
|
||||||
|
false
|
||||||
|
} else if (toAddress.any { !it.isLetterOrDigit() }) {
|
||||||
|
view.setAddressError(R.string.send_error_address_invalid_char.toAppString())
|
||||||
|
requiresValidation = true
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
view.setAddressError(null)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the given amount, calling the related `showError` methods on the view, when appropriate
|
||||||
|
*
|
||||||
|
* @param amount the amount to consider for validation, for now this can be either USD or ZEC. In the future we will
|
||||||
|
* will separate those into types.
|
||||||
|
*
|
||||||
|
* @return true when the given amount is valid and all errors have been cleared on the view
|
||||||
|
*/
|
||||||
|
private fun validateAmount(amountString: String): Boolean {
|
||||||
|
if (!sendUiModel.hasBeenUpdated) return true // don't mark zero as bad until the model has been updated
|
||||||
|
|
||||||
|
var amount = amountString.safelyConvertToBigDecimal()
|
||||||
|
// no need to convert when we know it's null
|
||||||
|
return if (amount == null ) {
|
||||||
|
validateZatoshiAmount(null)
|
||||||
|
} else {
|
||||||
|
val zecAmount =
|
||||||
|
if (sendUiModel.isUsdSelected) amount.convertUsdToZec(SampleProperties.USD_PER_ZEC) else amount
|
||||||
|
validateZatoshiAmount(zecAmount.convertZecToZatoshi())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateZatoshiAmount(zatoshiValue: Long?): Boolean {
|
||||||
|
return if (zatoshiValue == null || zatoshiValue <= minersFee) {
|
||||||
|
view.setAmountError("Please specify a larger amount")
|
||||||
|
requiresValidation = true
|
||||||
|
false
|
||||||
|
} else if (sendUiModel.availableBalance != null
|
||||||
|
&& zatoshiValue >= sendUiModel.availableBalance!!) {
|
||||||
|
view.setAmountError("Exceeds available balance of " +
|
||||||
|
"${sendUiModel.availableBalance.convertZatoshiToZecString(3)}")
|
||||||
|
requiresValidation = true
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
view.setAmountError(null)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateAll(): Boolean {
|
||||||
|
with(sendUiModel) {
|
||||||
|
val isValid = validateZatoshiAmount(zatoshiValue)
|
||||||
|
&& validateAddress(toAddress)
|
||||||
|
&& validateMemo(memo)
|
||||||
|
requiresValidation = !isValid
|
||||||
|
view.setSendEnabled(isValid)
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class SendUiModel(
|
||||||
|
val availableBalance: Long? = null,
|
||||||
|
var hasBeenUpdated: Boolean = false,
|
||||||
|
val isUsdSelected: Boolean = true,
|
||||||
|
val zatoshiValue: Long? = null,
|
||||||
|
val usdValue: BigDecimal = BigDecimal.ZERO,
|
||||||
|
val toAddress: String = "",
|
||||||
|
val memo: String = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Module
|
||||||
|
abstract class SendPresenterModule {
|
||||||
|
@Binds
|
||||||
|
@FragmentScope
|
||||||
|
abstract fun providePresenter(sendPresenter: SendPresenter): Presenter
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package cash.z.android.wallet.ui.util
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import cash.z.android.wallet.R
|
||||||
|
|
||||||
|
class AlternatingRowColorDecoration(@DrawableRes val evenBackground: Int = R.color.zcashBlueGray, @DrawableRes val oddBackground: Int = R.color.zcashWhite) :
|
||||||
|
RecyclerView.ItemDecoration() {
|
||||||
|
|
||||||
|
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
super.getItemOffsets(outRect, view, parent, state)
|
||||||
|
val adapterPosition = parent.getChildAdapterPosition(view)
|
||||||
|
val rowBackground = if (adapterPosition.rem(2) == 0) evenBackground else oddBackground
|
||||||
|
view.setBackgroundResource(rowBackground)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package cash.z.android.wallet.ui.util
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
|
||||||
|
class AnimatorCompleteListener(val block: (Animator) -> Unit) : Animator.AnimatorListener {
|
||||||
|
override fun onAnimationRepeat(animation: Animator?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
block(animation)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationStart(animation: Animator?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(animation: Animator?) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package cash.z.android.wallet.ui.util
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import cash.z.android.wallet.extention.Toaster
|
||||||
|
import com.airbnb.lottie.LottieAnimationView
|
||||||
|
import com.airbnb.lottie.LottieDrawable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to help with looping a lottie animation over a particular range. It will start the animation and play it up
|
||||||
|
* to the end of the range and then set it to loop over the range and, once stopped, it will proceed from the current
|
||||||
|
* frame to the end of the animation. Visually: BEGIN...LOOP...LOOP...LOOP...END
|
||||||
|
*/
|
||||||
|
class LottieLooper(private val lottie: LottieAnimationView, private val loopRange: IntRange, private val lastFrame: Int = Int.MAX_VALUE) :
|
||||||
|
Animator.AnimatorListener {
|
||||||
|
|
||||||
|
var isPlaying = false
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
if (isPlaying) return
|
||||||
|
with(lottie) {
|
||||||
|
setMinAndMaxFrame(1, loopRange.last)
|
||||||
|
progress = 0f
|
||||||
|
repeatCount = 0
|
||||||
|
addAnimatorListener(this@LottieLooper)
|
||||||
|
lottie.post { playAnimation() }
|
||||||
|
}
|
||||||
|
isPlaying = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
with(lottie) {
|
||||||
|
setMinAndMaxFrame(lottie.frame, lastFrame)
|
||||||
|
repeatCount = 0
|
||||||
|
// we don't want to just cancel the animation. We want it to finish it's final frames but the moment it is
|
||||||
|
// done, we need it to freeze on that final frame and then die
|
||||||
|
addAnimatorListener(LottieAssassin())
|
||||||
|
}
|
||||||
|
isPlaying = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationRepeat(animation: Animator?) {
|
||||||
|
onAnimationEnd(animation)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
|
with(lottie) {
|
||||||
|
removeAllAnimatorListeners()
|
||||||
|
setMinAndMaxFrame(loopRange.first, loopRange.last)
|
||||||
|
repeatCount = LottieDrawable.INFINITE
|
||||||
|
playAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(animation: Animator?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationStart(animation: Animator?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** I have one job: kill lottie */
|
||||||
|
inner class LottieAssassin : Animator.AnimatorListener {
|
||||||
|
override fun onAnimationRepeat(animation: Animator?) {
|
||||||
|
finishingMove()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
|
finishingMove()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(animation: Animator?) {
|
||||||
|
finishingMove()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationStart(animation: Animator?) {
|
||||||
|
finishingMove()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Agressively force it to freeze on the lastframe */
|
||||||
|
private fun finishingMove() {
|
||||||
|
lottie.pauseAnimation()
|
||||||
|
lottie.setMinAndMaxFrame(lastFrame, lastFrame)
|
||||||
|
lottie.progress = 1.0f
|
||||||
|
// wait around a bit to see if my listeners detect any movement, then quietly make my getaway
|
||||||
|
lottie.postDelayed({
|
||||||
|
lottie.removeAllAnimatorListeners()
|
||||||
|
// lottie.removeAnimatorListener(this)
|
||||||
|
lottie.setMinAndMaxFrame(1, lastFrame)
|
||||||
|
}, 500L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cash.z.android.wallet.ui.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
|
import androidx.viewpager.widget.ViewPager
|
||||||
|
|
||||||
|
class ViewpagerHeader @JvmOverloads constructor(
|
||||||
|
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||||
|
) : MotionLayout(context, attrs, defStyleAttr), ViewPager.OnPageChangeListener {
|
||||||
|
|
||||||
|
override fun onPageScrollStateChanged(state: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
||||||
|
val numPages = 3
|
||||||
|
progress = (position + positionOffset) / (numPages - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageSelected(position: Int) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package cash.z.android.wallet.ui.view
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.util.Log
|
||||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
|
||||||
|
@ -11,6 +12,7 @@ class CollapsingMotionToolbar @JvmOverloads constructor(
|
||||||
|
|
||||||
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
|
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
|
||||||
progress = -verticalOffset / appBarLayout.totalScrollRange.toFloat()
|
progress = -verticalOffset / appBarLayout.totalScrollRange.toFloat()
|
||||||
|
Log.e("MotionL", "progress: $progress verticalOffset: $verticalOffset scrollRange: ${appBarLayout.totalScrollRange.toFloat()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
package cash.z.android.wallet.vo
|
|
||||||
|
|
||||||
import cash.z.android.wallet.R
|
|
||||||
import androidx.annotation.ColorRes
|
|
||||||
import java.math.BigDecimal
|
|
||||||
|
|
||||||
data class WalletTransaction(val status: WalletTransactionStatus, val timestamp: Long, val amount: BigDecimal)
|
|
||||||
|
|
||||||
enum class WalletTransactionStatus(@ColorRes val color: Int) {
|
|
||||||
SENT(R.color.colorPrimary),
|
|
||||||
RECEIVED(R.color.colorAccent);
|
|
||||||
}
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item android:state_activated="true">
|
||||||
|
|
||||||
|
<set android:ordering="together">
|
||||||
|
|
||||||
|
<objectAnimator
|
||||||
|
android:propertyName="backgroundColor"
|
||||||
|
android:valueFrom="@color/fragment_home_background"
|
||||||
|
android:valueTo="@color/zcashWhite"
|
||||||
|
android:duration="@android:integer/config_shortAnimTime"
|
||||||
|
android:valueType="colorType" />
|
||||||
|
|
||||||
|
<objectAnimator
|
||||||
|
android:propertyName="translationZ"
|
||||||
|
android:valueTo="4dp"
|
||||||
|
android:startOffset="300"
|
||||||
|
android:duration="@android:integer/config_shortAnimTime"
|
||||||
|
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||||
|
|
||||||
|
</set>
|
||||||
|
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
|
||||||
|
<set android:ordering="together">
|
||||||
|
|
||||||
|
<objectAnimator
|
||||||
|
android:propertyName="backgroundColor"
|
||||||
|
android:valueFrom="@color/zcashWhite"
|
||||||
|
android:valueTo="@color/fragment_home_background"
|
||||||
|
android:duration="@android:integer/config_shortAnimTime"
|
||||||
|
android:valueType="colorType" />
|
||||||
|
|
||||||
|
<objectAnimator
|
||||||
|
android:propertyName="translationZ"
|
||||||
|
android:valueTo="0dp"
|
||||||
|
android:duration="@android:integer/config_shortAnimTime"
|
||||||
|
android:interpolator="@android:interpolator/fast_out_slow_in" />
|
||||||
|
|
||||||
|
</set>
|
||||||
|
|
||||||
|
</item>
|
||||||
|
|
||||||
|
</selector>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item android:state_enabled="false" android:color="@color/text_dark_dimmed"/>
|
||||||
|
<item android:state_enabled="true" android:color="@color/zcashRed" />
|
||||||
|
</selector>
|
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 65 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 102 KiB |
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<corners android:radius="6dp" />
|
||||||
|
<solid android:color="@color/zcashWhite_87" />
|
||||||
|
</shape>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
<solid android:color="@color/zcashWhite" />
|
||||||
|
</shape>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<corners android:radius="2dp" />
|
||||||
|
<solid android:color="@color/zcashWhite" />
|
||||||
|
</shape>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_enabled="false" android:drawable="@color/zcashGray"/>
|
||||||
|
<item android:state_enabled="true" android:drawable="@color/colorPrimary" />
|
||||||
|
</selector>
|
After Width: | Height: | Size: 865 B |
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M19,2h-4.18C14.4,0.84 13.3,0 12,0c-1.3,0 -2.4,0.84 -2.82,2L5,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,4c0,-1.1 -0.9,-2 -2,-2zM12,2c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM19,20L5,20L5,4h2v3h10L17,4h2v16z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
|
||||||
|
</vector>
|
|
@ -4,6 +4,6 @@
|
||||||
android:viewportWidth="24.0"
|
android:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF000000"
|
android:fillColor="@color/zcashBlue"
|
||||||
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
|
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
|
||||||
</vector>
|
</vector>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<vector android:height="48dp" android:viewportHeight="24.0"
|
||||||
|
android:viewportWidth="24.0" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,37 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="160dp"
|
||||||
|
android:height="160dp"
|
||||||
|
android:viewportWidth="160"
|
||||||
|
android:viewportHeight="160">
|
||||||
|
<group
|
||||||
|
android:name="_R_G_L_1_G"
|
||||||
|
android:rotation="-20"
|
||||||
|
android:scaleX="1"
|
||||||
|
android:scaleY="1"
|
||||||
|
android:translateX="80"
|
||||||
|
android:translateY="80">
|
||||||
|
<path
|
||||||
|
android:name="_R_G_L_1_G_D_0_P_0"
|
||||||
|
android:fillAlpha="1"
|
||||||
|
android:fillColor="#009688"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData=" M66 0 C66,36.45 36.45,66 0,66 C-36.45,66 -66,36.45 -66,0 C-66,-36.45 -36.45,-66 0,-66 C36.45,-66 66,-36.45 66,0c " />
|
||||||
|
</group>
|
||||||
|
<group
|
||||||
|
android:name="_R_G_L_0_G_T_1"
|
||||||
|
android:rotation="180"
|
||||||
|
android:translateX="80"
|
||||||
|
android:translateY="80">
|
||||||
|
<group
|
||||||
|
android:name="_R_G_L_0_G"
|
||||||
|
android:translateY="-3">
|
||||||
|
<path
|
||||||
|
android:name="_R_G_L_0_G_D_0_P_0"
|
||||||
|
android:fillAlpha="1"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData=" M-17.21 17.58 C-17.21,17.58 -25.85,9.26 -25.85,9.26 C-25.85,9.26 0,-17.58 0,-17.58 C0,-17.58 25.85,9.26 25.85,9.26 C25.85,9.26 17.21,17.58 17.21,17.58 C17.21,17.58 0,-0.28 0,-0.28 C0,-0.28 -17.21,17.58 -17.21,17.58c " />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="160dp"
|
||||||
|
android:height="160dp"
|
||||||
|
android:viewportWidth="160"
|
||||||
|
android:viewportHeight="160">
|
||||||
|
<group
|
||||||
|
android:name="_R_G_L_3_G"
|
||||||
|
android:translateX="80"
|
||||||
|
android:translateY="80">
|
||||||
|
<path
|
||||||
|
android:name="_R_G_L_3_G_D_0_P_0"
|
||||||
|
android:fillAlpha="1"
|
||||||
|
android:fillColor="#f5a623"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData=" M55.59 0 C55.59,30.7 30.7,55.59 0,55.59 C-30.7,55.59 -55.58,30.7 -55.58,0 C-55.58,-30.7 -30.7,-55.58 0,-55.58 C30.7,-55.58 55.59,-30.7 55.59,0c " />
|
||||||
|
</group>
|
||||||
|
<group
|
||||||
|
android:name="_R_G_L_2_G"
|
||||||
|
android:translateX="76"
|
||||||
|
android:translateY="84">
|
||||||
|
<path
|
||||||
|
android:name="_R_G_L_2_G_D_0_P_0"
|
||||||
|
android:fillAlpha="1"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData=" M10.76 -10.09 C10.76,-10.09 -4.17,8.75 -4.17,8.75 C-4.17,8.75 5.67,25.78 5.67,25.78 C7.07,27.55 9.88,27.06 10.6,24.92 C10.6,24.92 26.8,-23.19 26.8,-23.19 C27.55,-25.42 25.42,-27.55 23.19,-26.8 C23.19,-26.8 -24.92,-10.6 -24.92,-10.6 C-27.06,-9.88 -27.55,-7.07 -25.77,-5.67 C-25.77,-5.67 -8.75,4.18 -8.75,4.18 C-8.75,4.18 10.09,-10.76 10.09,-10.76 C10.54,-11.12 11.12,-10.54 10.76,-10.09c " />
|
||||||
|
</group>
|
||||||
|
|
||||||
|
</vector>
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:tint="#FFFFFF"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z" />
|
||||||
|
</vector>
|
|
@ -0,0 +1,64 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="50dp"
|
||||||
|
android:height="50dp"
|
||||||
|
android:viewportWidth="50"
|
||||||
|
android:viewportHeight="50">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M25,25m-22.75,0a22.75,22.75 0,1 1,45.5 0a22.75,22.75 0,1 1,-45.5 0"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="0.5"
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="evenOdd">
|
||||||
|
<aapt:attr name="android:strokeColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="2.25"
|
||||||
|
android:startX="25"
|
||||||
|
android:endY="47.304756"
|
||||||
|
android:endX="25"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#00000000"/>
|
||||||
|
<item android:offset="0.8" android:color="#05000000"/>
|
||||||
|
<item android:offset="1" android:color="#0A000000"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M25,25m-22.75,0a22.75,22.75 0,1 1,45.5 0a22.75,22.75 0,1 1,-45.5 0"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="0.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd">
|
||||||
|
<aapt:attr name="android:strokeColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="2.25"
|
||||||
|
android:startX="25"
|
||||||
|
android:endY="47.75"
|
||||||
|
android:endX="25"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#1EFFFFFF"/>
|
||||||
|
<item android:offset="0.2" android:color="#0FFFFFFF"/>
|
||||||
|
<item android:offset="1" android:color="#00FFFFFF"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M25.0005,2C12.3176,2 2,12.3176 2,24.9995C2,37.6814 12.3176,48 25.0005,48C37.6834,48 48,37.6824 48,24.9995C48,12.3166 37.6824,2 25.0005,2ZM25.0005,44.203C14.4115,44.203 5.797,35.5885 5.797,24.9995C5.797,14.4105 14.4115,5.797 25.0005,5.797C35.5895,5.797 44.203,14.4115 44.203,24.9995C44.203,35.5875 35.5885,44.203 25.0005,44.203Z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:fillColor="#231F20"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M24.9747,24.9747m-18.0462,0a18.0462,18.0462 0,1 1,36.0923 0a18.0462,18.0462 0,1 1,-36.0923 0"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:fillColor="#F4B728"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M33.0626,17.5611l0,-3.5028l-6.2269,0l0,-3.8441l-3.8242,0l0,3.8441l-6.2259,0l0,4.6398l9.6521,0l-7.8934,10.9311l-1.7587,2.2496l0,3.5028l6.2259,0l0,3.8318l0.4587,0l0,0.0163l2.9069,0l0,-0.0163l0.4587,0l0,-3.8318l6.2269,0l0,-4.6398l-9.6532,0l7.8934,-10.9311z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:fillColor="#231F20"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
</vector>
|
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 4.4 KiB |