zcash-android-wallet-zcon1/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/activity/MainActivity.kt

469 lines
15 KiB
Kotlin

package cash.z.android.wallet.ui.activity
import android.animation.Animator
import android.media.MediaPlayer
import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.view.inputmethod.InputMethodManager.HIDE_NOT_ALWAYS
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.Toolbar
import androidx.core.content.getSystemService
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import androidx.navigation.Navigation
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import cash.z.android.wallet.*
import cash.z.android.wallet.data.DataSyncronizer
import cash.z.android.wallet.data.StableSynchronizer
import cash.z.android.wallet.databinding.ActivityMainBinding
import cash.z.android.wallet.di.annotation.ActivityScope
import cash.z.android.wallet.extention.Toaster
import cash.z.android.wallet.extention.alert
import cash.z.android.wallet.extention.copyToClipboard
import cash.z.android.wallet.ui.fragment.ScanFragment
import cash.z.android.wallet.ui.presenter.BalancePresenter
import cash.z.android.wallet.ui.presenter.MainPresenter
import cash.z.android.wallet.ui.presenter.MainPresenterModule
import cash.z.android.wallet.ui.util.Analytics
import cash.z.android.wallet.ui.util.Analytics.PokerChipFunnel.StartSweep
import cash.z.android.wallet.ui.util.Analytics.PokerChipFunnel.Swept
import cash.z.android.wallet.ui.util.Analytics.Tap.*
import cash.z.android.wallet.ui.util.Analytics.trackAction
import cash.z.android.wallet.ui.util.Analytics.trackFunnelStep
import cash.z.android.wallet.ui.util.Broom
import cash.z.wallet.sdk.data.twig
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.random.Random
class MainActivity : BaseActivity(), Animator.AnimatorListener, ScanFragment.BarcodeCallback, MainPresenter.MainView {
@Inject
lateinit var synchronizer: DataSyncronizer
@Inject
lateinit var mainPresenter: MainPresenter
@Inject
lateinit var broom: Broom
@Inject
lateinit var chipBucket: ChipBucket
lateinit var binding: ActivityMainBinding
lateinit var loadMessages: List<String>
private lateinit var appBarConfiguration: AppBarConfiguration
lateinit var navController: NavController
private val mediaPlayer: MediaPlayer = MediaPlayer()
lateinit var balancePresenter: BalancePresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.lifecycle
chipBucket.restore()
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.activity = this
initAppBar()
loadMessages = generateFunLoadMessages().shuffled()
balancePresenter = BalancePresenter()
}
override fun onAttachFragment(childFragment: Fragment) {
super.onAttachFragment(childFragment)
(childFragment as? ScanFragment)?.barcodeCallback = this
}
override fun onResume() {
super.onResume()
chipBucket.restore()
launch {
synchronizer.start(this)
balancePresenter.start(this, synchronizer.balances())
mainPresenter.start()
}
}
override fun onPause() {
super.onPause()
balancePresenter.stop()
mainPresenter.stop()
chipBucket.save()
}
override fun onDestroy() {
super.onDestroy()
Analytics.clear()
}
private fun initAppBar() {
val toolbar = findViewById<Toolbar>(R.id.main_toolbar)
setSupportActionBar(toolbar)
toolbar.navigationIcon = null
setupNavigation()
// show content behind the status bar
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
/**
* Let the navController override the default behavior when the drawer icon or back arrow are clicked. This
* automatically takes care of the drawer toggle behavior. Note that without overriding this method, the up/drawer
* buttons will not function.
*/
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
fun setToolbarShown(isShown: Boolean) {
binding.mainAppBar.visibility = if (isShown) View.VISIBLE else View.INVISIBLE
}
fun setNavigationShown(isShown: Boolean) {
binding.groupNavigation.visibility = if (isShown) View.VISIBLE else View.INVISIBLE
binding.groupNavigation.requestLayout()
}
fun setupNavigation() {
// create and setup the navController and appbarConfiguration
navController = Navigation.findNavController(this, R.id.nav_host_fragment)
navController.addOnDestinationChangedListener { _, _, _ ->
// hide the keyboard anytime we change destinations
getSystemService<InputMethodManager>()?.hideSoftInputFromWindow(binding.root.windowToken, HIDE_NOT_ALWAYS)
}
binding.lottieNavigationZcon1.apply {
speed = 2.0f
// alpha = 0.85f
}
}
// TODO: move all this nav stuff to a separate component
var navSelection = -1
var start = -1
var end = -1
val frameSize = 24
fun onNavButtonSelected2(index: Int) {
if (navSelection == index) return
val previousEnd = (navSelection * frameSize + 6) + frameSize / 2
navSelection = index.rem(4)
start = navSelection * frameSize + 6
end = start + frameSize / 2 - 6
if(previousEnd > start) onAnimationEnd(null)
else {
binding.lottieNavigationZcon1.apply {
addAnimatorListener(this@MainActivity)
setMinAndMaxFrame(previousEnd, start)
playAnimation()
}
}
val navOptions = NavOptions.Builder()
.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.build()
navController.navigate(when(navSelection) {
0 -> R.id.nav_zcon1_home_fragment
1 -> R.id.nav_send_fragment
2 -> R.id.nav_receive_fragment
3 -> R.id.nav_zcon1_cart_fragment
else -> R.id.nav_zcon1_home_fragment
}, null, navOptions)
}
val enterRanges = arrayOf<IntRange>(
0..13,
24..37,
48..61,
72..85
)
val exitRanges = arrayOf<IntRange>(
14..23,
38..47,
62..71,
86..95
)
fun onNavButtonSelected(index: Int) {
if (navSelection == index || index < 0 || index > 3) return
val previousSelection = navSelection
navSelection = index
if (previousSelection == -1) {
onAnimationEnd(null)
} else {
// play nav section exit animation
binding.lottieNavigationZcon1.apply {
addAnimatorListener(this@MainActivity)
setMinAndMaxFrame(exitRanges[previousSelection].first, exitRanges[previousSelection].last)
playAnimation()
}
}
val navOptions = NavOptions.Builder()
.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.build()
navController.navigate(when(navSelection) {
0 -> R.id.nav_zcon1_home_fragment
1 -> R.id.nav_send_fragment
2 -> R.id.nav_receive_fragment
3 -> R.id.nav_zcon1_cart_fragment
else -> R.id.nav_zcon1_home_fragment
}, null, navOptions)
}
override fun onAnimationRepeat(animation: Animator?) {
}
override fun onAnimationEnd(animation: Animator?) {
// play nav section enter animation
binding.lottieNavigationZcon1.apply {
removeAnimatorListener(this@MainActivity)
setMinAndMaxFrame(enterRanges[navSelection].first, enterRanges[navSelection].last)
playAnimation()
}
}
fun onAnimationEnd2(animation: Animator?) {
binding.lottieNavigationZcon1.apply {
removeAnimatorListener(this@MainActivity)
setMinAndMaxFrame(start, end)
playAnimation()
}
}
override fun onAnimationCancel(animation: Animator?) {
}
override fun onAnimationStart(animation: Animator?) {
}
fun nextLoadMessage(index: Int = -1): String {
return if (index < 0) loadMessages.random() else loadMessages[index]
}
companion object {
init {
// Enable vector drawable magic
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"
)
}
}
fun onScanQr(view: View) {
trackAction(TAPPED_SCAN_QR_HOME)
supportFragmentManager.beginTransaction()
.add(R.id.camera_placeholder, ScanFragment(), "camera_fragment")
.addToBackStack("camera_fragment_scanning")
.commit()
}
fun onSendFeedback(view: View) {
trackAction(TAPPED_GIVE_FEEDBACK)
navController.navigate(R.id.nav_feedback_fragment)
}
override fun onBarcodeScanned(value: String) {
exitScanMode()
// For now, empty happens when back is pressed
if (value.isEmpty()) return
chipBucket.findChip(value)?.let {existingChip ->
if (existingChip.isRedeemed()) {
alert("Previously Redeemed!", "We scanned this one already and the funds went to this wallet.")
} else {
alert(
title = "Still Processing",
message = "We scanned this one already and it is still processing. Would you rather wait until it finishes or abort and try again later?",
positiveButtonResId = R.string.wait,
negativeButtonResId = R.string.abort,
negativeAction = {
onAbortChip(existingChip)
}
)
}
return
}
alert(
title = "You found a token!",
message = "Would you like to magically convert this poker chip into digital money?"
) {
playSound(Random.nextBoolean())
funQuote()
launch {
val chip = PokerChip(value)
chipBucket.add(chip)
val result = sweepChip(chip)
twig("Sweep result? $result")
trackFunnelStep(Swept(chip, result))
}
}
}
private fun onAbortChip(chip: PokerChip) {
// TODO: don't remove until we're sure we can because this triggers a funnel event
chipBucket.remove(chip)
}
private suspend fun sweepChip(chip: PokerChip): String? {
trackFunnelStep(StartSweep(chip))
val provider = PokerChipSeedProvider(chip)
return broom.sweep(provider, chip.zatoshiValue)
}
val scanQuote = arrayOf(
"You're the Scrooge McDuck of Zcon1!",
"We're rich!",
"Show me the money! Oh wait, you just did. Literally.",
"Doing magic. Actual magic.",
"This is TAZmania!"
)
private fun funQuote() {
var message = scanQuote.random()
// if(message == scanQuote[0]) message = scanQuote.random() // simple way to make 0 more rare
Toaster.short(message)
}
override fun isTargetBarcode(value: String?): Boolean {
if(value == null) return false
return value.startsWith("r-") || value.startsWith("b-")
}
private fun playSound(isLarge: Boolean) {
mediaPlayer.apply {
if (isPlaying) stop()
try {
reset()
val fileName = if (isLarge) "sound_receive_large.mp3" else "sound_receive_small.mp3"
assets.openFd(fileName).let { afd ->
setDataSource(afd.fileDescriptor, afd.startOffset, afd.length)
}
prepare()
start()
} catch (t: Throwable) {
twig("ERROR: unable to play sound due to $t")
}
}
}
private fun exitScanMode() {
with(supportFragmentManager) {
findFragmentByTag("camera_fragment")?.let { cameraFragment ->
beginTransaction().remove(cameraFragment).commit()
}
}
}
fun buyProduct(product: Zcon1Store.CartItem) {
alert(
message = "Are you sure you'd like to buy a ${product.name} for ${product.zatoshiValue.convertZatoshiToZecString(1)} TAZ?",
positiveButtonResId = R.string.ok_allcaps,
negativeButtonResId = R.string.cancel,
positiveAction = { sendPurchaseOrder(product) }
)
}
private fun sendPurchaseOrder(item: Zcon1Store.CartItem) {
launch {
synchronizer.sendToAddress(item.zatoshiValue, item.toAddress, item.memo)
}
}
override fun orderFailed(error: MainPresenter.PurchaseResult.Failure) {
alert(
title = "Purchase Failed",
message = "${error.reason}"
)
}
override fun orderUpdated(processing: MainPresenter.PurchaseResult.Processing) {
Toaster.short(processing.pendingTransaction.toString())
}
fun onSynchronizerError(error: Throwable?): Boolean {
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 = { alert("Synchronization error:\n\n$error") }
)
return false
}
//
// Events from Layout Files
//
fun onFeedbackSubmit(view: View) {
trackAction(TAPPED_SUBMIT_FEEDBACK)
Toaster.short("Feedback Submitted! (j/k)")
navController.navigateUp()
}
fun onFeedbackCancel(view: View) {
Toaster.short("Feedback cancelled")
trackAction(TAPPED_CANCEL_FEEDBACK)
navController.navigateUp()
}
fun copyAddress(view: View) {
trackAction(TAPPED_COPY_ADDRESS)
Toaster.short("Address copied!")
launch {
copyToClipboard(synchronizer.getAddress())
}
}
}
@Module
abstract class MainActivityModule {
@ActivityScope
@ContributesAndroidInjector(modules = [MainPresenterModule::class])
abstract fun contributeMainActivity(): MainActivity
}