Refactor feedback logic and configuration.

This commit is contained in:
Kevin Gorham 2019-12-17 16:34:42 -05:00
parent e9e41e4a9d
commit 83056ad492
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
8 changed files with 124 additions and 59 deletions

View File

@ -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) }
}
/**

View File

@ -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
}

View File

@ -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)

View File

@ -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()
}

View File

@ -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"

View File

@ -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"
}
}
}

View File

@ -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

View File

@ -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) {