diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/DeviceControlsComponent.kt b/app/src/main/java/com/droiddeck/launcher/runtime/DeviceControlsComponent.kt index f5780da94..cf0d3d7f6 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/DeviceControlsComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/DeviceControlsComponent.kt @@ -1,7 +1,9 @@ package com.droiddeck.launcher.runtime +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager @@ -34,6 +36,9 @@ class DeviceControlsComponent(context: Context, root: File, private val activity private val observer = object : ContentObserver(handler) { override fun onChange(selfChange: Boolean) { if (running) publish() } } + private val volumeSteps = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { if (running) publishVolume() } + } private val requests = object : FileObserver(directory.path, CLOSE_WRITE or MOVED_TO) { override fun onEvent(event: Int, path: String?) { if (path in REQUESTS) handler.post { if (running) consume() } @@ -50,13 +55,22 @@ class DeviceControlsComponent(context: Context, root: File, private val activity } } - fun publish(force: Boolean = false) { + private fun publishVolume() { try { directory.mkdirs() val level = audio.getStreamVolume(AudioManager.STREAM_MUSIC) val maximum = audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC).coerceAtLeast(1) val muted = audio.isStreamMute(AudioManager.STREAM_MUSIC) write("volume.state", "$level $maximum ${if (muted) 1 else 0}\n") + } catch (e: Exception) { + Log.w(TAG, "Could not publish Android volume", e) + } + } + + fun publish(force: Boolean = false) { + publishVolume() + try { + directory.mkdirs() val value = Settings.System.getInt(context.contentResolver, Settings.System.SCREEN_BRIGHTNESS, 128) write("backlight/droiddeck/brightness", "$value\n") write("backlight/droiddeck/actual_brightness", "$value\n") @@ -81,6 +95,10 @@ class DeviceControlsComponent(context: Context, root: File, private val activity permissionNotified = false context.contentResolver.registerContentObserver(Settings.System.CONTENT_URI, true, observer) requests.startWatching() + context.registerReceiver(volumeSteps, IntentFilter().apply { + addAction(VOLUME_CHANGED_ACTION) + addAction(STREAM_MUTE_CHANGED_ACTION) + }, null, handler) refresh.run() } } @@ -90,6 +108,7 @@ class DeviceControlsComponent(context: Context, root: File, private val activity running = false handler.removeCallbacks(refresh) requests.stopWatching() + try { context.unregisterReceiver(volumeSteps) } catch (e: IllegalArgumentException) { } context.contentResolver.unregisterContentObserver(observer) context.getSystemService(NotificationManager::class.java).cancel(PERMISSION_NOTIFICATION) } @@ -170,6 +189,9 @@ class DeviceControlsComponent(context: Context, root: File, private val activity companion object { private const val TAG = "DeviceControls" + // AudioService sends these on every stream step; the Settings observer and 250 ms poll lag a held volume key. + private const val VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION" + private const val STREAM_MUTE_CHANGED_ACTION = "android.media.STREAM_MUTE_CHANGED_ACTION" private const val PERMISSION_CHANNEL = "brightness_permission" private const val PERMISSION_NOTIFICATION = 0x4442 private val REQUESTS = setOf("brightness.request", "volume.request", "mute.request") diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/DeviceControlsComponentTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/DeviceControlsComponentTest.kt index db035a656..fb9a834fa 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/DeviceControlsComponentTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/DeviceControlsComponentTest.kt @@ -66,6 +66,15 @@ class DeviceControlsComponentTest { assertEquals(3, audio.getStreamVolume(AudioManager.STREAM_MUSIC)) } + @Test fun everyVolumeStepIsPublishedWithoutWaitingForThePoll() { + for (level in 2..5) { + audio.setStreamVolume(AudioManager.STREAM_MUSIC, level, 0) + context.sendBroadcast(android.content.Intent("android.media.VOLUME_CHANGED_ACTION")) + shadowOf(Looper.getMainLooper()).idle() + assertEquals(level.toString(), File(directory, "volume.state").readText().split(" ").first()) + } + } + @Test fun deniedBrightnessDoesNotOpenSettingsOrReplayAnOldSliderValueAfterPermission() { WriteSettingsShadow.writable = false request("brightness", "0.75") diff --git a/tools/aaudio-sink/android-volume.h b/tools/aaudio-sink/android-volume.h index 6a31befeb..0e71caf65 100644 --- a/tools/aaudio-sink/android-volume.h +++ b/tools/aaudio-sink/android-volume.h @@ -3,10 +3,15 @@ #include #include +#define ANDROID_VOLUME_IDLE_MS 100 +#define ANDROID_VOLUME_ACTIVE_MS 16 +#define ANDROID_VOLUME_ACTIVE_TICKS 40 + struct android_volume { pa_sink *sink; char *directory; pa_time_event *timer; + unsigned active_ticks; }; static bool android_volume_read(const char *directory, unsigned *level, unsigned *maximum, unsigned *muted) { @@ -57,22 +62,39 @@ static void android_volume_refresh(pa_mainloop_api *api, pa_time_event *event, c char *processing = pa_sprintf_malloc("%s.processing", request); char *mute_processing = pa_sprintf_malloc("%s.processing", mute_request); if (access(request, F_OK) != 0 && access(mute_request, F_OK) != 0 && - access(processing, F_OK) != 0 && access(mute_processing, F_OK) != 0 && - android_volume_read(bridge->directory, &level, &maximum, &muted)) { - pa_cvolume volume; - if (bridge->sink->n_volume_steps != maximum + 1) { - bridge->sink->n_volume_steps = maximum + 1; - pa_subscription_post(bridge->sink->core, PA_SUBSCRIPTION_EVENT_SINK | PA_SUBSCRIPTION_EVENT_CHANGE, bridge->sink->index); + access(processing, F_OK) != 0 && access(mute_processing, F_OK) != 0) { + if (android_volume_read(bridge->directory, &level, &maximum, &muted)) { + pa_cvolume volume; + bool changed = false; + if (bridge->sink->n_volume_steps != maximum + 1) { + bridge->sink->n_volume_steps = maximum + 1; + pa_subscription_post(bridge->sink->core, PA_SUBSCRIPTION_EVENT_SINK | PA_SUBSCRIPTION_EVENT_CHANGE, bridge->sink->index); + } + pa_cvolume_set(&volume, bridge->sink->sample_spec.channels, (uint64_t)level * PA_VOLUME_NORM / maximum); + if (!pa_cvolume_equal(&volume, &bridge->sink->real_volume)) { + pa_sink_volume_changed(bridge->sink, &volume); + changed = true; + } + if (bridge->sink->muted != !!muted) { + pa_sink_mute_changed(bridge->sink, !!muted); + changed = true; + } + if (changed) bridge->active_ticks = ANDROID_VOLUME_ACTIVE_TICKS; } - pa_cvolume_set(&volume, bridge->sink->sample_spec.channels, (uint64_t)level * PA_VOLUME_NORM / maximum); - if (!pa_cvolume_equal(&volume, &bridge->sink->real_volume)) pa_sink_volume_changed(bridge->sink, &volume); - if (bridge->sink->muted != !!muted) pa_sink_mute_changed(bridge->sink, !!muted); + } else { + bridge->active_ticks = ANDROID_VOLUME_ACTIVE_TICKS; } pa_xfree(request); pa_xfree(mute_request); pa_xfree(processing); pa_xfree(mute_processing); - pa_core_rttime_restart(bridge->sink->core, event, pa_rtclock_now() + 100 * PA_USEC_PER_MSEC); + /* A held volume key steps Android every ~50 ms; sampling at 100 ms hands Steam uneven multi-step jumps. */ + unsigned delay_ms = ANDROID_VOLUME_IDLE_MS; + if (bridge->active_ticks) { + bridge->active_ticks--; + delay_ms = ANDROID_VOLUME_ACTIVE_MS; + } + pa_core_rttime_restart(bridge->sink->core, event, pa_rtclock_now() + delay_ms * PA_USEC_PER_MSEC); } static struct android_volume *android_volume_new(pa_sink *sink, const char *directory) { @@ -90,7 +112,7 @@ static struct android_volume *android_volume_new(pa_sink *sink, const char *dire pa_cvolume_reset(&sink->soft_volume, sink->sample_spec.channels); pa_sink_set_set_volume_callback(sink, android_volume_set); pa_sink_set_set_mute_callback(sink, android_volume_mute); - bridge->timer = pa_core_rttime_new(sink->core, pa_rtclock_now() + 100 * PA_USEC_PER_MSEC, + bridge->timer = pa_core_rttime_new(sink->core, pa_rtclock_now() + ANDROID_VOLUME_IDLE_MS * PA_USEC_PER_MSEC, android_volume_refresh, bridge); return bridge; } diff --git a/tools/tests/test_android_volume.py b/tools/tests/test_android_volume.py index 8246f34fe..44d34f9b4 100644 --- a/tools/tests/test_android_volume.py +++ b/tools/tests/test_android_volume.py @@ -44,6 +44,7 @@ typedef struct pa_sink { struct android_volume; struct userdata { struct android_volume *android_volume; }; static int restarts, frees; +static uint64_t last_delay; static char *pa_sprintf_malloc(const char *format, ...) { va_list ap; va_start(ap,format); char *result; assert(vasprintf(&result,format,ap)>=0); va_end(ap); return result; } @@ -57,7 +58,7 @@ static void pa_sink_set_set_volume_callback(pa_sink *sink, void (*callback)(pa_s static void pa_sink_set_set_mute_callback(pa_sink *sink, void (*callback)(pa_sink*)) { sink->set_mute=callback; } static void pa_subscription_post(pa_core *core, int event, unsigned index) {} static uint64_t pa_rtclock_now(void) { return 0; } -static void pa_core_rttime_restart(pa_core *core, pa_time_event *event, uint64_t time) { restarts++; } +static void pa_core_rttime_restart(pa_core *core, pa_time_event *event, uint64_t time) { restarts++; last_delay=time; } static pa_time_event *pa_core_rttime_new(pa_core *core, uint64_t time, void (*callback)(pa_mainloop_api*,pa_time_event*,const struct timeval*,void*), void *userdata) { return calloc(1,sizeof(pa_time_event)); @@ -76,11 +77,18 @@ int main(int argc, char **argv) { assert(userdata.android_volume && userdata.android_volume->timer && sink.set_volume && sink.set_mute); state(argv[1],"invalid\n"); android_volume_refresh(&api,userdata.android_volume->timer,NULL,userdata.android_volume); - assert(sink.real_volume.value==PA_VOLUME_NORM && restarts==1); + assert(sink.real_volume.value==PA_VOLUME_NORM && restarts==1 && last_delay==100*PA_USEC_PER_MSEC); state(argv[1],"3 15 1\n"); android_volume_refresh(&api,userdata.android_volume->timer,NULL,userdata.android_volume); assert(sink.real_volume.value==3*PA_VOLUME_NORM/15 && sink.muted && sink.n_volume_steps==16); assert(sink.soft_volume.value==PA_VOLUME_NORM && restarts==2); + assert(last_delay==16*PA_USEC_PER_MSEC); + state(argv[1],"4 15 1\n"); + android_volume_refresh(&api,userdata.android_volume->timer,NULL,userdata.android_volume); + assert(sink.real_volume.value==4*PA_VOLUME_NORM/15 && last_delay==16*PA_USEC_PER_MSEC); + for (int i = 0; i < ANDROID_VOLUME_ACTIVE_TICKS + 1; i++) + android_volume_refresh(&api,userdata.android_volume->timer,NULL,userdata.android_volume); + assert(last_delay==100*PA_USEC_PER_MSEC); sink.real_volume.value=2*PA_VOLUME_NORM/3; sink.set_volume(&sink); char *request=pa_sprintf_malloc("%s/volume.request",argv[1]); FILE *f=fopen(request,"r"); unsigned level; assert(f && fscanf(f,"%u",&level)==1 && level==10); fclose(f); free(request);