diff --git a/app/src/main/java/cash/z/ecc/android/ZcashWalletApp.kt b/app/src/main/java/cash/z/ecc/android/ZcashWalletApp.kt index b5fc5a1..8d606a2 100644 --- a/app/src/main/java/cash/z/ecc/android/ZcashWalletApp.kt +++ b/app/src/main/java/cash/z/ecc/android/ZcashWalletApp.kt @@ -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) } } /** diff --git a/app/src/main/java/cash/z/ecc/android/di/AppModule.kt b/app/src/main/java/cash/z/ecc/android/di/AppModule.kt index 0bf7315..d1cecfb 100644 --- a/app/src/main/java/cash/z/ecc/android/di/AppModule.kt +++ b/app/src/main/java/cash/z/ecc/android/di/AppModule.kt @@ -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 } diff --git a/app/src/main/java/cash/z/ecc/android/feedback/Report.kt b/app/src/main/java/cash/z/ecc/android/feedback/Report.kt index a9565b8..1cbf4f1 100644 --- a/app/src/main/java/cash/z/ecc/android/feedback/Report.kt +++ b/app/src/main/java/cash/z/ecc/android/feedback/Report.kt @@ -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 Feedback.measure(type: MetricType, block: () -> T) = + this.measure(type.key, type.description, block) \ No newline at end of file diff --git a/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt b/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt index 37b926d..a5ba268 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/MainActivity.kt @@ -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(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() } \ No newline at end of file diff --git a/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt b/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt index b7374f2..1fd4529 100644 --- a/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt +++ b/buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt @@ -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" diff --git a/feedback/src/main/java/cash/z/ecc/android/feedback/Feedback.kt b/feedback/src/main/java/cash/z/ecc/android/feedback/Feedback.kt index e7c19dd..d2a8952 100644 --- a/feedback/src/main/java/cash/z/ecc/android/feedback/Feedback.kt +++ b/feedback/src/main/java/cash/z/ecc/android/feedback/Feedback.kt @@ -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 measure(description: Any = "measurement", block: () -> T) { + inline fun 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 { 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 = 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" + } } } \ No newline at end of file diff --git a/feedback/src/main/java/cash/z/ecc/android/feedback/FeedbackCoordinator.kt b/feedback/src/main/java/cash/z/ecc/android/feedback/FeedbackCoordinator.kt index e77eae2..a2563dc 100644 --- a/feedback/src/main/java/cash/z/ecc/android/feedback/FeedbackCoordinator.kt +++ b/feedback/src/main/java/cash/z/ecc/android/feedback/FeedbackCoordinator.kt @@ -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 = 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 diff --git a/feedback/src/main/java/cash/z/ecc/android/feedback/util/CompositeJob.kt b/feedback/src/main/java/cash/z/ecc/android/feedback/util/CompositeJob.kt index d1e9388..57dddaa 100644 --- a/feedback/src/main/java/cash/z/ecc/android/feedback/util/CompositeJob.kt +++ b/feedback/src/main/java/cash/z/ecc/android/feedback/util/CompositeJob.kt @@ -4,39 +4,39 @@ import kotlinx.coroutines.Job class CompositeJob { - private val jobs = mutableListOf() - val size: Int get() = jobs.size + private val activeJobs = mutableListOf() + 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) {