mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 03:21:08 +00:00
fix: show every held volume step in Steam's volume slider (#1704)
This commit is contained in:
4 files changed
+75
-14
No files matched your search
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
#include <pulsecore/core-rtclock.h>
|
||||
#include <pulsecore/core-subscribe.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user