@@ -184,6 +184,23 @@ public void onServiceDisconnected(ComponentName componentName) {
184184 private GlyphRenderer mGlyphRenderer ;
185185 private AudioDeviceManager mAudioDeviceManager ;
186186 private long mLastSendMs = 0L ;
187+ private long mLastAudioActivityMs = 0L ;
188+ private final Handler mMainHandler = new Handler (android .os .Looper .getMainLooper ());
189+ private final Runnable mIdlePulseRunnable = new Runnable () {
190+ @ Override
191+ public void run () {
192+ if (mIdleBreathingEnabled && mSessionOpen && mVisualizerConfig != null ) {
193+ long now = SystemClock .elapsedRealtime ();
194+ // If it's been more than 100ms since the last audio frame, manually trigger a frame for breathing
195+ if (now - mLastAudioActivityMs > 100 ) {
196+ processFrame (new float [0 ], 0f , mVisualizerConfig , mPresetConfigVersion );
197+ }
198+ }
199+ if (sIsRunning ) {
200+ mMainHandler .postDelayed (this , 33 ); // ~30fps for idle breathing
201+ }
202+ }
203+ };
187204
188205 private final AudioDeviceCallback mAudioDeviceCallback = new AudioDeviceCallback () {
189206 @ Override
@@ -243,16 +260,19 @@ public void onCreate() {
243260
244261 mHapticEngine = new ContinuousHapticEngine (this );
245262 mAudioProcessor = new AudioProcessor ();
246- mGlyphRenderer = new GlyphRenderer (mGamma , mIdleBreathingEnabled , mNotificationFlashEnabled );
247263 mAudioDeviceManager = new AudioDeviceManager (this , this ::refreshLatencyForCurrentAudioRoute );
248264
249265 mSelectedDevice = DeviceProfile .detectDevice ();
266+ mGlyphRenderer = new GlyphRenderer (mGamma , mIdleBreathingEnabled , mNotificationFlashEnabled , mSelectedDevice );
250267 mLatencyCompensationMs = loadLatencyCompensationMs (this , mSelectedDevice );
251268 mGamma = loadGamma (this );
252269
253270 SharedPreferences appPrefs = getSharedPreferences (APP_PREFS_NAME , MODE_PRIVATE );
254271 mIdleBreathingEnabled = appPrefs .getBoolean ("idle_breathing_enabled" , false );
255272 mNotificationFlashEnabled = appPrefs .getBoolean ("notification_flash_enabled" , false );
273+
274+ String idlePattern = appPrefs .getString ("idle_pattern" , "pulse" );
275+ mGlyphRenderer .setIdlePattern (idlePattern );
256276
257277 refreshLatencyForCurrentAudioRoute ();
258278
@@ -270,6 +290,8 @@ public void onCreate() {
270290
271291 mGM = GlyphManager .getInstance (getApplicationContext ());
272292 mGM .init (mGlyphCallback );
293+
294+ mMainHandler .post (mIdlePulseRunnable );
273295 }
274296
275297 @ Override
@@ -455,6 +477,9 @@ public void reloadConfig() {
455477
456478 public void setDevice (int device ) {
457479 mSelectedDevice = device ;
480+ if (mGlyphRenderer != null ) {
481+ mGlyphRenderer .setDeviceType (device );
482+ }
458483 setLatencyCompensationMs (loadLatencyCompensationMs (this , device ));
459484 try {
460485 refreshPresetCatalog ();
@@ -486,6 +511,12 @@ public void setIdleBreathingEnabled(boolean enabled) {
486511 mGlyphRenderer .setIdleBreathingEnabled (enabled );
487512 }
488513
514+ public void setIdlePattern (String pattern ) {
515+ if (mGlyphRenderer != null ) {
516+ mGlyphRenderer .setIdlePattern (pattern );
517+ }
518+ }
519+
489520 public void setNotificationFlashEnabled (boolean enabled ) {
490521 mNotificationFlashEnabled = enabled ;
491522 }
@@ -766,6 +797,10 @@ private void processFrame(float[] uniquePeaks, float hapticPeak, AudioProcessor.
766797 mGlyphRenderer .triggerNotificationFlash (now );
767798 }
768799
800+ if (uniquePeaks .length > 0 ) {
801+ mLastAudioActivityMs = now ;
802+ }
803+
769804 int [] frameColors = mGlyphRenderer .processFrame (uniquePeaks , config , now );
770805 if (frameColors == null ) {
771806 return ; // No change
0 commit comments