Summary
The first time the media volume changes while the game runs (a hardware volume key, for example), a handled ensure fires on Android’s main (UI) thread. The game carries on.
The ensure costs a stack walk on Android’s main thread and an error report: about 8 ms in all on our device (the stack walk 3 to 5 ms), once per process, in every build with ensures compiled in.
Engine: 5.8.3 (source build from GitHub, tag commit 396c9f059 “5.8.3 release”)
Platform: Android arm64 (Vulkan), Development build. Seen on a Samsung Galaxy S26 Ultra, Android 17 (API 37). Any Android device should behave the same: the code path is in Core, not device-specific.
Area: Core / Android platform (AndroidPlatformMisc, the Java broadcast receivers)
Cause
Engine/Build/Android/Java/src/com/epicgames/unreal/VolumeReceiver.java,onReceive, runs on Android’s main thread forandroid.media.VOLUME_CHANGED_ACTIONand calls the nativevolumeChanged.Engine/Source/Runtime/Core/Private/Android/AndroidPlatformMisc.cpp:413-420,UE::Jni::FVolumeReceiver::volumeChanged, storesCurrentVolume.TimeOfChange = FApp::GetCurrentTime();(line 418).FApp::GetCurrentTime()isFAppTime::GetCurrentTime()(App.h:567-569), that isFAppTime::Get().CurrentTime.FAppTime::Get(Runtime/Core/Private/Misc/AppTime.cpp:26-46) finds no time context on Android’s main thread: it is not the game thread, not a task with an inherited context, and not the Slate or a loading thread. So it failsensureMsgf(IsInGameThread(), ...)at line 43.- The initial call from
VolumeReceiver.startReceiverruns on another thread and does not fire it; the first change after start does. Later changes do not fire it again (an ensure reports once).
The stored time is returned only by FAndroidMisc::GetVolumeState(double* OutTimeOfChangeInSec), which nothing else in the engine calls. The other receivers in the same file (headset, battery) do not read the app time.
What Type of Bug are you experiencing?
Platform Mobile
Steps to Reproduce
- Package any project for Android, Development.
- Launch it on a device with the media volume below its maximum (at the maximum, volume up changes nothing and Android sends no broadcast).
- Press volume up or down once while the game runs.
- The device log (
adb logcat, tagUE) showsnativeVolumeEvent(n)and, straight after it on the same thread, the ensure (see the log below) and its “=== Handled ensure: ===” report.
Expected Result
The volume change is recorded without an ensure.
Observed Result
A handled ensure (IsInGameThread() at AppTime.cpp:43) with a stack walk and an error report on Android’s main thread, once per process. The read of the game thread’s time from another thread is also unsynchronized.
Affects Versions
5.8
Platform(s)
Android
For crash reports, include your callstack
The ensure, as the device log shows it (tag UE):
[GameActivity] OnReceive VOLUME_CHANGED_ACTION
nativeVolumeEvent(11)
Ensure condition failed: IsInGameThread() [File:./Runtime/Core/Private/Misc/AppTime.cpp] [Line: 43]
Attempted to retrieve FAppTime on a thread where there is no inherited time context. Copies of the game thread's FAppTime are passed automatically through the TaskGraph via FInheritedContextBase / FInheritedContextScope, to ensure the values are pipelined correctly to tasks that may be running concurrently. The current thread or task has not been directly or indirectly spawned from the game thread, so it did not receive a time context.
LogStats: FPlatformStackWalk::StackWalkAndDump - 0.003 s
LogOutputDevice: Error: === Handled ensure: ===
Additional Notes
Suggested fix (one line; not applied or built by us)
Engine/Source/Runtime/Core/Private/Android/AndroidPlatformMisc.cpp, FVolumeReceiver::volumeChanged (line 418)
- CurrentVolume.TimeOfChange = FApp::GetCurrentTime();
+ CurrentVolume.TimeOfChange = FAppTime::GetApproximateCurrentTime();
AppTime.h offers FAppTime::GetApproximateCurrentTime() for exactly this case: it “can be called from any thread”, for “an approximate incrementing timer”. The value is a change timestamp, so a read that is one frame off does not matter. FPlatformTime::Seconds() would also do, if a different time base is acceptable for GetVolumeState’s callers.
Workaround we use
None in the engine. Our device-log check knows this one ensure only when it comes straight after nativeVolumeEvent on Android’s main thread, together with the full report the engine writes for it.