From e23dc0b4177c40064e4750946bafa507868d976e Mon Sep 17 00:00:00 2001 From: Paul Fisher Date: Mon, 19 Jan 2026 20:29:18 -0500 Subject: [PATCH 1/2] Replace MetronomeListenerAdapter with default interface implementations. --- .../patrick/tack/fragment/SongsFragment.java | 3 +- .../tack/metronome/MetronomeEngine.java | 62 +++++++++++-------- .../tack/service/MetronomeService.java | 4 +- .../tack/util/dialog/LatencyDialogUtil.java | 3 +- .../patrick/tack/activity/MainActivity.kt | 2 +- .../patrick/tack/metronome/MetronomeEngine.kt | 21 ++----- .../patrick/tack/service/MetronomeService.kt | 4 +- 7 files changed, 48 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/xyz/zedler/patrick/tack/fragment/SongsFragment.java b/app/src/main/java/xyz/zedler/patrick/tack/fragment/SongsFragment.java index 611177bd..6ce2ec50 100644 --- a/app/src/main/java/xyz/zedler/patrick/tack/fragment/SongsFragment.java +++ b/app/src/main/java/xyz/zedler/patrick/tack/fragment/SongsFragment.java @@ -51,7 +51,6 @@ import xyz.zedler.patrick.tack.fragment.SongsFragmentDirections.ActionSongsToSong; import xyz.zedler.patrick.tack.metronome.MetronomeEngine; import xyz.zedler.patrick.tack.metronome.MetronomeEngine.MetronomeListener; -import xyz.zedler.patrick.tack.metronome.MetronomeEngine.MetronomeListenerAdapter; import xyz.zedler.patrick.tack.recyclerview.adapter.SongAdapter; import xyz.zedler.patrick.tack.recyclerview.adapter.SongAdapter.OnSongClickListener; import xyz.zedler.patrick.tack.recyclerview.layoutmanager.WrapperLinearLayoutManager; @@ -212,7 +211,7 @@ public void onTopScroll() { ViewUtil.setTooltipText(binding.buttonSongsBack, R.string.action_back); ViewUtil.setTooltipText(binding.buttonSongsMenu, R.string.action_more); - metronomeListener = new MetronomeListenerAdapter() { + metronomeListener = new MetronomeListener() { @Override public void onMetronomeStart() { activity.runOnUiThread(() -> adapter.setPlaying(true)); diff --git a/app/src/main/java/xyz/zedler/patrick/tack/metronome/MetronomeEngine.java b/app/src/main/java/xyz/zedler/patrick/tack/metronome/MetronomeEngine.java index f7d14fd9..5aec5857 100644 --- a/app/src/main/java/xyz/zedler/patrick/tack/metronome/MetronomeEngine.java +++ b/app/src/main/java/xyz/zedler/patrick/tack/metronome/MetronomeEngine.java @@ -1571,33 +1571,41 @@ private String getCurrentTickTypePoly() { } public interface MetronomeListener { - void onMetronomeStart(); - void onMetronomeStop(); - void onMetronomePreTick(Tick tick); - void onMetronomeTick(Tick tick); - void onMetronomeTempoChanged(int tempoOld, int tempoNew); - void onMetronomeElapsedTimeSecondsChanged(); - void onMetronomeTimerStarted(); - void onMetronomeTimerSecondsChanged(); - void onMetronomeTimerProgressOneTime(boolean withTransition); - void onMetronomeConfigChanged(); - void onMetronomeSongOrPartChanged(@Nullable SongWithParts song, int partIndex); - void onMetronomePermissionMissing(); - } - - public static class MetronomeListenerAdapter implements MetronomeListener { - public void onMetronomeStart() {} - public void onMetronomeStop() {} - public void onMetronomePreTick(Tick tick) {} - public void onMetronomeTick(Tick tick) {} - public void onMetronomeTempoChanged(int tempoOld, int tempoNew) {} - public void onMetronomeElapsedTimeSecondsChanged() {} - public void onMetronomeTimerStarted() {} - public void onMetronomeTimerSecondsChanged() {} - public void onMetronomeTimerProgressOneTime(boolean withTransition) {} - public void onMetronomeConfigChanged() {} - public void onMetronomeSongOrPartChanged(@Nullable SongWithParts song, int partIndex) {} - public void onMetronomePermissionMissing() {} + default void onMetronomeStart() { + } + + default void onMetronomeStop() { + } + + default void onMetronomePreTick(Tick tick) { + } + + default void onMetronomeTick(Tick tick) { + } + + default void onMetronomeTempoChanged(int tempoOld, int tempoNew) { + } + + default void onMetronomeElapsedTimeSecondsChanged() { + } + + default void onMetronomeTimerStarted() { + } + + default void onMetronomeTimerSecondsChanged() { + } + + default void onMetronomeTimerProgressOneTime(boolean withTransition) { + } + + default void onMetronomeConfigChanged() { + } + + default void onMetronomeSongOrPartChanged(@Nullable SongWithParts song, int partIndex) { + } + + default void onMetronomePermissionMissing() { + } } public static class Tick { diff --git a/app/src/main/java/xyz/zedler/patrick/tack/service/MetronomeService.java b/app/src/main/java/xyz/zedler/patrick/tack/service/MetronomeService.java index 5d643670..a0b6942b 100644 --- a/app/src/main/java/xyz/zedler/patrick/tack/service/MetronomeService.java +++ b/app/src/main/java/xyz/zedler/patrick/tack/service/MetronomeService.java @@ -38,7 +38,7 @@ import xyz.zedler.patrick.tack.Constants.EXTRA; import xyz.zedler.patrick.tack.Constants.PREF; import xyz.zedler.patrick.tack.metronome.MetronomeEngine; -import xyz.zedler.patrick.tack.metronome.MetronomeEngine.MetronomeListenerAdapter; +import xyz.zedler.patrick.tack.metronome.MetronomeEngine.MetronomeListener; import xyz.zedler.patrick.tack.util.NotificationUtil; import xyz.zedler.patrick.tack.util.PrefsUtil; @@ -58,7 +58,7 @@ public void onCreate() { notificationUtil = new NotificationUtil(this); metronomeEngine = new MetronomeEngine(this); - metronomeEngine.addListener(new MetronomeListenerAdapter() { + metronomeEngine.addListener(new MetronomeListener() { @Override public void onMetronomeStart() { if (permNotification && hasPermission()) { diff --git a/app/src/main/java/xyz/zedler/patrick/tack/util/dialog/LatencyDialogUtil.java b/app/src/main/java/xyz/zedler/patrick/tack/util/dialog/LatencyDialogUtil.java index 7d78596c..e303c50c 100644 --- a/app/src/main/java/xyz/zedler/patrick/tack/util/dialog/LatencyDialogUtil.java +++ b/app/src/main/java/xyz/zedler/patrick/tack/util/dialog/LatencyDialogUtil.java @@ -33,7 +33,6 @@ import xyz.zedler.patrick.tack.fragment.SettingsFragment; import xyz.zedler.patrick.tack.metronome.MetronomeEngine; import xyz.zedler.patrick.tack.metronome.MetronomeEngine.MetronomeListener; -import xyz.zedler.patrick.tack.metronome.MetronomeEngine.MetronomeListenerAdapter; import xyz.zedler.patrick.tack.metronome.MetronomeEngine.Tick; import xyz.zedler.patrick.tack.util.DialogUtil; import xyz.zedler.patrick.tack.util.ResUtil; @@ -69,7 +68,7 @@ public LatencyDialogUtil(MainActivity activity, SettingsFragment fragment) { colorBg = ResUtil.getColor(activity, R.attr.colorSurfaceBright); colorBgFlash = ResUtil.getColor(activity, R.attr.colorTertiaryContainer); - latencyListener = new MetronomeListenerAdapter() { + latencyListener = new MetronomeListener() { @Override public void onMetronomeTick(Tick tick) { activity.runOnUiThread(() -> { diff --git a/wear/src/main/kotlin/xyz/zedler/patrick/tack/activity/MainActivity.kt b/wear/src/main/kotlin/xyz/zedler/patrick/tack/activity/MainActivity.kt index 288cba3d..b6a58061 100644 --- a/wear/src/main/kotlin/xyz/zedler/patrick/tack/activity/MainActivity.kt +++ b/wear/src/main/kotlin/xyz/zedler/patrick/tack/activity/MainActivity.kt @@ -66,7 +66,7 @@ class MainActivity : ComponentActivity(), ServiceConnection { setTheme(android.R.style.Theme_DeviceDefault) metronomeEngine = MetronomeEngine(this, false) - metronomeEngine.addListener(object : MetronomeEngine.MetronomeListenerAdapter() { + metronomeEngine.addListener(object : MetronomeEngine.MetronomeListener { override fun onMetronomePreTick(tick: MetronomeEngine.Tick) { runOnUiThread { viewModel.onPreTick(tick) diff --git a/wear/src/main/kotlin/xyz/zedler/patrick/tack/metronome/MetronomeEngine.kt b/wear/src/main/kotlin/xyz/zedler/patrick/tack/metronome/MetronomeEngine.kt index bef1105f..ec5dd95f 100644 --- a/wear/src/main/kotlin/xyz/zedler/patrick/tack/metronome/MetronomeEngine.kt +++ b/wear/src/main/kotlin/xyz/zedler/patrick/tack/metronome/MetronomeEngine.kt @@ -307,21 +307,12 @@ class MetronomeEngine( } interface MetronomeListener { - fun onMetronomeStart() - fun onMetronomeStop() - fun onMetronomePreTick(tick: Tick) - fun onMetronomeTick(tick: Tick) - fun onFlashScreenEnd() - fun onPermissionMissing() - } - - open class MetronomeListenerAdapter : MetronomeListener { - override fun onMetronomeStart() {} - override fun onMetronomeStop() {} - override fun onMetronomePreTick(tick: Tick) {} - override fun onMetronomeTick(tick: Tick) {} - override fun onFlashScreenEnd() {} - override fun onPermissionMissing() {} + fun onMetronomeStart() {} + fun onMetronomeStop() {} + fun onMetronomePreTick(tick: Tick) {} + fun onMetronomeTick(tick: Tick) {} + fun onFlashScreenEnd() {} + fun onPermissionMissing() {} } data class Tick( diff --git a/wear/src/main/kotlin/xyz/zedler/patrick/tack/service/MetronomeService.kt b/wear/src/main/kotlin/xyz/zedler/patrick/tack/service/MetronomeService.kt index 01bc15df..611cffce 100644 --- a/wear/src/main/kotlin/xyz/zedler/patrick/tack/service/MetronomeService.kt +++ b/wear/src/main/kotlin/xyz/zedler/patrick/tack/service/MetronomeService.kt @@ -29,7 +29,7 @@ import android.util.Log import androidx.lifecycle.LifecycleService import xyz.zedler.patrick.tack.Constants.Action import xyz.zedler.patrick.tack.metronome.MetronomeEngine -import xyz.zedler.patrick.tack.metronome.MetronomeEngine.MetronomeListenerAdapter +import xyz.zedler.patrick.tack.metronome.MetronomeEngine.MetronomeListener import xyz.zedler.patrick.tack.util.NotificationUtil class MetronomeService : LifecycleService() { @@ -48,7 +48,7 @@ class MetronomeService : LifecycleService() { super.onCreate() metronomeEngine = MetronomeEngine(this, true) - metronomeEngine.addListener(object : MetronomeListenerAdapter() { + metronomeEngine.addListener(object : MetronomeListener { override fun onMetronomeStop() { stopForeground() } From 470baae3c0e3aab5e6f680d908ebe27282699f20 Mon Sep 17 00:00:00 2001 From: Paul Fisher Date: Mon, 19 Jan 2026 20:36:18 -0500 Subject: [PATCH 2/2] Use AutoValue to implement Tick class. AutoValue is essentially Kotlin's data classes, but in annotation form, so you can use them in regular Java source. This change adds it as a dep and uses it to implement Tick. --- app/build.gradle | 2 + .../patrick/tack/fragment/MainFragment.java | 14 ++-- .../patrick/tack/metronome/AudioEngine.java | 4 +- .../tack/metronome/MetronomeEngine.java | 66 ++++++++----------- gradle/libs.versions.toml | 3 + 5 files changed, 42 insertions(+), 47 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2e3a80ad..994cc004 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,6 +109,7 @@ android { } dependencies { + annotationProcessor libs.auto.value implementation(libs.appcompat) implementation(libs.fragment) implementation(libs.navigation.fragment) @@ -122,4 +123,5 @@ dependencies { implementation(libs.gson) implementation(libs.room.runtime) annotationProcessor(libs.room.compiler) + compileOnly libs.auto.value.annotations } \ No newline at end of file diff --git a/app/src/main/java/xyz/zedler/patrick/tack/fragment/MainFragment.java b/app/src/main/java/xyz/zedler/patrick/tack/fragment/MainFragment.java index 0799f9e6..2bd558c2 100644 --- a/app/src/main/java/xyz/zedler/patrick/tack/fragment/MainFragment.java +++ b/app/src/main/java/xyz/zedler/patrick/tack/fragment/MainFragment.java @@ -944,8 +944,8 @@ public void onMetronomePreTick(Tick tick) { if (binding == null || getMetronomeEngine() == null) { return; } - View beat = binding.linearMainBeats.getChildAt(tick.beat - 1); - if (beat instanceof BeatView && tick.subdivision == 1 && !tick.isPoly) { + View beat = binding.linearMainBeats.getChildAt(tick.beat() - 1); + if (beat instanceof BeatView && tick.subdivision() == 1 && !tick.isPoly()) { resetActiveBeats(); ViewUtil.scrollToViewMinimal(binding.scrollHorizMainBeats, beat); if (activeBeat) { @@ -953,10 +953,10 @@ public void onMetronomePreTick(Tick tick) { } ((BeatView) beat).beat(); } - View subdivision = binding.linearMainSubs.getChildAt(tick.subdivision - 1); + View subdivision = binding.linearMainSubs.getChildAt(tick.subdivision() - 1); if (!(subdivision instanceof BeatView)) { return; - } else if (getMetronomeEngine().getConfig().usePolyrhythm() && !tick.isPoly) { + } else if (getMetronomeEngine().getConfig().usePolyrhythm() && !tick.isPoly()) { return; } ViewUtil.scrollToViewMinimal(binding.scrollHorizMainSubs, subdivision); @@ -972,7 +972,7 @@ public void onMetronomeTick(Tick tick) { } if (flashScreen) { int color; - switch (tick.type) { + switch (tick.type()) { case TICK_TYPE.STRONG: color = colorFlashStrong; break; @@ -984,7 +984,7 @@ public void onMetronomeTick(Tick tick) { color = colorFlashNormal; break; } - if (tick.isMuted) { + if (tick.isMuted()) { color = colorFlashMuted; } if (isLandTablet && binding.cardMainContainerEnd != null) { @@ -1003,7 +1003,7 @@ public void onMetronomeTick(Tick tick) { }, 100); // flash screen for 100 milliseconds } } - if (tick.subdivision == 1) { + if (tick.subdivision() == 1) { if (!reduceAnimations) { logoUtil.nextBeat(getMetronomeEngine().getInterval()); } diff --git a/app/src/main/java/xyz/zedler/patrick/tack/metronome/AudioEngine.java b/app/src/main/java/xyz/zedler/patrick/tack/metronome/AudioEngine.java index 79234d0b..229c189f 100644 --- a/app/src/main/java/xyz/zedler/patrick/tack/metronome/AudioEngine.java +++ b/app/src/main/java/xyz/zedler/patrick/tack/metronome/AudioEngine.java @@ -246,12 +246,12 @@ public boolean getIgnoreFocus() { } public void playTick(Tick tick) { - if (!playing || !isInitialized() || muted || tick.isMuted) { + if (!playing || !isInitialized() || muted || tick.isMuted()) { return; } int nativeTickType; - switch (tick.type) { + switch (tick.type()) { case TICK_TYPE.STRONG: nativeTickType = NATIVE_TICK_TYPE_STRONG; break; diff --git a/app/src/main/java/xyz/zedler/patrick/tack/metronome/MetronomeEngine.java b/app/src/main/java/xyz/zedler/patrick/tack/metronome/MetronomeEngine.java index 5aec5857..bc5a24a7 100644 --- a/app/src/main/java/xyz/zedler/patrick/tack/metronome/MetronomeEngine.java +++ b/app/src/main/java/xyz/zedler/patrick/tack/metronome/MetronomeEngine.java @@ -25,9 +25,13 @@ import android.os.Handler; import android.os.HandlerThread; import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; + +import com.google.auto.value.AutoValue; + import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; @@ -40,6 +44,7 @@ import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; + import xyz.zedler.patrick.tack.Constants; import xyz.zedler.patrick.tack.Constants.BEAT_MODE; import xyz.zedler.patrick.tack.Constants.DEF; @@ -577,7 +582,7 @@ public void run() { if (config.isMuteActive() && config.getMuteUnit().equals(UNIT.BEATS)) { muted = random.nextInt(100) < config.getMuteMute(); } - Tick tick = new Tick( + Tick tick = Tick.create( tickIndexPoly, 1, subdivisionPoly, tickTypePoly, muted, true ); @@ -626,13 +631,13 @@ public void run() { if (config.isMuteActive() && config.getMuteUnit().equals(UNIT.BEATS)) { muted = random.nextInt(100) < config.getMuteMute(); } - Tick tick = new Tick(tickIndex, beat, subdivision, tickType, muted, false); + Tick tick = Tick.create(tickIndex, beat, subdivision, tickType, muted, false); long interval = config.usePolyrhythm() ? getInterval() : getInterval() / config.getSubdivisionsCount(); tickHandler.postDelayed(this, interval); - if (tick.beat == 1 && config.usePolyrhythm()) { + if (tick.beat() == 1 && config.usePolyrhythm()) { // start polyrhythm subdivisions every new bar tickHandler.post(tickRunnablePoly); } @@ -1371,7 +1376,7 @@ private boolean performTick(Tick tick) { long barIndexWithoutCountIn = barIndex - config.getCountIn(); boolean isCountIn = barIndex < config.getCountIn(); - boolean isBeat = tick.subdivision == 1; + boolean isBeat = tick.subdivision() == 1; boolean isFirstBeat = isBeat && (beatIndex % config.getBeatsCount()) == 0; if (config.isTimerActive() && config.getTimerUnit().equals(UNIT.BARS) && !isCountIn) { @@ -1456,8 +1461,8 @@ private boolean performTick(Tick tick) { } }, Math.max(0, latency - Constants.BEAT_ANIM_OFFSET)); latencyHandler.postDelayed(() -> { - if (!beatMode.equals(BEAT_MODE.SOUND) && !tick.isMuted) { - switch (tick.type) { + if (!beatMode.equals(BEAT_MODE.SOUND) && !tick.isMuted()) { + switch (tick.type()) { case TICK_TYPE.STRONG: hapticUtil.heavyClick(); break; @@ -1493,13 +1498,13 @@ private void performTickPoly(Tick tick) { boolean shouldVibrate = !beatMode.equals(BEAT_MODE.SOUND) && !isMuted; if (shouldVibrate) { // check whether any poly subdivision collides with a beat - long product = (long) (tick.subdivision - 1) * config.getBeatsCount(); + long product = (long) (tick.subdivision() - 1) * config.getBeatsCount(); if (product % config.getSubdivisionsCount() == 0) { shouldVibrate = false; } } if (shouldVibrate) { - switch (tick.type) { + switch (tick.type()) { case TICK_TYPE.STRONG: hapticUtil.heavyClick(); break; @@ -1608,38 +1613,23 @@ default void onMetronomePermissionMissing() { } } - public static class Tick { - public final long index; - public final int beat, subdivision; - @NonNull - public final String type; - public final boolean isMuted, isPoly; - - public Tick( - long index, - int beat, - int subdivision, - @NonNull String type, - boolean isMuted, - boolean isPoly - ) { - this.index = index; - this.beat = beat; - this.subdivision = subdivision; - this.type = type; - this.isMuted = isMuted; - this.isPoly = isPoly; + @AutoValue + public static abstract class Tick { + static Tick create(long index, int beat, int subdivision, @NonNull String type, boolean isMuted, boolean isPoly) { + return new AutoValue_MetronomeEngine_Tick(index, beat, subdivision, type, isMuted, isPoly); } + public abstract long index(); + + public abstract int beat(); + + public abstract int subdivision(); + @NonNull - @Override - public String toString() { - return "Tick{index = " + index + - ", beat=" + beat + - ", sub=" + subdivision + - ", type=" + type + - ", isPoly=" + isPoly + - ", muted=" + isMuted + '}'; - } + public abstract String type(); + + public abstract boolean isMuted(); + + public abstract boolean isPoly(); } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cfcee0a8..d4282b33 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,7 @@ compose-navigation = "1.5.6" compose-foundation = "1.5.6" compose-activity = "1.12.2" compose-constraintlayout = "1.1.1" +auto-value = "1.11.1" [libraries] appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } @@ -60,6 +61,8 @@ compose-animation-graphics = { group = "androidx.compose.animation", name = "ani compose-navigation = { group = "androidx.wear.compose", name = "compose-navigation", version.ref = "compose-navigation" } compose-activity = { group = "androidx.activity", name = "activity-compose", version.ref = "compose-activity" } compose-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "compose-constraintlayout" } +auto-value-annotations = { group = "com.google.auto.value", name = "auto-value-annotations", version.ref = "auto-value" } +auto-value = { group = "com.google.auto.value", name = "auto-value", version.ref = "auto-value" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }