2019-11-26 12:29:16 -08:00
package cash.z.ecc.android.ui
2020-01-09 23:53:16 -08:00
import android.Manifest
2020-02-12 04:58:41 -08:00
import android.app.Dialog
2019-11-26 12:29:16 -08:00
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
2020-01-09 23:53:16 -08:00
import android.content.pm.PackageManager
2019-11-26 12:29:16 -08:00
import android.graphics.Color
2019-11-27 06:24:00 -08:00
import android.media.MediaPlayer
2020-01-09 23:53:16 -08:00
import android.os.Build
2019-11-26 12:29:16 -08:00
import android.os.Bundle
2019-11-27 06:24:00 -08:00
import android.os.Vibrator
import android.util.Log
2019-11-26 12:29:16 -08:00
import android.view.View
2019-12-17 13:34:42 -08:00
import android.view.ViewGroup
2019-11-26 12:29:16 -08:00
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
2020-01-09 08:00:20 -08:00
import androidx.activity.OnBackPressedCallback
2020-02-12 04:58:41 -08:00
import androidx.annotation.IdRes
2020-01-05 21:01:06 -08:00
import androidx.appcompat.app.AppCompatActivity
2020-01-09 23:53:16 -08:00
import androidx.core.content.ContextCompat
2019-11-26 12:29:16 -08:00
import androidx.core.content.getSystemService
2020-01-09 08:00:20 -08:00
import androidx.fragment.app.Fragment
2019-12-14 11:39:19 -08:00
import androidx.lifecycle.lifecycleScope
2019-11-26 12:29:16 -08:00
import androidx.navigation.NavController
import androidx.navigation.findNavController
import cash.z.ecc.android.R
2019-12-14 11:39:19 -08:00
import cash.z.ecc.android.ZcashWalletApp
2020-01-05 21:01:06 -08:00
import cash.z.ecc.android.di.component.MainActivitySubcomponent
2020-01-06 22:45:24 -08:00
import cash.z.ecc.android.di.component.SynchronizerSubcomponent
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.ecc.android.feedback.LaunchMetric
2020-02-21 15:49:16 -08:00
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Error.NonFatal.Reorg
2019-12-23 11:12:34 -08:00
import cash.z.ecc.android.feedback.Report.NonUserAction.FEEDBACK_STOPPED
import cash.z.ecc.android.feedback.Report.NonUserAction.SYNC_START
2020-02-21 15:49:16 -08:00
import cash.z.ecc.android.feedback.Report.Tap.COPY_ADDRESS
2020-06-10 04:49:38 -07:00
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.twig
2020-02-12 04:58:41 -08:00
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2019-12-17 13:34:42 -08:00
import com.google.android.material.snackbar.Snackbar
2019-12-14 11:39:19 -08:00
import kotlinx.coroutines.launch
import javax.inject.Inject
2019-11-26 12:29:16 -08:00
2020-01-05 21:01:06 -08:00
class MainActivity : AppCompatActivity ( ) {
2019-12-14 11:39:19 -08:00
@Inject
lateinit var feedback : Feedback
2019-12-17 13:34:42 -08:00
@Inject
lateinit var feedbackCoordinator : FeedbackCoordinator
2019-12-23 11:16:00 -08:00
@Inject
2020-01-05 21:01:06 -08:00
lateinit var clipboard : ClipboardManager
2019-12-23 11:16:00 -08:00
2019-11-27 06:24:00 -08:00
private val mediaPlayer : MediaPlayer = MediaPlayer ( )
2019-12-17 13:34:42 -08:00
private var snackbar : Snackbar ? = null
2020-02-12 04:58:41 -08:00
private var dialog : Dialog ? = null
2020-02-21 15:49:16 -08:00
private var ignoreScanFailure : Boolean = false
2019-12-17 13:34:42 -08:00
2020-01-05 21:01:06 -08:00
lateinit var component : MainActivitySubcomponent
2020-01-06 22:45:24 -08:00
lateinit var synchronizerComponent : SynchronizerSubcomponent
2020-01-05 21:01:06 -08:00
2020-02-12 04:58:41 -08:00
var navController : NavController ? = null
private val navInitListeners : MutableList < ( ) -> Unit > = mutableListOf ( )
2020-01-09 23:53:16 -08:00
private val hasCameraPermission
get ( ) = ContextCompat . checkSelfPermission (
this ,
Manifest . permission . CAMERA
) == PackageManager . PERMISSION _GRANTED
2019-12-23 11:16:00 -08:00
2019-11-26 12:29:16 -08:00
override fun onCreate ( savedInstanceState : Bundle ? ) {
2020-01-06 22:45:24 -08:00
component = ZcashWalletApp . component . mainActivitySubcomponent ( ) . create ( this ) . also {
2020-01-05 21:01:06 -08:00
it . inject ( this )
}
2019-11-26 12:29:16 -08:00
super . onCreate ( savedInstanceState )
setContentView ( R . layout . main _activity )
initNavigation ( )
2019-12-18 08:09:13 -08:00
window . statusBarColor = Color . TRANSPARENT
window . navigationBarColor = Color . TRANSPARENT
2019-11-26 12:29:16 -08:00
window . setFlags (
WindowManager . LayoutParams . FLAG _LAYOUT _NO _LIMITS ,
WindowManager . LayoutParams . FLAG _LAYOUT _NO _LIMITS
)
2019-12-18 08:09:13 -08:00
setWindowFlag ( WindowManager . LayoutParams . FLAG _TRANSLUCENT _STATUS , false )
setWindowFlag ( WindowManager . LayoutParams . FLAG _TRANSLUCENT _NAVIGATION , false )
2019-12-14 11:39:19 -08:00
2019-12-17 13:34:42 -08:00
lifecycleScope . launch {
feedback . start ( )
}
2019-12-14 11:39:19 -08:00
}
override fun onResume ( ) {
super . onResume ( )
// keep track of app launch metrics
// (how long does it take the app to open when it is not already in the foreground)
ZcashWalletApp . instance . let { app ->
if ( ! app . creationMeasured ) {
app . creationMeasured = true
feedback . report ( LaunchMetric ( ) )
}
}
}
override fun onDestroy ( ) {
lifecycleScope . launch {
2019-12-23 11:12:34 -08:00
feedback . report ( FEEDBACK _STOPPED )
2019-12-14 11:39:19 -08:00
feedback . stop ( )
}
super . onDestroy ( )
2019-11-26 12:29:16 -08:00
}
private fun setWindowFlag ( bits : Int , on : Boolean ) {
val win = window
val winParams = win . attributes
if ( on ) {
winParams . flags = winParams . flags or bits
} else {
winParams . flags = winParams . flags and bits . inv ( )
}
win . attributes = winParams
}
2019-11-27 06:24:00 -08:00
2019-11-26 12:29:16 -08:00
private fun initNavigation ( ) {
navController = findNavController ( R . id . nav _host _fragment )
2020-02-12 04:58:41 -08:00
navController !! . addOnDestinationChangedListener { _ , _ , _ ->
2019-11-26 12:29:16 -08:00
// hide the keyboard anytime we change destinations
getSystemService < InputMethodManager > ( ) ?. hideSoftInputFromWindow (
this @MainActivity . window . decorView . rootView . windowToken ,
InputMethodManager . HIDE _NOT _ALWAYS
)
}
2020-02-12 04:58:41 -08:00
for ( listener in navInitListeners ) {
listener ( )
}
navInitListeners . clear ( )
}
fun safeNavigate ( @IdRes destination : Int ) {
if ( navController == null ) {
navInitListeners . add {
try {
navController ?. navigate ( destination )
} catch ( t : Throwable ) {
twig ( " WARNING: during callback, did not navigate to destination: R.id. ${resources.getResourceEntryName(destination)} due to: $t " )
}
}
} else {
try {
navController ?. navigate ( destination )
} catch ( t : Throwable ) {
twig ( " WARNING: did not immediately navigate to destination: R.id. ${resources.getResourceEntryName(destination)} due to: $t " )
}
}
2019-11-26 12:29:16 -08:00
}
2020-01-06 22:45:24 -08:00
fun startSync ( initializer : Initializer ) {
2020-02-12 04:58:41 -08:00
if ( ! :: synchronizerComponent . isInitialized ) {
synchronizerComponent = ZcashWalletApp . component . synchronizerSubcomponent ( ) . create ( initializer )
feedback . report ( SYNC _START )
synchronizerComponent . synchronizer ( ) . let { synchronizer ->
synchronizer . onProcessorErrorHandler = :: onProcessorError
2020-02-21 15:49:16 -08:00
synchronizer . onChainErrorHandler = :: onChainError
2020-02-12 04:58:41 -08:00
synchronizer . start ( lifecycleScope )
}
} else {
twig ( " Ignoring request to start sync because sync has already been started! " )
}
2019-12-23 11:12:34 -08:00
}
2020-02-21 15:49:16 -08:00
fun reportScreen ( screen : Report . Screen ? ) = reportAction ( screen )
fun reportTap ( tap : Report . Tap ? ) = reportAction ( tap )
fun reportFunnel ( step : Feedback . Funnel ? ) = reportAction ( step )
private fun reportAction ( action : Feedback . Action ? ) {
action ?. let { feedback . report ( it ) }
}
2019-11-27 06:24:00 -08:00
fun playSound ( fileName : String ) {
mediaPlayer . apply {
if ( isPlaying ) stop ( )
try {
reset ( )
assets . openFd ( fileName ) . let { afd ->
setDataSource ( afd . fileDescriptor , afd . startOffset , afd . length )
}
prepare ( )
start ( )
} catch ( t : Throwable ) {
Log . e ( " SDK_ERROR " , " ERROR: unable to play sound due to $t " )
}
}
}
// TODO: spruce this up with API 26 stuff
fun vibrateSuccess ( ) {
val vibrator = getSystemService ( Context . VIBRATOR _SERVICE ) as Vibrator
if ( vibrator . hasVibrator ( ) ) {
vibrator . vibrate ( longArrayOf ( 0 , 200 , 200 , 100 , 100 , 800 ) , - 1 )
}
}
2020-01-09 08:00:20 -08:00
fun copyAddress ( view : View ? = null ) {
2020-02-21 15:49:16 -08:00
reportTap ( COPY _ADDRESS )
2019-12-23 11:16:00 -08:00
lifecycleScope . launch {
clipboard . setPrimaryClip (
ClipData . newPlainText (
" Z-Address " ,
2020-01-06 22:45:24 -08:00
synchronizerComponent . synchronizer ( ) . getAddress ( )
2019-12-23 11:16:00 -08:00
)
2019-11-26 12:29:16 -08:00
)
2019-12-23 11:16:00 -08:00
showMessage ( " Address copied! " , " Sweet " )
}
2019-11-26 12:29:16 -08:00
}
2020-01-13 16:09:22 -08:00
fun copyText ( textToCopy : String , label : String = " zECC Wallet Text " ) {
clipboard . setPrimaryClip (
ClipData . newPlainText ( label , textToCopy )
)
showMessage ( " $label copied! " , " Sweet " )
}
2020-01-09 08:00:20 -08:00
fun preventBackPress ( fragment : Fragment ) {
onFragmentBackPressed ( fragment ) { }
}
fun onFragmentBackPressed ( fragment : Fragment , block : ( ) -> Unit ) {
onBackPressedDispatcher . addCallback ( fragment , object : OnBackPressedCallback ( true ) {
override fun handleOnBackPressed ( ) {
block ( )
}
} )
}
2019-11-26 12:29:16 -08:00
private fun showMessage ( message : String , action : String ) {
Toast . makeText ( this , message , Toast . LENGTH _SHORT ) . show ( )
}
2019-12-17 13:34:42 -08:00
fun showSnackbar ( message : String , action : String = " OK " ) : Snackbar {
return if ( snackbar == null ) {
val view = findViewById < View > ( R . id . main _activity _container )
val snacks = Snackbar
. make ( view , " $message " , Snackbar . LENGTH _INDEFINITE )
. setAction ( action ) { /*auto-close*/ }
val snackBarView = snacks . view as ViewGroup
val navigationBarHeight = resources . getDimensionPixelSize ( resources . getIdentifier ( " navigation_bar_height " , " dimen " , " android " ) )
val params = snackBarView . getChildAt ( 0 ) . layoutParams as ViewGroup . MarginLayoutParams
params . setMargins (
params . leftMargin ,
params . topMargin ,
params . rightMargin ,
navigationBarHeight
)
snackBarView . getChildAt ( 0 ) . setLayoutParams ( params )
snacks
} else {
snackbar !! . setText ( message ) . setAction ( action ) { /*auto-close*/ }
} . also {
if ( ! it . isShownOrQueued ) it . show ( )
}
}
2020-01-09 23:53:16 -08:00
2020-02-12 04:58:41 -08:00
fun showKeyboard ( focusedView : View ) {
window . setSoftInputMode ( WindowManager . LayoutParams . SOFT _INPUT _STATE _ALWAYS _VISIBLE )
val imm = getSystemService ( Context . INPUT _METHOD _SERVICE ) as InputMethodManager
imm . showSoftInput ( focusedView , InputMethodManager . SHOW _FORCED )
}
fun hideKeyboard ( ) {
val imm = getSystemService ( Context . INPUT _METHOD _SERVICE ) as InputMethodManager
imm . hideSoftInputFromWindow ( findViewById < View > ( android . R . id . content ) . windowToken , 0 )
}
2020-01-13 16:09:22 -08:00
/ * *
* @param popUpToInclusive the destination to remove from the stack before opening the camera .
* This only takes effect in the common case where the permission is granted .
* /
fun maybeOpenScan ( popUpToInclusive : Int ? = null ) {
2020-01-09 23:53:16 -08:00
if ( hasCameraPermission ) {
2020-01-13 16:09:22 -08:00
openCamera ( popUpToInclusive )
2020-01-09 23:53:16 -08:00
} else {
if ( Build . VERSION . SDK _INT >= Build . VERSION_CODES . M ) {
requestPermissions ( arrayOf ( Manifest . permission . CAMERA ) , 101 )
} else {
onNoCamera ( )
}
}
}
override fun onRequestPermissionsResult (
requestCode : Int ,
permissions : Array < out String > ,
grantResults : IntArray
) {
super . onRequestPermissionsResult ( requestCode , permissions , grantResults )
if ( requestCode == 101 ) {
if ( grantResults . firstOrNull ( ) == PackageManager . PERMISSION _GRANTED ) {
openCamera ( )
} else {
onNoCamera ( )
}
}
}
2020-01-13 16:09:22 -08:00
private fun openCamera ( popUpToInclusive : Int ? = null ) {
2020-02-12 04:58:41 -08:00
navController ?. navigate ( popUpToInclusive ?: R . id . action _global _nav _scan )
2020-01-09 23:53:16 -08:00
}
private fun onNoCamera ( ) {
showSnackbar ( " Well, this is awkward. You denied permission for the camera. " )
}
2020-02-12 04:58:41 -08:00
2020-03-27 13:43:08 -07:00
// TODO: clean up this error handling
2020-02-21 15:49:16 -08:00
private var ignoredErrors = 0
2020-02-12 04:58:41 -08:00
private fun onProcessorError ( error : Throwable ? ) : Boolean {
2020-02-21 15:49:16 -08:00
var notified = false
2020-02-12 04:58:41 -08:00
when ( error ) {
is CompactBlockProcessorException . Uninitialized -> {
2020-02-21 15:49:16 -08:00
if ( dialog == null ) {
notified = true
2020-02-12 04:58:41 -08:00
runOnUiThread {
dialog = MaterialAlertDialogBuilder ( this )
. setTitle ( " Wallet Improperly Initialized " )
. setMessage ( " This wallet has not been initialized correctly! Perhaps an error occurred during install. \n \n This can be fixed with a reset. Please reimport using your backup seed phrase. " )
. setCancelable ( false )
. setPositiveButton ( " Exit " ) { dialog , _ ->
dialog . dismiss ( )
throw error
}
. show ( )
}
2020-02-21 15:49:16 -08:00
}
}
is CompactBlockProcessorException . FailedScan -> {
if ( dialog == null && ! ignoreScanFailure ) throttle ( " scanFailure " , 20 _000L ) {
notified = true
runOnUiThread {
dialog = MaterialAlertDialogBuilder ( this )
. setTitle ( " Scan Failure " )
. setMessage ( " ${error.message} ${if (error.cause != null) "\n\nCaused by: ${error.cause} " else " " } " )
. setCancelable ( true )
. setPositiveButton ( " Retry " ) { d , _ ->
d . dismiss ( )
dialog = null
}
. setNegativeButton ( " Ignore " ) { d , _ ->
d . dismiss ( )
ignoreScanFailure = true
dialog = null
}
. show ( )
}
}
2020-02-12 04:58:41 -08:00
}
}
2020-02-21 15:49:16 -08:00
if ( ! notified ) {
ignoredErrors ++
2020-03-27 13:43:08 -07:00
if ( ignoredErrors >= ZcashSdk . RETRIES ) {
if ( dialog == null ) {
notified = true
runOnUiThread {
dialog = MaterialAlertDialogBuilder ( this )
. setTitle ( " Processor Error " )
. setMessage ( error ?. message ?: " Critical error while processing blocks! " )
. setCancelable ( false )
. setPositiveButton ( " Retry " ) { d , _ ->
d . dismiss ( )
dialog = null
}
. setNegativeButton ( " Exit " ) { dialog , _ ->
dialog . dismiss ( )
throw error
?: RuntimeException ( " Critical error while processing blocks and the user chose to exit. " )
}
. show ( )
}
2020-02-21 15:49:16 -08:00
}
}
}
twig ( " MainActivity has received an error ${if (notified) " and notified the user" else ""} and reported it to crashlytics and mixpanel. " )
2020-02-12 04:58:41 -08:00
feedback . report ( error )
return true
}
2020-02-21 15:49:16 -08:00
private fun onChainError ( errorHeight : Int , rewindHeight : Int ) {
feedback . report ( Reorg ( errorHeight , rewindHeight ) )
}
// TODO: maybe move this quick helper code somewhere general or throttle the dialogs differently (like with a flow and stream operators, instead)
private val throttles = mutableMapOf < String , ( ) -> Any > ( )
private val noWork = { }
private fun throttle ( key : String , delay : Long , block : ( ) -> Any ) {
// if the key exists, just add the block to run later and exit
if ( throttles . containsKey ( key ) ) {
throttles [ key ] = block
return
}
block ( )
// after doing the work, check back in later and if another request came in, throttle it, otherwise exit
throttles [ key ] = noWork
findViewById < View > ( android . R . id . content ) . postDelayed ( {
throttles [ key ] ?. let { pendingWork ->
throttles . remove ( key )
if ( pendingWork !== noWork ) throttle ( key , delay , pendingWork )
}
} , delay )
}
2019-11-26 12:29:16 -08:00
}