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-08-28 00:40:14 -07:00
|
|
|
import android.content.Intent
|
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-08-28 00:40:14 -07:00
|
|
|
import android.net.Uri
|
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
|
2020-12-19 13:44:10 -08:00
|
|
|
import android.widget.TextView
|
2019-11-26 12:29:16 -08:00
|
|
|
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-08-28 00:35:50 -07:00
|
|
|
import androidx.annotation.StringRes
|
2020-01-05 21:01:06 -08:00
|
|
|
import androidx.appcompat.app.AppCompatActivity
|
2021-04-23 07:40:46 -07:00
|
|
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
|
|
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
|
|
|
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
2020-07-31 23:46:49 -07:00
|
|
|
import androidx.biometric.BiometricPrompt
|
2021-04-23 07:40:46 -07:00
|
|
|
import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC
|
|
|
|
import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL
|
|
|
|
import androidx.biometric.BiometricPrompt.ERROR_CANCELED
|
|
|
|
import androidx.biometric.BiometricPrompt.ERROR_HW_NOT_PRESENT
|
|
|
|
import androidx.biometric.BiometricPrompt.ERROR_HW_UNAVAILABLE
|
|
|
|
import androidx.biometric.BiometricPrompt.ERROR_LOCKOUT
|
|
|
|
import androidx.biometric.BiometricPrompt.ERROR_LOCKOUT_PERMANENT
|
|
|
|
import androidx.biometric.BiometricPrompt.ERROR_NEGATIVE_BUTTON
|
|
|
|
import androidx.biometric.BiometricPrompt.ERROR_NO_BIOMETRICS
|
|
|
|
import androidx.biometric.BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL
|
|
|
|
import androidx.biometric.BiometricPrompt.ERROR_NO_SPACE
|
|
|
|
import androidx.biometric.BiometricPrompt.ERROR_TIMEOUT
|
|
|
|
import androidx.biometric.BiometricPrompt.ERROR_UNABLE_TO_PROCESS
|
|
|
|
import androidx.biometric.BiometricPrompt.ERROR_USER_CANCELED
|
|
|
|
import androidx.biometric.BiometricPrompt.ERROR_VENDOR
|
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
|
2020-08-28 00:40:14 -07:00
|
|
|
import androidx.navigation.Navigator
|
2019-11-26 12:29:16 -08:00
|
|
|
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-08-28 00:35:50 -07:00
|
|
|
import cash.z.ecc.android.databinding.DialogFirstUseMessageBinding
|
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
|
2020-08-28 00:35:50 -07:00
|
|
|
import cash.z.ecc.android.di.viewmodel.activityViewModel
|
2020-12-19 13:44:10 -08:00
|
|
|
import cash.z.ecc.android.ext.goneIf
|
2021-04-23 11:28:38 -07:00
|
|
|
import cash.z.ecc.android.ext.showCriticalMessage
|
2020-10-07 16:46:14 -07:00
|
|
|
import cash.z.ecc.android.ext.showCriticalProcessorError
|
|
|
|
import cash.z.ecc.android.ext.showScanFailure
|
|
|
|
import cash.z.ecc.android.ext.showUninitializedError
|
2020-01-06 22:45:24 -08:00
|
|
|
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
|
2020-12-19 13:46:01 -08:00
|
|
|
import cash.z.ecc.android.sdk.SdkSynchronizer
|
2020-08-28 00:40:14 -07:00
|
|
|
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
2020-06-10 04:49:38 -07:00
|
|
|
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException
|
2021-04-23 06:31:28 -07:00
|
|
|
import cash.z.ecc.android.sdk.ext.BatchMetrics
|
2020-06-10 04:49:38 -07:00
|
|
|
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
2020-08-28 00:40:14 -07:00
|
|
|
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
|
2020-06-10 04:49:38 -07:00
|
|
|
import cash.z.ecc.android.sdk.ext.twig
|
2020-08-28 00:40:14 -07:00
|
|
|
import cash.z.ecc.android.ui.history.HistoryViewModel
|
2021-04-23 07:07:52 -07:00
|
|
|
import cash.z.ecc.android.ui.util.MemoUtil
|
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
|
2020-12-19 13:44:10 -08:00
|
|
|
import kotlinx.coroutines.CoroutineScope
|
|
|
|
import kotlinx.coroutines.delay
|
|
|
|
import kotlinx.coroutines.flow.collect
|
2019-12-14 11:39:19 -08:00
|
|
|
import kotlinx.coroutines.launch
|
2021-04-23 11:28:38 -07:00
|
|
|
import java.lang.RuntimeException
|
2019-12-14 11:39:19 -08:00
|
|
|
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
|
|
|
|
2020-12-19 13:44:10 -08:00
|
|
|
@Inject
|
|
|
|
lateinit var mainViewModel: MainViewModel
|
|
|
|
|
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
|
|
|
|
2020-08-28 00:40:14 -07:00
|
|
|
val isInitialized get() = ::synchronizerComponent.isInitialized
|
|
|
|
|
2020-08-28 00:35:50 -07:00
|
|
|
val historyViewModel: HistoryViewModel by activityViewModel()
|
|
|
|
|
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
|
|
|
|
2020-08-28 00:40:14 -07:00
|
|
|
val latestHeight: Int? get() = if (isInitialized) {
|
2020-07-31 23:50:25 -07:00
|
|
|
synchronizerComponent.synchronizer().latestHeight
|
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
}
|
2020-09-25 08:50:20 -07:00
|
|
|
lifecycleScope.launch {
|
|
|
|
feedback.start()
|
|
|
|
}
|
2019-11-26 12:29:16 -08:00
|
|
|
super.onCreate(savedInstanceState)
|
2020-09-25 08:50:20 -07:00
|
|
|
|
2019-11-26 12:29:16 -08:00
|
|
|
setContentView(R.layout.main_activity)
|
|
|
|
initNavigation()
|
2020-12-19 13:44:10 -08:00
|
|
|
initLoadScreen()
|
2019-11-26 12:29:16 -08:00
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2020-12-19 13:44:10 -08:00
|
|
|
private fun initLoadScreen() {
|
|
|
|
lifecycleScope.launchWhenResumed {
|
|
|
|
mainViewModel.loadingMessage.collect { message ->
|
|
|
|
onLoadingMessage(message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun onLoadingMessage(message: String?) {
|
|
|
|
twig("Applying loading message: $message")
|
|
|
|
// TODO: replace with view binding
|
|
|
|
findViewById<View>(R.id.container_loading).goneIf(message == null)
|
|
|
|
findViewById<TextView>(R.id.text_message).text = message
|
|
|
|
}
|
|
|
|
|
2021-04-23 11:22:44 -07:00
|
|
|
fun popBackTo(@IdRes destination: Int, inclusive: Boolean = false) {
|
|
|
|
navController?.popBackStack(destination, inclusive)
|
|
|
|
}
|
|
|
|
|
2020-08-28 00:40:14 -07:00
|
|
|
fun safeNavigate(@IdRes destination: Int, extras: Navigator.Extras? = null) {
|
2020-02-12 04:58:41 -08:00
|
|
|
if (navController == null) {
|
|
|
|
navInitListeners.add {
|
|
|
|
try {
|
2020-08-28 00:40:14 -07:00
|
|
|
navController?.navigate(destination, null, null, extras)
|
2020-02-12 04:58:41 -08:00
|
|
|
} catch (t: Throwable) {
|
2020-08-28 00:40:14 -07:00
|
|
|
twig(
|
|
|
|
"WARNING: during callback, did not navigate to destination: R.id.${
|
|
|
|
resources.getResourceEntryName(
|
|
|
|
destination
|
|
|
|
)
|
|
|
|
} due to: $t"
|
|
|
|
)
|
2020-02-12 04:58:41 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
try {
|
2020-08-28 00:40:14 -07:00
|
|
|
navController?.navigate(destination, null, null, extras)
|
2020-02-12 04:58:41 -08:00
|
|
|
} catch (t: Throwable) {
|
2020-08-28 00:40:14 -07:00
|
|
|
twig(
|
|
|
|
"WARNING: did not immediately navigate to destination: R.id.${
|
|
|
|
resources.getResourceEntryName(
|
|
|
|
destination
|
|
|
|
)
|
|
|
|
} due to: $t"
|
|
|
|
)
|
2020-02-12 04:58:41 -08:00
|
|
|
}
|
|
|
|
}
|
2019-11-26 12:29:16 -08:00
|
|
|
}
|
|
|
|
|
2021-04-23 11:23:46 -07:00
|
|
|
fun startSync(initializer: Initializer, isRestart: Boolean = false) {
|
2020-12-19 13:46:01 -08:00
|
|
|
twig("MainActivity.startSync")
|
2021-04-23 11:23:46 -07:00
|
|
|
if (!isInitialized || isRestart) {
|
2020-12-19 13:44:10 -08:00
|
|
|
mainViewModel.setLoading(true)
|
2020-08-28 00:40:14 -07:00
|
|
|
synchronizerComponent = ZcashWalletApp.component.synchronizerSubcomponent().create(
|
|
|
|
initializer
|
|
|
|
)
|
2020-12-19 13:46:01 -08:00
|
|
|
twig("Synchronizer component created")
|
2020-02-12 04:58:41 -08:00
|
|
|
feedback.report(SYNC_START)
|
|
|
|
synchronizerComponent.synchronizer().let { synchronizer ->
|
|
|
|
synchronizer.onProcessorErrorHandler = ::onProcessorError
|
2020-02-21 15:49:16 -08:00
|
|
|
synchronizer.onChainErrorHandler = ::onChainError
|
2021-04-23 11:21:52 -07:00
|
|
|
synchronizer.onCriticalErrorHandler = ::onCriticalError
|
2021-04-23 06:31:28 -07:00
|
|
|
(synchronizer as SdkSynchronizer).processor.onScanMetricCompleteListener = ::onScanMetricComplete
|
|
|
|
|
2020-02-12 04:58:41 -08:00
|
|
|
synchronizer.start(lifecycleScope)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
twig("Ignoring request to start sync because sync has already been started!")
|
|
|
|
}
|
2020-12-19 13:44:10 -08:00
|
|
|
mainViewModel.setLoading(false)
|
2020-12-19 13:46:01 -08:00
|
|
|
twig("MainActivity.startSync COMPLETE")
|
2019-12-23 11:12:34 -08:00
|
|
|
}
|
|
|
|
|
2021-04-23 06:31:28 -07:00
|
|
|
private fun onScanMetricComplete(batchMetrics: BatchMetrics, isComplete: Boolean) {
|
|
|
|
val reportingThreshold = 100
|
|
|
|
if (isComplete) {
|
|
|
|
if (batchMetrics.cumulativeItems > reportingThreshold) {
|
|
|
|
val network = synchronizerComponent.synchronizer().network.networkName
|
|
|
|
reportAction(Report.Performance.ScanRate(network, batchMetrics.cumulativeItems, batchMetrics.cumulativeTime, batchMetrics.cumulativeIps))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-23 11:21:52 -07:00
|
|
|
private fun onCriticalError(error: Throwable?): Boolean {
|
|
|
|
val errorMessage = error?.message
|
|
|
|
?: error?.cause?.message
|
|
|
|
?: error?.toString()
|
|
|
|
?: "A critical error has occurred but no details were provided. Please report and consider submitting logs to help track this one down."
|
|
|
|
showCriticalMessage(
|
|
|
|
title = "Unrecoverable Error",
|
|
|
|
message = errorMessage,
|
|
|
|
) {
|
|
|
|
throw error ?: RuntimeException("A critical error occurred but it was null")
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
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) }
|
|
|
|
}
|
|
|
|
|
2020-12-19 13:44:10 -08:00
|
|
|
fun setLoading(isLoading: Boolean, message: String? = null) {
|
|
|
|
mainViewModel.setLoading(isLoading, message)
|
|
|
|
}
|
|
|
|
|
2020-12-19 13:46:01 -08:00
|
|
|
/**
|
|
|
|
* Launch the given block if the synchronizer is ready and syncing. Otherwise, wait until it is.
|
|
|
|
* The block will be scoped to the synchronizer when it runs.
|
|
|
|
*/
|
|
|
|
fun launchWhenSyncing(block: suspend CoroutineScope.() -> Unit) {
|
|
|
|
// TODO: update this quick and dirty implementation, after the holidays. For now, this gets
|
|
|
|
// the job done but the synchronizer should expose a method that helps with this so that
|
|
|
|
// any complexity is taken care of at the library level.
|
|
|
|
lifecycleScope.launch {
|
|
|
|
while (mainViewModel.isLoading) {
|
|
|
|
delay(25L)
|
|
|
|
}
|
|
|
|
(synchronizerComponent.synchronizer() as SdkSynchronizer).coroutineScope.launch {
|
|
|
|
block()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-19 07:23:47 -08:00
|
|
|
fun authenticate(description: String, title: String = getString(R.string.biometric_prompt_title), block: () -> Unit) {
|
2020-07-31 23:46:49 -07:00
|
|
|
val callback = object : BiometricPrompt.AuthenticationCallback() {
|
|
|
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
2021-04-23 07:40:46 -07:00
|
|
|
twig("Authentication success with type: ${if (result.authenticationType == AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL) "DEVICE_CREDENTIAL" else if (result.authenticationType == AUTHENTICATION_RESULT_TYPE_BIOMETRIC) "BIOMETRIC" else "UNKNOWN"} object: ${result.cryptoObject}")
|
2020-07-31 23:46:49 -07:00
|
|
|
block()
|
2021-04-23 07:40:46 -07:00
|
|
|
twig("Done authentication block")
|
|
|
|
// we probably only need to do this if the type is DEVICE_CREDENTIAL
|
|
|
|
// but it doesn't hurt to hide the keyboard every time
|
|
|
|
hideKeyboard()
|
2020-07-31 23:46:49 -07:00
|
|
|
}
|
|
|
|
override fun onAuthenticationFailed() {
|
|
|
|
twig("Authentication failed!!!!")
|
2020-12-19 07:23:47 -08:00
|
|
|
showMessage("Authentication failed :(")
|
2020-07-31 23:46:49 -07:00
|
|
|
}
|
|
|
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
2020-12-19 07:23:47 -08:00
|
|
|
twig("Authentication Error")
|
|
|
|
fun doNothing(message: String, interruptUser: Boolean = true) {
|
|
|
|
if (interruptUser) {
|
|
|
|
showSnackbar(message)
|
|
|
|
} else {
|
|
|
|
showMessage(message, true)
|
|
|
|
}
|
|
|
|
}
|
2020-07-31 23:46:49 -07:00
|
|
|
when (errorCode) {
|
2020-12-19 07:23:47 -08:00
|
|
|
ERROR_HW_NOT_PRESENT, ERROR_HW_UNAVAILABLE,
|
|
|
|
ERROR_NO_BIOMETRICS, ERROR_NO_DEVICE_CREDENTIAL -> {
|
2020-07-31 23:46:49 -07:00
|
|
|
twig("Warning: bypassing authentication because $errString [$errorCode]")
|
2020-12-19 07:23:47 -08:00
|
|
|
showMessage("Please enable screen lock on this device to add security here!", true)
|
2020-07-31 23:46:49 -07:00
|
|
|
block()
|
|
|
|
}
|
2020-12-19 07:23:47 -08:00
|
|
|
ERROR_LOCKOUT -> doNothing("Too many attempts. Try again in 30s.")
|
|
|
|
ERROR_LOCKOUT_PERMANENT -> doNothing("Whoa. Waaaay too many attempts!")
|
|
|
|
ERROR_CANCELED -> doNothing("I just can't right now. Please try again.")
|
|
|
|
ERROR_NEGATIVE_BUTTON -> doNothing("Authentication cancelled", false)
|
|
|
|
ERROR_USER_CANCELED -> doNothing("Cancelled", false)
|
|
|
|
ERROR_NO_SPACE -> doNothing("Not enough storage space!")
|
|
|
|
ERROR_TIMEOUT -> doNothing("Oops. It timed out.")
|
|
|
|
ERROR_UNABLE_TO_PROCESS -> doNothing(".")
|
|
|
|
ERROR_VENDOR -> doNothing("We got some weird error and you should report this.")
|
2021-04-23 07:40:46 -07:00
|
|
|
else -> {
|
|
|
|
twig("Warning: unrecognized authentication error $errorCode")
|
|
|
|
doNothing("Authentication failed with error code $errorCode")
|
|
|
|
}
|
2020-07-31 23:46:49 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
BiometricPrompt(this, ContextCompat.getMainExecutor(this), callback).apply {
|
|
|
|
authenticate(
|
|
|
|
BiometricPrompt.PromptInfo.Builder()
|
2020-12-19 07:23:47 -08:00
|
|
|
.setTitle(title)
|
2020-07-31 23:46:49 -07:00
|
|
|
.setConfirmationRequired(false)
|
|
|
|
.setDescription(description)
|
2021-04-23 07:40:46 -07:00
|
|
|
.setAllowedAuthenticators(BIOMETRIC_STRONG or BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
2020-07-31 23:46:49 -07:00
|
|
|
.build()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
2021-04-23 07:37:28 -07:00
|
|
|
fun vibrateSuccess() = vibrate(0, 200, 200, 100, 100, 800)
|
|
|
|
|
|
|
|
fun vibrate(initialDelay: Long, vararg durations: Long) {
|
2019-11-27 06:24:00 -08:00
|
|
|
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
|
|
|
if (vibrator.hasVibrator()) {
|
2021-04-23 07:37:28 -07:00
|
|
|
vibrator.vibrate(longArrayOf(initialDelay, *durations), -1)
|
2019-11-27 06:24:00 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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 {
|
2021-04-23 11:21:14 -07:00
|
|
|
copyText(synchronizerComponent.synchronizer().getAddress(), "Address")
|
2019-12-23 11:16:00 -08:00
|
|
|
}
|
2019-11-26 12:29:16 -08:00
|
|
|
}
|
|
|
|
|
2020-06-10 14:09:20 -07:00
|
|
|
fun copyText(textToCopy: String, label: String = "ECC Wallet Text") {
|
2020-01-13 16:09:22 -08:00
|
|
|
clipboard.setPrimaryClip(
|
|
|
|
ClipData.newPlainText(label, textToCopy)
|
|
|
|
)
|
2020-12-19 07:23:47 -08:00
|
|
|
showMessage("$label copied!")
|
2021-04-23 11:21:14 -07:00
|
|
|
vibrate(0, 50)
|
|
|
|
}
|
|
|
|
|
|
|
|
suspend fun isValidAddress(address: String): Boolean {
|
|
|
|
try {
|
|
|
|
return !synchronizerComponent.synchronizer().validateAddress(address).isNotValid
|
|
|
|
} catch (t: Throwable) { }
|
|
|
|
return false
|
2020-01-13 16:09:22 -08:00
|
|
|
}
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-12-19 07:23:47 -08:00
|
|
|
private fun showMessage(message: String, linger: Boolean = false) {
|
2021-04-23 11:28:38 -07:00
|
|
|
twig("toast: $message")
|
2020-12-19 07:23:47 -08:00
|
|
|
Toast.makeText(this, message, if (linger) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show()
|
2019-11-26 12:29:16 -08:00
|
|
|
}
|
2019-12-17 13:34:42 -08:00
|
|
|
|
2020-10-07 16:46:14 -07:00
|
|
|
fun showSnackbar(message: String, action: String = getString(android.R.string.ok)): Snackbar {
|
2019-12-17 13:34:42 -08:00
|
|
|
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
|
2020-08-28 00:40:14 -07:00
|
|
|
val navigationBarHeight = resources.getDimensionPixelSize(
|
|
|
|
resources.getIdentifier(
|
|
|
|
"navigation_bar_height",
|
|
|
|
"dimen",
|
|
|
|
"android"
|
|
|
|
)
|
|
|
|
)
|
2019-12-17 13:34:42 -08:00
|
|
|
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() {
|
2020-10-07 16:46:14 -07:00
|
|
|
showSnackbar(getString(R.string.camera_permission_denied))
|
2020-01-09 23:53:16 -08:00
|
|
|
}
|
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 {
|
2020-10-07 16:46:14 -07:00
|
|
|
dialog = showUninitializedError(error) {
|
|
|
|
dialog = null
|
|
|
|
}
|
2020-02-12 04:58:41 -08:00
|
|
|
}
|
2020-02-21 15:49:16 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
is CompactBlockProcessorException.FailedScan -> {
|
|
|
|
if (dialog == null && !ignoreScanFailure) throttle("scanFailure", 20_000L) {
|
|
|
|
notified = true
|
|
|
|
runOnUiThread {
|
2020-10-07 16:46:14 -07:00
|
|
|
dialog = showScanFailure(error,
|
|
|
|
onCancel = { dialog = null },
|
|
|
|
onDismiss = { dialog = null }
|
|
|
|
)
|
2020-02-21 15:49:16 -08:00
|
|
|
}
|
|
|
|
}
|
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 {
|
2020-10-07 16:46:14 -07:00
|
|
|
dialog = showCriticalProcessorError(error) {
|
|
|
|
dialog = null
|
|
|
|
}
|
2020-03-27 13:43:08 -07:00
|
|
|
}
|
2020-02-21 15:49:16 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-08-13 20:46:53 -07:00
|
|
|
twig("MainActivity has received an error${if (notified) " and notified the user" else ""} and reported it to bugsnag 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)
|
|
|
|
}
|
2020-08-28 00:40:14 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Memo functions that might possibly get moved to MemoUtils */
|
|
|
|
|
2021-04-23 07:07:52 -07:00
|
|
|
// private val addressRegex = """zs\d\w{65,}""".toRegex()
|
2020-08-28 00:40:14 -07:00
|
|
|
|
|
|
|
suspend fun getSender(transaction: ConfirmedTransaction?): String {
|
|
|
|
if (transaction == null) return getString(R.string.unknown)
|
2021-04-23 07:07:52 -07:00
|
|
|
return MemoUtil.findAddressInMemo(transaction, ::isValidAddress)?.toAbbreviatedAddress() ?: getString(R.string.unknown)
|
2020-08-28 00:40:14 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
suspend fun String?.validateAddress(): String? {
|
|
|
|
if (this == null) return null
|
|
|
|
return if (isValidAddress(this)) this else null
|
|
|
|
}
|
|
|
|
|
2020-08-28 00:35:50 -07:00
|
|
|
fun showFirstUseWarning(
|
|
|
|
prefKey: String,
|
|
|
|
@StringRes titleResId: Int = R.string.blank,
|
|
|
|
@StringRes msgResId: Int = R.string.blank,
|
|
|
|
@StringRes positiveResId: Int = android.R.string.ok,
|
|
|
|
@StringRes negativeResId: Int = android.R.string.cancel,
|
|
|
|
action: MainActivity.() -> Unit = {}
|
|
|
|
) {
|
|
|
|
historyViewModel.prefs.getBoolean(prefKey).let { doNotWarnAgain ->
|
|
|
|
if (doNotWarnAgain) {
|
|
|
|
action()
|
|
|
|
return@showFirstUseWarning
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
val dialogViewBinding = DialogFirstUseMessageBinding.inflate(layoutInflater)
|
|
|
|
|
|
|
|
fun savePref() {
|
|
|
|
dialogViewBinding.dialogFirstUseCheckbox.isChecked.let { wasChecked ->
|
|
|
|
historyViewModel.prefs.setBoolean(prefKey, wasChecked)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
dialogViewBinding.dialogMessage.setText(msgResId)
|
|
|
|
if (dialog != null) dialog?.dismiss()
|
|
|
|
dialog = MaterialAlertDialogBuilder(this)
|
|
|
|
.setTitle(titleResId)
|
|
|
|
.setView(dialogViewBinding.root)
|
|
|
|
.setCancelable(false)
|
|
|
|
.setPositiveButton(positiveResId) { d, _ ->
|
|
|
|
d.dismiss()
|
|
|
|
dialog = null
|
|
|
|
savePref()
|
|
|
|
action()
|
|
|
|
}
|
|
|
|
.setNegativeButton(negativeResId) { d, _ ->
|
|
|
|
d.dismiss()
|
|
|
|
dialog = null
|
|
|
|
savePref()
|
|
|
|
}
|
|
|
|
.show()
|
|
|
|
}
|
2020-08-28 00:40:14 -07:00
|
|
|
|
|
|
|
fun onLaunchUrl(url: String) {
|
|
|
|
try {
|
|
|
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
|
|
|
} catch (t: Throwable) {
|
2020-12-19 07:23:47 -08:00
|
|
|
showMessage(getString(R.string.error_launch_url))
|
2020-08-28 00:40:14 -07:00
|
|
|
twig("Warning: failed to open browser due to $t")
|
|
|
|
}
|
|
|
|
}
|
2020-12-19 13:44:10 -08:00
|
|
|
|
2019-11-26 12:29:16 -08:00
|
|
|
}
|