Refactor feedback logic and configuration.
This commit is contained in:
parent
e9e41e4a9d
commit
83056ad492
|
@ -11,12 +11,6 @@ import javax.inject.Inject
|
|||
|
||||
class ZcashWalletApp : DaggerApplication() {
|
||||
|
||||
@Inject
|
||||
lateinit var feedbackCoordinator: FeedbackCoordinator
|
||||
|
||||
@Inject
|
||||
lateinit var feedbackObservers: Set<@JvmSuppressWildcards FeedbackCoordinator.FeedbackObserver>
|
||||
|
||||
var creationTime: Long = 0
|
||||
private set
|
||||
|
||||
|
@ -30,7 +24,6 @@ class ZcashWalletApp : DaggerApplication() {
|
|||
|
||||
Thread.setDefaultUncaughtExceptionHandler(ExceptionReporter(Thread.getDefaultUncaughtExceptionHandler()))
|
||||
// Twig.plant(TroubleshootingTwig())
|
||||
feedbackObservers.forEach { feedbackCoordinator.addObserver(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,39 +1,15 @@
|
|||
package cash.z.ecc.android.di
|
||||
|
||||
import cash.z.ecc.android.feedback.*
|
||||
import android.content.Context
|
||||
import cash.z.ecc.android.ZcashWalletApp
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoSet
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module(includes = [AppBindingModule::class])
|
||||
@Module(includes = [AppBindingModule::class, ViewModelModule::class])
|
||||
class AppModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeedback() = Feedback()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeedbackCoordinator(feedback: Feedback) = FeedbackCoordinator(feedback)
|
||||
|
||||
|
||||
//
|
||||
// Feedback Observer Set
|
||||
//
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideFeedbackFile(): FeedbackCoordinator.FeedbackObserver = FeedbackFile()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideFeedbackConsole(): FeedbackCoordinator.FeedbackObserver = FeedbackConsole()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideFeedbackMixpanel(): FeedbackCoordinator.FeedbackObserver = FeedbackMixpanel()
|
||||
fun provideAppContext(): Context = ZcashWalletApp.instance
|
||||
}
|
||||
|
|
|
@ -9,14 +9,23 @@ enum class NonUserAction(override val key: String, val description: String) : Fe
|
|||
override fun toString(): String = description
|
||||
}
|
||||
|
||||
enum class MetricType(override val key: String, val description: String) : Feedback.Action {
|
||||
SEED_CREATION("metric.seed.creation", "seed created")
|
||||
}
|
||||
|
||||
class LaunchMetric private constructor(private val metric: Feedback.TimeMetric) :
|
||||
Feedback.Metric by metric {
|
||||
constructor() : this(
|
||||
Feedback
|
||||
.TimeMetric("metric.app.launch", mutableListOf(ZcashWalletApp.instance.creationTime))
|
||||
.TimeMetric(
|
||||
"metric.app.launch",
|
||||
"app launched",
|
||||
mutableListOf(ZcashWalletApp.instance.creationTime)
|
||||
)
|
||||
.markTime()
|
||||
)
|
||||
override fun toString(): String {
|
||||
return "app launched in ${metric.elapsedTime}ms"
|
||||
}
|
||||
override fun toString(): String = metric.toString()
|
||||
}
|
||||
|
||||
fun <T> Feedback.measure(type: MetricType, block: () -> T) =
|
||||
this.measure(type.key, type.description, block)
|
|
@ -9,6 +9,7 @@ import android.os.Bundle
|
|||
import android.os.Vibrator
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
|
@ -19,12 +20,13 @@ import androidx.navigation.findNavController
|
|||
import cash.z.ecc.android.R
|
||||
import cash.z.ecc.android.ZcashWalletApp
|
||||
import cash.z.ecc.android.di.annotation.ActivityScope
|
||||
import cash.z.ecc.android.feedback.Feedback
|
||||
import cash.z.ecc.android.feedback.LaunchMetric
|
||||
import cash.z.ecc.android.feedback.NonUserAction.FEEDBACK_STOPPED
|
||||
import cash.z.ecc.android.feedback.*
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import dagger.android.support.DaggerAppCompatActivity
|
||||
import dagger.multibindings.IntoSet
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -34,10 +36,15 @@ class MainActivity : DaggerAppCompatActivity() {
|
|||
@Inject
|
||||
lateinit var feedback: Feedback
|
||||
|
||||
@Inject
|
||||
lateinit var feedbackCoordinator: FeedbackCoordinator
|
||||
|
||||
lateinit var navController: NavController
|
||||
|
||||
private val mediaPlayer: MediaPlayer = MediaPlayer()
|
||||
|
||||
private var snackbar: Snackbar? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.main_activity)
|
||||
|
@ -53,7 +60,9 @@ class MainActivity : DaggerAppCompatActivity() {
|
|||
false
|
||||
)// | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, false)
|
||||
|
||||
lifecycleScope.launch { feedback.start() }
|
||||
lifecycleScope.launch {
|
||||
feedback.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -70,7 +79,7 @@ class MainActivity : DaggerAppCompatActivity() {
|
|||
|
||||
override fun onDestroy() {
|
||||
lifecycleScope.launch {
|
||||
feedback.report(FEEDBACK_STOPPED)
|
||||
feedback.report(NonUserAction.FEEDBACK_STOPPED)
|
||||
feedback.stop()
|
||||
}
|
||||
super.onDestroy()
|
||||
|
@ -140,11 +149,73 @@ class MainActivity : DaggerAppCompatActivity() {
|
|||
private fun showMessage(message: String, action: String) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
abstract class MainActivityModule {
|
||||
@ActivityScope
|
||||
@ContributesAndroidInjector
|
||||
@ContributesAndroidInjector(modules = [MainActivityProviderModule::class])
|
||||
abstract fun contributeActivity(): MainActivity
|
||||
|
||||
}
|
||||
|
||||
@Module
|
||||
class MainActivityProviderModule {
|
||||
|
||||
@Provides
|
||||
@ActivityScope
|
||||
fun provideFeedback(): Feedback = Feedback()
|
||||
|
||||
@Provides
|
||||
@ActivityScope
|
||||
fun provideFeedbackCoordinator(
|
||||
feedback: Feedback,
|
||||
defaultObservers: Set<@JvmSuppressWildcards FeedbackCoordinator.FeedbackObserver>
|
||||
): FeedbackCoordinator = FeedbackCoordinator(feedback, defaultObservers)
|
||||
|
||||
|
||||
//
|
||||
// Default Feedback Observer Set
|
||||
//
|
||||
|
||||
@Provides
|
||||
@ActivityScope
|
||||
@IntoSet
|
||||
fun provideFeedbackFile(): FeedbackCoordinator.FeedbackObserver = FeedbackFile()
|
||||
|
||||
@Provides
|
||||
@ActivityScope
|
||||
@IntoSet
|
||||
fun provideFeedbackConsole(): FeedbackCoordinator.FeedbackObserver = FeedbackConsole()
|
||||
|
||||
@Provides
|
||||
@ActivityScope
|
||||
@IntoSet
|
||||
fun provideFeedbackMixpanel(): FeedbackCoordinator.FeedbackObserver = FeedbackMixpanel()
|
||||
}
|
|
@ -32,6 +32,9 @@ object Deps {
|
|||
object Google {
|
||||
const val MATERIAL = "com.google.android.material:material:1.1.0-beta01"
|
||||
}
|
||||
object JavaX {
|
||||
const val INJECT = "javax.inject:javax.inject:1"
|
||||
}
|
||||
object Kotlin : Version(kotlinVersion) {
|
||||
val STDLIB = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$version"
|
||||
object Coroutines : Version("1.3.2") {
|
||||
|
@ -50,9 +53,7 @@ object Deps {
|
|||
}
|
||||
}
|
||||
|
||||
open class Version(@JvmField val version: String) {
|
||||
@JvmField val t = version
|
||||
}
|
||||
open class Version(@JvmField val version: String)
|
||||
|
||||
//zzz
|
||||
//zzz "androidx.constraintlayout:constraintlayout:2.0.0-alpha3"
|
||||
|
|
|
@ -92,9 +92,9 @@ class Feedback(capacity: Int = 256) {
|
|||
* will run concurrently--meaning a "happens before" relationship between the measurer and the
|
||||
* measured cannot be established and thereby the concurrent action cannot be timed.
|
||||
*/
|
||||
inline fun <T> measure(description: Any = "measurement", block: () -> T) {
|
||||
inline fun <T> measure(key: String = "measurement.generic", description: Any = "measurement", block: () -> T) {
|
||||
ensureScope()
|
||||
val metric = TimeMetric(description.toString()).markTime()
|
||||
val metric = TimeMetric(key, description.toString()).markTime()
|
||||
block()
|
||||
metric.markTime()
|
||||
report(metric)
|
||||
|
@ -151,10 +151,12 @@ class Feedback(capacity: Int = 256) {
|
|||
val startTime: Long?
|
||||
val endTime: Long?
|
||||
val elapsedTime: Long?
|
||||
val description: String
|
||||
|
||||
override fun toMap(): Map<String, Any> {
|
||||
return mapOf(
|
||||
"key" to key,
|
||||
"description" to description,
|
||||
"startTime" to (startTime ?: 0),
|
||||
"endTime" to (endTime ?: 0),
|
||||
"elapsedTime" to (elapsedTime ?: 0)
|
||||
|
@ -175,6 +177,7 @@ class Feedback(capacity: Int = 256) {
|
|||
|
||||
data class TimeMetric(
|
||||
override val key: String,
|
||||
override val description: String,
|
||||
val times: MutableList<Long> = mutableListOf()
|
||||
) : Metric {
|
||||
override val startTime: Long? get() = times.firstOrNull()
|
||||
|
@ -184,5 +187,9 @@ class Feedback(capacity: Int = 256) {
|
|||
times.add(System.currentTimeMillis())
|
||||
return this
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "$description in ${elapsedTime}ms"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,16 @@
|
|||
package cash.z.ecc.android.feedback
|
||||
|
||||
import android.util.Log
|
||||
import cash.z.ecc.android.feedback.util.CompositeJob
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.lang.IllegalStateException
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
|
||||
/**
|
||||
* Takes care of the boilerplate involved in processing feedback emissions. Simply provide callbacks
|
||||
* and emissions will occur in a mutually exclusive way, across all processors, so that things like
|
||||
|
@ -15,7 +18,7 @@ import kotlin.coroutines.coroutineContext
|
|||
* waiting for any in-flight emissions to complete. Lastly, all monitoring will cleanly complete
|
||||
* whenever the feedback is stopped or its parent scope is cancelled.
|
||||
*/
|
||||
class FeedbackCoordinator(val feedback: Feedback) {
|
||||
class FeedbackCoordinator(val feedback: Feedback, defaultObservers: Set<FeedbackObserver> = setOf()) {
|
||||
|
||||
init {
|
||||
feedback.apply {
|
||||
|
@ -25,6 +28,11 @@ class FeedbackCoordinator(val feedback: Feedback) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (defaultObservers.size != 3) throw IllegalStateException("BOOM")
|
||||
defaultObservers.forEach {
|
||||
Log.e("BOOM", "adding observer: $it to $feedback")
|
||||
addObserver(it)
|
||||
}
|
||||
}
|
||||
|
||||
private var contextMetrics = Dispatchers.IO
|
||||
|
|
|
@ -4,39 +4,39 @@ import kotlinx.coroutines.Job
|
|||
|
||||
class CompositeJob {
|
||||
|
||||
private val jobs = mutableListOf<Job>()
|
||||
val size: Int get() = jobs.size
|
||||
private val activeJobs = mutableListOf<Job>()
|
||||
val size: Int get() = activeJobs.size
|
||||
|
||||
fun add(job: Job) {
|
||||
jobs.add(job)
|
||||
activeJobs.add(job)
|
||||
job.invokeOnCompletion {
|
||||
remove(job)
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(job: Job): Boolean {
|
||||
return jobs.remove(job)
|
||||
return activeJobs.remove(job)
|
||||
}
|
||||
|
||||
fun isActive(): Boolean {
|
||||
return jobs.any { isActive() }
|
||||
return activeJobs.any { isActive() }
|
||||
}
|
||||
|
||||
suspend fun await() {
|
||||
// allow for concurrent modification since the list isn't coroutine or thread safe
|
||||
do {
|
||||
val job = jobs.firstOrNull()
|
||||
val job = activeJobs.firstOrNull()
|
||||
if (job?.isActive == true) {
|
||||
job.join()
|
||||
} else {
|
||||
// prevents an infinite loop in the extreme edge case where the list has a null item
|
||||
try { jobs.remove(job) } catch (t: Throwable) {}
|
||||
try { activeJobs.remove(job) } catch (t: Throwable) {}
|
||||
}
|
||||
} while (size > 0)
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
jobs.filter { isActive() }.forEach { it.cancel() }
|
||||
activeJobs.filter { isActive() }.forEach { it.cancel() }
|
||||
}
|
||||
|
||||
operator fun plusAssign(also: Job) {
|
||||
|
|
Loading…
Reference in New Issue