Android: pressing a volume key fails the ensure in FAppTime::Get ("no inherited time context"), because VolumeReceiver.volumeChanged reads FApp::GetCurrentTime() on Android's main thread

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

  1. Engine/Build/Android/Java/src/com/epicgames/unreal/VolumeReceiver.java, onReceive, runs on Android’s main thread for android.media.VOLUME_CHANGED_ACTION and calls the native volumeChanged.
  2. Engine/Source/Runtime/Core/Private/Android/AndroidPlatformMisc.cpp:413-420, UE::Jni::FVolumeReceiver::volumeChanged, stores CurrentVolume.TimeOfChange = FApp::GetCurrentTime(); (line 418).
  3. FApp::GetCurrentTime() is FAppTime::GetCurrentTime() (App.h:567-569), that is FAppTime::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 fails ensureMsgf(IsInGameThread(), ...) at line 43.
  4. The initial call from VolumeReceiver.startReceiver runs 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

  1. Package any project for Android, Development.
  2. Launch it on a device with the media volume below its maximum (at the maximum, volume up changes nothing and Android sends no broadcast).
  3. Press volume up or down once while the game runs.
  4. The device log (adb logcat, tag UE) shows nativeVolumeEvent(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.