mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-18 02:01:25 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1d099b14a |
No files matched your search
@@ -949,6 +949,12 @@ Android 11. `frame.vivox_audio_route` (analysis `vivox_api31`, ANALYSIS_VERSION
|
||||
once (`Dex.return_early`: return-void / `const/4 v0,0; return v0`; nopping the invoke would leave a move-result the
|
||||
verifier rejects). Older Vivox (Eleven Table Tennis, BattleSisters) lacks that code and isn't matched. Verified
|
||||
headless: Vivox initialises, 150 s at ~65-70 fps.
|
||||
Photon Voice does the same (GitHub #362 Table Troopers, #325): `com.exitgames.photon.audioinaec.AudioInAEC.
|
||||
updateAudioDevice` (void; only caller of getAvailableCommunicationDevices/setCommunicationDevice, reached from the mic
|
||||
thread `AudioInAEC$1.run` via access$000; no SDK_INT check left) → sibling patch `frame.photon_voice_audio_route`
|
||||
(analysis `photon_voice_api31`, ANALYSIS_VERSION 20; shares `vivox_audio_route.patch_class`; Vivox's id, title and
|
||||
output unchanged, no revision bump). Triage: `vivox-api31` / `photon-voice-api31` now need the SDK's class in the
|
||||
stack; any other `*CommunicationDevice*` NoSuchMethodError = `audio-api31` (report). Host-tested on the real dex only.
|
||||
**Steam Input gamepad for 2D apps (GitHub #162, agent v72, 2026-10-10):** Lepton's Android only has the Wayland seat's `wayland_touch/keyboard/pointer`. Steam makes its virtual pad (uinput, `/devices/virtual/input`, 28de:11ff, "Microsoft X-Box 360 pad N", ACL for steamos) only while the Frame's controllers are on (controller.txt "Steam Controller reserving XInput slot 0"; gone when they sleep), also with no game running; steamos-manager's 28de:0000 keys device isn't a pad (no BTN_SOUTH). Lepton's container root (system_server too) = steamos (`keep-id:uid=0`), so a bind-mounted `/dev/input/eventN` opens. Opt-in `device.steam_gamepad` (vr_kind none; suggested for `android.hardware.gamepad`/`LEANBACK_LAUNCHER`, analysis `gamepad`, ANALYSIS_VERSION 9): deployment.json `steam_gamepad` → launcher `GAMEPAD_LINE` (after the codec line: exports FRAMEPORT_GAMEPAD + LEPTON_ENV_SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD, puts `agent/bin` first on PATH) → `PODMAN_WRAPPER` (written by ensure_host_fixes; non-`run` calls go straight on; `run` imports the agent's `podman_run_args`; hands on to the next Podman after its own PATH entry with itself removed from PATH, so it chains to the codec wrapper either way round) adds the pads + `Vendor_28de_Product_<pid>.kl` (Xbox 360 layout). Verified headless with a uinput stand-in pad (Stremio's container: EventHub `classes=0x80000141`, our key layout, KeyEvent BUTTON_A dispatched); with Steam's real pad and a game: not yet. Pads made after the start aren't seen until the next one.
|
||||
**Multiview programs on flat framebuffers (GitHub #77, Doom3Quest, 2026-10-09, prototype):** Mesa enforces OVR_multiview's
|
||||
"program num_views == draw framebuffer views" rule (`draw_validate.c`, the draw is dropped silently); Qualcomm doesn't.
|
||||
|
||||
+13
-1
@@ -360,10 +360,22 @@ signatures:
|
||||
suggest: [frame.slz_vulkan_hooks]
|
||||
supersedes: [java-crash, native-crash]
|
||||
- id: vivox-api31
|
||||
pattern: 'NoSuchMethodError: No virtual method \w*CommunicationDevice\w*\(.*Landroid/media/AudioManager'
|
||||
pattern: 'NoSuchMethodError: No virtual method \w*CommunicationDevice\w*\(.*Landroid/media/AudioManager[^\n]*(?:\n[^\n]*\sat [^\n]*){0,12}?\n[^\n]*\sat com\.vivox\.'
|
||||
severity: fatal
|
||||
diagnosis: The game's Vivox voice-chat SDK calls Android 12 audio-routing methods (AudioManager communication devices) without checking the Android version; Lepton runs Android 11, so the app crashes (for example Green Hell VR). The patch makes Vivox's audio-route check return early (voice chat keeps the default route).
|
||||
suggest: [frame.vivox_audio_route]
|
||||
supersedes: [java-crash, audio-api31]
|
||||
- id: photon-voice-api31
|
||||
pattern: 'NoSuchMethodError: No virtual method \w*CommunicationDevice\w*\(.*Landroid/media/AudioManager[^\n]*(?:\n[^\n]*\sat [^\n]*){0,12}?\n[^\n]*\sat com\.exitgames\.photon\.'
|
||||
severity: fatal
|
||||
diagnosis: The game's Photon Voice microphone plugin calls Android 12 audio-routing methods (AudioManager communication devices) without checking the Android version; Lepton runs Android 11, so the app crashes when voice chat starts (for example Table Troopers at the tutorial). The patch makes Photon's audio-device pick return early (the microphone keeps the default route).
|
||||
suggest: [frame.photon_voice_audio_route]
|
||||
supersedes: [java-crash, audio-api31]
|
||||
- id: audio-api31
|
||||
pattern: 'NoSuchMethodError: No virtual method \w*CommunicationDevice\w*\(.*Landroid/media/AudioManager'
|
||||
severity: fatal
|
||||
diagnosis: Code in the game (often a voice-chat SDK) calls Android 12 audio-routing methods (AudioManager communication devices) without checking the Android version; Lepton runs Android 11, so the app crashes. FramePort has fixes for Vivox and Photon Voice; this one comes from another library, so please report it with the log.
|
||||
report: true
|
||||
supersedes: [java-crash]
|
||||
- id: lepton-data-link-failed
|
||||
pattern: "ln: cannot create symbolic link from '[^']*' to '/data/data/|Couldn't create directory for SharedPreferences file /data/user/0/"
|
||||
|
||||
+16
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
# Patches
|
||||
|
||||
Every patch and game setting FramePort knows: 169 entries in 5 groups. A game's recipe is its chosen patches and settings; tested games get theirs from the catalog ([GAMES.md](GAMES.md)), and other games get the patches FramePort suggests from its analysis. You change them on the game page under Customize.
|
||||
Every patch and game setting FramePort knows: 170 entries in 5 groups. A game's recipe is its chosen patches and settings; tested games get theirs from the catalog ([GAMES.md](GAMES.md)), and other games get the patches FramePort suggests from its analysis. You change them on the game page under Customize.
|
||||
|
||||
Each entry says what the patch does, whether it changes the APK or only the install, when FramePort suggests it, its options, the catalog games whose recipe names it and the launch-test signatures that suggest it. Patches that are on by default are on for every game, so the catalog rarely names them.
|
||||
|
||||
@@ -11,7 +11,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
## Groups
|
||||
|
||||
- [Patches from OVRPort](#group-overport) (25)
|
||||
- [Steam Frame patches](#group-frame) (55)
|
||||
- [Steam Frame patches](#group-frame) (56)
|
||||
- [Game settings](#group-adapter) (68)
|
||||
- [Files and settings on the Frame](#group-device) (8)
|
||||
- [PC VR (Proton and Revive)](#group-pcvr) (13)
|
||||
@@ -71,6 +71,7 @@ Link to an entry with its id, dots replaced by dashes: `PATCHES.md#frame-unity_t
|
||||
| [Platform compat (ovrMessageType_ToString)](#frame-ovrplatformcompat) | `frame.ovrplatformcompat` | Steam Frame patches | default |
|
||||
| [Stub missing Meta platform functions](#frame-ovrstubs) | `frame.ovrstubs` | Steam Frame patches | default |
|
||||
| [Unpaired pointer-authentication checks](#frame-pac_hints) | `frame.pac_hints` | Steam Frame patches | 1 |
|
||||
| [Photon Voice: skip Android 12 audio routing](#frame-photon_voice_audio_route) | `frame.photon_voice_audio_route` | Steam Frame patches | |
|
||||
| [SDL apps: start without a clipboard service](#frame-sdl_clipboard) | `frame.sdl_clipboard` | Steam Frame patches | 5 |
|
||||
| [Stress Level Zero: let Unity start Vulkan itself](#frame-slz_vulkan_hooks) | `frame.slz_vulkan_hooks` | Steam Frame patches | 1 |
|
||||
| [Start straight in VR (skip the 2D launcher)](#frame-start_activity) | `frame.start_activity` | Steam Frame patches | |
|
||||
@@ -831,6 +832,19 @@ Stops a crash on the Frame's processor in games whose code the Quest runs more l
|
||||
|
||||
> Some engines contain assembly (for example OpenSSL's) that checks a return-address signature it never made. The Quest's CPU ignores those checks, the Steam Frame's enforces them: the game crashes with SIGILL as soon as it uses that code, for example on its first network connection (Star Wars Pinball VR). In a library with unpaired checks, the signing and checking instructions become no-ops.
|
||||
|
||||
### <a id="frame-photon_voice_audio_route"></a>Photon Voice: skip Android 12 audio routing
|
||||
|
||||
Stops games with Photon Voice chat from crashing on the Frame when voice chat starts (an Android 12 audio call).
|
||||
|
||||
- `frame.photon_voice_audio_route` · also for 2D apps
|
||||
- **Changes:** Changes the APK (a rebuild and reinstall apply it).
|
||||
- **Suggested (on):** Photon Voice calls Android 12 audio methods, which Lepton (Android 11) lacks, and crashes when voice chat starts.
|
||||
- **Suggested by launch-test signatures:**
|
||||
- `photon-voice-api31` (fatal): The game's Photon Voice microphone plugin calls Android 12 audio-routing methods (AudioManager communication devices) without checking the Android version; Lepton runs Android 11, so the app crashes when voice chat starts (for example Table Troopers at the tutorial).
|
||||
- **Source:** [patches/frame/photon_voice_audio_route.py](../src/frameport/patches/frame/photon_voice_audio_route.py)
|
||||
|
||||
> Photon Voice's microphone plugin (for example Table Troopers) picks the audio device with Android 12 methods (AudioManager communication devices) without checking the Android version; Lepton runs Android 11, so the game crashes with a NoSuchMethodError when voice chat starts. That method returns at once instead (an in-place edit of classes.dex); the microphone keeps Android's default audio route.
|
||||
|
||||
### <a id="frame-sdl_clipboard"></a>SDL apps: start without a clipboard service
|
||||
|
||||
Lets SDL and LÖVE games start on the Frame (they'd crash looking for a clipboard).
|
||||
|
||||
@@ -136,6 +136,7 @@ version is `catalog/triage.yaml` (used by `frameport test` / the Job screen); ke
|
||||
| SDL / LÖVE app crashes at start: `NullPointerException … ClipboardManager.addPrimaryClipChangedListener` in `SDLClipboardHandler.<init>` (for example Dramatic Shape) | Lepton's Android has no clipboard service (`service check clipboard`: not found) | `frame.sdl_clipboard` (that call → nops in classes.dex, in place, `apk/dex.py`); reproduced and verified with LÖVE for Android 11.5 |
|
||||
| Godot 4 app crashes at start: `java.lang.NullPointerException: null cannot be cast to non-null type android.content.ClipboardManager` at `org.godotengine.godot.Godot.<init>` (Godot 4.3/4.4; 4.2 when the clipboard is first used, in `Godot$mClipboard$2.invoke`) (for example EndoparasiticVR, GitHub #134); triage `godot-no-clipboard` | Lepton's Android has no clipboard service and Godot's Kotlin code casts it with a non-null `as ClipboardManager` (`Intrinsics.checkNotNull` before the check-cast). Godot 4.1 (Java) and 4.5+ (`as?`) don't throw | `frame.godot_clipboard` (suggested by analysis `godot_clipboard`): that checkNotNull call → nops in classes.dex (check-cast accepts null), and Godot's getClipboard/hasClipboard/setClipboard return at once ("" / false / nothing; `Dex.return_early`), in place; matched exactly, other dex files untouched. Checked against the Maven godot-lib 4.2.2/4.3.0/4.4.1 and the 4.3/4.4.1 release export templates (D8 re-reads the result); not yet run on the device |
|
||||
| Game crashes at start: `java.lang.NoSuchMethodError: No virtual method getAvailableCommunicationDevices()… in class Landroid/media/AudioManager` at `com.vivox.sdk.AudioChangeListener.checkAudioRouteAndApplyChanges` (for example Green Hell VR, GitHub #101); triage `vivox-api31` | Newer Vivox (voice chat) builds call Android 12 audio-routing methods without a version check; Lepton is Android 11. Older Vivox builds (Eleven Table Tennis, BattleSisters) don't have that code | `frame.vivox_audio_route` (suggested by analysis `vivox_api31`): every AudioChangeListener method that calls one of them returns at once (`Dex.return_early`: return-void / `const/4 v0, 0; return v0`, in place); voice chat keeps the default audio route |
|
||||
| Game crashes when voice chat starts (Table Troopers: at the tutorial's start): `java.lang.NoSuchMethodError: No virtual method getAvailableCommunicationDevices()… in class Landroid/media/AudioManager` at `com.exitgames.photon.audioinaec.AudioInAEC.updateAudioDevice` ← `AudioInAEC$1.run` (GitHub #362/#325); triage `photon-voice-api31` (the same error from another library: `audio-api31`, report it) | Photon Voice's Android microphone plugin picks the audio device with Android 12 methods (getAvailableCommunicationDevices / setCommunicationDevice) and has no version check (it logs "Start: SDK >= 31, using BroadcastReceiver" on every Android); Lepton is Android 11. `frame.vivox_audio_route` only edits Vivox's class, so it can't help here | `frame.photon_voice_audio_route` (suggested by analysis `photon_voice_api31`): AudioInAEC.updateAudioDevice (void, the only caller) returns at once (`Dex.return_early`, in place); the microphone thread goes on (communication mode, echo canceller, AudioRecord) on the default route. Checked against Table Troopers' classes.dex; not yet run on the device |
|
||||
| A game on a microSD card or another drive doesn't start, loses its saves or loops at start; launch.log has `ln: cannot create symbolic link from 'Frame' to '/data/data/<pkg>'` or `Couldn't create directory for SharedPreferences file /data/user/0/<pkg>/…` (GitHub #213/#214/#215/#221: a card named "Steam Frame 512g"); triage `lepton-data-link-failed` | Valve's Lepton pastes `STEAM_COMPAT_DATA_PATH` unquoted into `sh -c "ln -s …/internal/<pkg> /data/data/<pkg>"` (liblepton mounting.sh) and APK paths into unquoted substitutions, so a path with a space never gets the app's internal storage | agent v83: the Quest launcher links the game folder to `~/.local/share/frameport/lepton-paths/<pkg>` at every start and gives Lepton that path (`app_dir_lines`); `upgrade_launchers` converts existing launchers at the next connection; deployment.json keeps the real path |
|
||||
| Start never finishes, 100 % CPU, launch.log of gigabytes repeating `EOSSDK.createKeychain` / `resetKeychain` (Beyond Sandbox, GitHub #214); triage `eos-keychain-loop` | Epic Online Services' Java keychain can't write its SharedPreferences and resets/creates without end (seen when the app had no internal storage, row above) | fix the storage first; `frame.eos_keychain_loop` (opt-in, offered by triage; analysis `eos_keychain`) makes `EOSSDK.resetKeychain` return at once (`Dex.return_early`) |
|
||||
| Unity game freezes on its loading screen ~10-15 s in (Yupitergrad, GitHub #235); launch.log has `GrantPermissionsActivity: Permission grant result … permission=android.permission.BLUETOOTH_CONNECT`, then OVRPlugin `SetupDisplayObjects(): Unable to find EGLConfig for configId 0` and no more frames; triage `bhaptics-permission-prompt` | bHaptics' vest SDK (`com.bhaptics.commons.PermissionUtils.requestBluetoothPermission`) asks for Android 12's BLUETOOTH_CONNECT; on Lepton's Android 11 it can't be granted at install, so Android's permission screen takes the foreground for a moment, the game pauses/resumes and OVRPlugin's GLES compositor stops drawing | `frame.bhaptics_permission` (suggested by analysis `bhaptics_permission`): the request returns at once (`Dex.return_early`) |
|
||||
|
||||
@@ -509,6 +509,11 @@
|
||||
"title": "Unpaired pointer-authentication checks",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.photon_voice_audio_route": {
|
||||
"anchor": "frame-photon_voice_audio_route",
|
||||
"title": "Photon Voice: skip Android 12 audio routing",
|
||||
"group": "Steam Frame patches"
|
||||
},
|
||||
"frame.sdl_clipboard": {
|
||||
"anchor": "frame-sdl_clipboard",
|
||||
"title": "SDL apps: start without a clipboard service",
|
||||
|
||||
@@ -43,7 +43,9 @@ IL2CPP_METADATA = "assets/bin/Data/Managed/Metadata/global-metadata.dat"
|
||||
# 17: headset (the Meta headset a VR app was made for: Gear VR / Go games run through frame.a32_bridge) (2026-10)
|
||||
# 18: headset "quest" also for 32-bit apps that require 6DoF head tracking (Santa Cruz builds, e.g. Sports
|
||||
# Scramble: Touch controllers on the a32 bridge) (2026-10)
|
||||
ANALYSIS_VERSION = 19
|
||||
# 20: photon_voice_api31 (Photon Voice's AudioInAEC calls Android 12 AudioManager methods:
|
||||
# frame.photon_voice_audio_route, GitHub #362/#325) (2026-10)
|
||||
ANALYSIS_VERSION = 20
|
||||
|
||||
|
||||
# Android versions by API level (for messages); the Frame's Lepton container runs Android 11 (API 30)
|
||||
@@ -242,6 +244,9 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
|
||||
godot_clipboard = any(b"Lorg/godotengine/godot/Godot;" in d and GODOT_CLIPBOARD_CAST in d for d in dexes)
|
||||
# Vivox voice chat calling Android 12 audio-routing methods: crashes in Lepton (Android 11)
|
||||
vivox_api31 = any(b"Lcom/vivox/sdk/AudioChangeListener;" in d and b"CommunicationDevice" in d for d in dexes)
|
||||
# Photon Voice's microphone plugin does the same (frame.photon_voice_audio_route, GitHub #362)
|
||||
photon_voice_api31 = any(b"Lcom/exitgames/photon/audioinaec/AudioInAEC;" in d and b"CommunicationDevice" in d
|
||||
for d in dexes)
|
||||
# Epic Online Services' keychain (it can retry without end when it can't write: frame.eos_keychain_loop)
|
||||
eos_keychain = any(b"Lcom/epicgames/mobile/eossdk/EOSSDK;" in d and b"resetKeychain" in d for d in dexes)
|
||||
# bHaptics asks for an Android 12 permission; the prompt interrupts the game (frame.bhaptics_permission)
|
||||
@@ -381,6 +386,7 @@ def analyze(path: Path, deep: bool = True, data_bytes: int | None = None) -> Ana
|
||||
"sdl_java": sdl_java,
|
||||
"godot_clipboard": godot_clipboard,
|
||||
"vivox_api31": vivox_api31,
|
||||
"photon_voice_api31": photon_voice_api31,
|
||||
"eos_keychain": eos_keychain,
|
||||
"bhaptics_permission": bhaptics_permission,
|
||||
"media_codec": media_codec,
|
||||
|
||||
@@ -1133,6 +1133,8 @@
|
||||
"PC VR ✓": "",
|
||||
"Per-eye diagnostics (log)": "",
|
||||
"Perfect pacing": "",
|
||||
"Photon Voice's microphone plugin (for example Table Troopers) picks the audio device with Android 12 methods (AudioManager communication devices) without checking the Android version; Lepton runs Android 11, so the game crashes with a NoSuchMethodError when voice chat starts. That method returns at once instead (an in-place edit of classes.dex); the microphone keeps Android's default audio route.": "",
|
||||
"Photon Voice: skip Android 12 audio routing": "",
|
||||
"Pick several games and install them in one go. FramePort asks every question first, then works through them in the background.": "",
|
||||
"Pick the field in the headset": "",
|
||||
"Pick the program you'd double-click to play. Oculus builds usually work better than Steam builds.": "",
|
||||
@@ -1526,6 +1528,7 @@
|
||||
"Stops bHaptics from asking for a Bluetooth permission the Frame doesn't have (the game would freeze on its loading screen).": "",
|
||||
"Stops Epic Online Services from retrying its login keychain forever (the game would never finish starting).": "",
|
||||
"Stops games from hanging when their eye picture is a little too big (for example PowerWash Simulator). Leave it on.": "",
|
||||
"Stops games with Photon Voice chat from crashing on the Frame when voice chat starts (an Android 12 audio call).": "",
|
||||
"Stops Meta's audio library from reporting to Meta (only matters on PC emulators).": "",
|
||||
"Stops Meta's old VR library from closing the game at start (for example Jurassic World Aftermath).": "",
|
||||
"Stops some Unreal games from closing themselves right after starting.": "",
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Photon Voice (Exit Games' Unity voice chat, e.g. Table Troopers, GitHub #362/#325) crashes in Lepton when voice
|
||||
chat starts: its Java microphone plugin (com.exitgames.photon.audioinaec.AudioInAEC) picks the audio route in
|
||||
updateAudioDevice with Android 12 AudioManager methods (getAvailableCommunicationDevices, setCommunicationDevice)
|
||||
and has no Android version check left (it logs "Start: SDK >= 31, using BroadcastReceiver" on every Android), so on
|
||||
Lepton's Android 11 the microphone thread dies with java.lang.NoSuchMethodError and takes the app with it. The
|
||||
AudioInAEC methods that call them (updateAudioDevice: void) return at once instead, the same in-place classes.dex edit
|
||||
as frame.vivox_audio_route. The microphone thread then goes on as on Android 12 (communication mode, echo canceller /
|
||||
gain control / noise suppressor, AudioRecord) on Android's default route; only the device pick is skipped."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ..base import ApkContext, Patch, Suggestion, register
|
||||
from .vivox_audio_route import patch_class, patch_dexes
|
||||
|
||||
AUDIO_IN = "Lcom/exitgames/photon/audioinaec/AudioInAEC;"
|
||||
|
||||
|
||||
def patch_dex(dex) -> list[str]:
|
||||
"""Methods of Photon Voice's AudioInAEC that now return at once; [] if nothing applies."""
|
||||
return patch_class(dex, AUDIO_IN)
|
||||
|
||||
|
||||
class PhotonVoiceAudioRoute(Patch):
|
||||
id = "frame.photon_voice_audio_route"
|
||||
title = "Photon Voice: skip Android 12 audio routing"
|
||||
description = ("Photon Voice's microphone plugin (for example Table Troopers) picks the audio device with "
|
||||
"Android 12 methods (AudioManager communication devices) without checking the Android version; "
|
||||
"Lepton runs Android 11, so the game crashes with a NoSuchMethodError when voice chat starts. "
|
||||
"That method returns at once instead (an in-place edit of classes.dex); the microphone keeps "
|
||||
"Android's default audio route.")
|
||||
order = 44
|
||||
needs_vr = False
|
||||
|
||||
def applies(self, a):
|
||||
return bool((a.extra or {}).get("photon_voice_api31"))
|
||||
|
||||
def detect(self, a):
|
||||
if self.applies(a):
|
||||
return Suggestion(True, "Photon Voice calls Android 12 audio methods, which Lepton (Android 11) lacks, "
|
||||
"and crashes when voice chat starts.")
|
||||
return None
|
||||
|
||||
def apply(self, ctx: ApkContext) -> bool:
|
||||
return patch_dexes(ctx, AUDIO_IN, "Photon Voice AudioInAEC")
|
||||
|
||||
|
||||
register(PhotonVoiceAudioRoute)
|
||||
@@ -18,13 +18,14 @@ API31 = ("getAvailableCommunicationDevices", "setCommunicationDevice", "clearCom
|
||||
"removeOnCommunicationDeviceChangedListener")
|
||||
|
||||
|
||||
def patch_dex(dex: Dex) -> list[str]:
|
||||
"""Methods of Vivox's AudioChangeListener that now return at once; [] if nothing applies."""
|
||||
def patch_class(dex: Dex, cls: str) -> list[str]:
|
||||
"""Methods of class `cls` that call one of the Android 12 communication-device methods and now return at once;
|
||||
[] if nothing applies. Shared with frame.photon_voice_audio_route."""
|
||||
targets = {i for name in API31 for i in dex.method_index(AUDIO_MANAGER, name)}
|
||||
if not targets:
|
||||
return []
|
||||
done = []
|
||||
for idx, code in dex.class_methods(LISTENER):
|
||||
for idx, code in dex.class_methods(cls):
|
||||
if not dex.invoked(code) & targets:
|
||||
continue
|
||||
if dex.return_early(code, dex.return_type(idx)):
|
||||
@@ -32,6 +33,27 @@ def patch_dex(dex: Dex) -> list[str]:
|
||||
return done
|
||||
|
||||
|
||||
def patch_dexes(ctx: ApkContext, cls: str, label: str) -> bool:
|
||||
"""patch_class on every classes*.dex that defines `cls`; notes what changed."""
|
||||
changed = False
|
||||
for name in sorted(n for n in ctx.ws.names() if n.startswith("classes") and n.endswith(".dex")):
|
||||
data = ctx.ws.read(name)
|
||||
if cls.encode() not in data:
|
||||
continue
|
||||
dex = Dex(data)
|
||||
done = patch_class(dex, cls)
|
||||
if done:
|
||||
ctx.ws.put(name, dex.finish())
|
||||
ctx.notes.append(f"{name}: {label} returns at once in {', '.join(sorted(done))}")
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
|
||||
def patch_dex(dex: Dex) -> list[str]:
|
||||
"""Methods of Vivox's AudioChangeListener that now return at once; [] if nothing applies."""
|
||||
return patch_class(dex, LISTENER)
|
||||
|
||||
|
||||
class VivoxAudioRoute(Patch):
|
||||
id = "frame.vivox_audio_route"
|
||||
title = "Vivox voice chat: skip Android 12 audio routing"
|
||||
@@ -52,18 +74,7 @@ class VivoxAudioRoute(Patch):
|
||||
return None
|
||||
|
||||
def apply(self, ctx: ApkContext) -> bool:
|
||||
changed = False
|
||||
for name in sorted(n for n in ctx.ws.names() if n.startswith("classes") and n.endswith(".dex")):
|
||||
data = ctx.ws.read(name)
|
||||
if LISTENER.encode() not in data:
|
||||
continue
|
||||
dex = Dex(data)
|
||||
done = patch_dex(dex)
|
||||
if done:
|
||||
ctx.ws.put(name, dex.finish())
|
||||
ctx.notes.append(f"{name}: Vivox AudioChangeListener returns at once in {', '.join(sorted(done))}")
|
||||
changed = True
|
||||
return changed
|
||||
return patch_dexes(ctx, LISTENER, "Vivox AudioChangeListener")
|
||||
|
||||
|
||||
register(VivoxAudioRoute)
|
||||
@@ -84,6 +84,8 @@ SUMMARIES = {
|
||||
"(they'd abort with \"cannot create thread specific key\"), for example Wander.",
|
||||
"frame.vivox_audio_route": "Lets games with Vivox voice chat start on the Frame (they'd crash on an Android 12 "
|
||||
"audio call).",
|
||||
"frame.photon_voice_audio_route": "Stops games with Photon Voice chat from crashing on the Frame when voice chat "
|
||||
"starts (an Android 12 audio call).",
|
||||
"frame.unity_oculus_check": "Lets older Unity games start VR without Meta's system apps (they'd stay on a 2D "
|
||||
"screen).",
|
||||
"frame.unity_multipass": "Draws each eye separately. Try it when one eye shows a gray or broken picture.",
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
"""frame.photon_voice_audio_route (GitHub #362 Table Troopers, #325): Photon Voice's AudioInAEC.updateAudioDevice calls
|
||||
Android 12 AudioManager methods with no version check; it returns at once (in-place dex edit, as for Vivox)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import struct
|
||||
import zipfile
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from test_vivox_audio_route import CHECK, build_dex
|
||||
|
||||
from frameport.analysis import detect
|
||||
from frameport.apk.dex import Dex
|
||||
from frameport.apk.workspace import ApkWorkspace
|
||||
from frameport.core.events import Reporter
|
||||
from frameport.patches import base
|
||||
from frameport.patches.frame import vivox_audio_route
|
||||
from frameport.patches.frame.photon_voice_audio_route import AUDIO_IN, PhotonVoiceAudioRoute, patch_dex
|
||||
from frameport.validate.triage import triage
|
||||
|
||||
AM = "Landroid/media/AudioManager;"
|
||||
RUNNER = "Lcom/exitgames/photon/audioinaec/AudioInAEC$1;"
|
||||
# method indices: 0 getAvailableCommunicationDevices, 1 setCommunicationDevice, 2 access$000 (see _dex)
|
||||
UPDATE = [0x1054, 0x0000, 0x106E, 0, 0x0000, 0x000C, 0x000E] # iget-object; invoke; move-result-object; return-void
|
||||
ACCESS = [0x1070, 3, 0x0000, 0x000E] # invoke-direct {v0} updateAudioDevice; return-void
|
||||
RUN = [0x1071, 2, 0x0000, 0x000E] # invoke-static {v0} access$000; return-void
|
||||
START = [0x0012, 0x000F] # const/4 v0, 0; return v0
|
||||
|
||||
|
||||
def _dex(vivox: bool = True) -> bytes:
|
||||
return build_dex([(AM, "getAvailableCommunicationDevices", "Ljava/util/List;", None),
|
||||
(AM, "setCommunicationDevice", "Z", None),
|
||||
(AUDIO_IN, "access$000", "V", ACCESS),
|
||||
(AUDIO_IN, "updateAudioDevice", "V", UPDATE),
|
||||
(AUDIO_IN, "Start", "Z", START),
|
||||
(RUNNER, "run", "V", RUN),
|
||||
*[(vivox_audio_route.LISTENER, "checkAudioRouteAndApplyChanges", "V", CHECK)] * vivox])
|
||||
|
||||
|
||||
def _code(dex: Dex, cls: str, name: str) -> list[int]:
|
||||
off = dex.code_items(cls, name)[0]
|
||||
n = struct.unpack_from("<I", dex.data, off + 12)[0]
|
||||
return list(struct.unpack_from(f"<{n}H", dex.data, off + 16))
|
||||
|
||||
|
||||
def test_update_audio_device_returns_at_once():
|
||||
raw = _dex()
|
||||
dex = Dex(raw)
|
||||
assert patch_dex(dex) == ["updateAudioDevice"]
|
||||
assert _code(dex, AUDIO_IN, "updateAudioDevice") == [0x000E, 0] + UPDATE[2:] # iget-object (2 units) -> return-void
|
||||
assert _code(dex, AUDIO_IN, "access$000") == ACCESS # callers stay as they are: the mic thread goes on
|
||||
assert _code(dex, AUDIO_IN, "Start") == START
|
||||
assert _code(dex, RUNNER, "run") == RUN
|
||||
assert _code(dex, vivox_audio_route.LISTENER, "checkAudioRouteAndApplyChanges") == CHECK # Vivox is its own patch
|
||||
out = dex.finish()
|
||||
assert len(out) == len(raw) and patch_dex(Dex(out)) == []
|
||||
|
||||
|
||||
def test_vivox_patch_leaves_photon_alone():
|
||||
dex = Dex(_dex())
|
||||
assert vivox_audio_route.patch_dex(dex) == ["checkAudioRouteAndApplyChanges"]
|
||||
assert _code(dex, AUDIO_IN, "updateAudioDevice") == UPDATE
|
||||
|
||||
|
||||
def test_apply_through_the_workspace(tmp_path, quest_manifest):
|
||||
path = tmp_path / "game.apk"
|
||||
with zipfile.ZipFile(path, "w") as z:
|
||||
z.writestr("AndroidManifest.xml", quest_manifest)
|
||||
z.writestr("classes.dex", _dex(vivox=False))
|
||||
a = detect.analyze(path, deep=False)
|
||||
assert a.extra["photon_voice_api31"] is True and a.extra["vivox_api31"] is False
|
||||
assert a.extra["analysis_version"] == detect.ANALYSIS_VERSION >= 20
|
||||
patch = base.get("frame.photon_voice_audio_route")
|
||||
assert patch.applies(a) and patch.detect(a).recommended and not patch.needs_vr
|
||||
assert not base.get("frame.vivox_audio_route").applies(a)
|
||||
with ApkWorkspace(path) as ws:
|
||||
ctx = base.ApkContext(ws, a, {}, Reporter(), {})
|
||||
assert patch.apply(ctx)
|
||||
assert ctx.notes == ["classes.dex: Photon Voice AudioInAEC returns at once in updateAudioDevice"]
|
||||
assert _code(Dex(ws.read("classes.dex")), AUDIO_IN, "updateAudioDevice")[0] == 0x000E
|
||||
assert not patch.apply(ctx) # once
|
||||
|
||||
|
||||
def test_detect_needs_the_flag():
|
||||
class A:
|
||||
extra = {"photon_voice_api31": True}
|
||||
assert PhotonVoiceAudioRoute().detect(A()).recommended
|
||||
A.extra = {"vivox_api31": True}
|
||||
assert PhotonVoiceAudioRoute().detect(A()) is None
|
||||
|
||||
|
||||
# the crash of GitHub #362 (Table Troopers, the tutorial's start)
|
||||
HEAD = ("10-16 16:22:38.975 1238 1413 E AndroidRuntime: FATAL EXCEPTION: Thread-23\n"
|
||||
"10-16 16:22:38.975 1238 1413 E AndroidRuntime: Process: com.cosmorama.tabletroopers, PID: 1238\n"
|
||||
"10-16 16:22:38.975 1238 1413 E AndroidRuntime: java.lang.NoSuchMethodError: No virtual method "
|
||||
"getAvailableCommunicationDevices()Ljava/util/List; in class Landroid/media/AudioManager; or its super classes "
|
||||
"(declaration of 'android.media.AudioManager' appears in /system/framework/framework.jar!classes2.dex)\n")
|
||||
PHOTON = ("10-16 16:22:38.975 1238 1413 E AndroidRuntime: \tat com.exitgames.photon.audioinaec.AudioInAEC."
|
||||
"updateAudioDevice(AudioInAEC.java:107)\n"
|
||||
"10-16 16:22:38.975 1238 1413 E AndroidRuntime: \tat com.exitgames.photon.audioinaec.AudioInAEC."
|
||||
"access$000(AudioInAEC.java:23)\n"
|
||||
"10-16 16:22:38.975 1238 1413 E AndroidRuntime: \tat com.exitgames.photon.audioinaec.AudioInAEC$1."
|
||||
"run(AudioInAEC.java:185)\n"
|
||||
"10-16 16:22:38.975 1238 1413 E AndroidRuntime: \tat java.lang.Thread.run(Thread.java:923)\n")
|
||||
VIVOX = ("10-16 16:22:38.975 1238 1413 E AndroidRuntime: \tat com.vivox.sdk.AudioChangeListener."
|
||||
"checkAudioRouteAndApplyChanges(AudioChangeListener.java:80)\n")
|
||||
OTHER = "10-16 16:22:38.975 1238 1413 E AndroidRuntime: \tat com.example.voice.Router.pick(Router.java:12)\n"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stack, finding, suggest", [
|
||||
(PHOTON, "photon-voice-api31", ["frame.photon_voice_audio_route"]),
|
||||
(VIVOX, "vivox-api31", ["frame.vivox_audio_route"]),
|
||||
(OTHER, "audio-api31", []),
|
||||
])
|
||||
def test_triage_names_the_sdk_in_the_stack(stack, finding, suggest):
|
||||
r = triage(HEAD + stack, "EXITED", "com.cosmorama.tabletroopers")
|
||||
assert [f.id for f in r.findings] == [finding] # java-crash and the generic one are superseded
|
||||
assert r.findings[0].suggest == suggest
|
||||
assert r.findings[0].report is (finding == "audio-api31")
|
||||
|
||||
|
||||
def _table_troopers_apk() -> Path:
|
||||
root = os.environ.get("FRAMEPORT_GAMES")
|
||||
if not root:
|
||||
pytest.skip("set FRAMEPORT_GAMES")
|
||||
found = sorted(Path(root).glob("*/com.cosmorama.tabletroopers.apk")) or sorted(
|
||||
Path(root).glob("**/com.cosmorama.tabletroopers.apk"))
|
||||
if not found:
|
||||
pytest.skip("no Table Troopers APK in FRAMEPORT_GAMES")
|
||||
return found[0]
|
||||
|
||||
|
||||
@pytest.mark.games
|
||||
def test_real_table_troopers_dex():
|
||||
with zipfile.ZipFile(_table_troopers_apk()) as z:
|
||||
raw = z.read("classes.dex")
|
||||
assert vivox_audio_route.patch_dex(Dex(raw)) == [] # no Vivox here: that patch was a no-op (#362)
|
||||
dex = Dex(raw)
|
||||
off = dex.code_items(AUDIO_IN, "updateAudioDevice")[0]
|
||||
assert patch_dex(dex) == ["updateAudioDevice"]
|
||||
out = dex.finish()
|
||||
assert len(out) == len(raw)
|
||||
# only the first instruction changed (const/16 -> return-void; nop), nothing else in the file
|
||||
start = off + 16
|
||||
assert raw[32:start] == out[32:start] and raw[start + 4:] == out[start + 4:]
|
||||
assert struct.unpack_from("<2H", out, start) == (0x000E, 0x0000)
|
||||
# header: SHA-1 signature and Adler-32 checksum match the new bytes
|
||||
assert out[12:32] == hashlib.sha1(out[32:]).digest()
|
||||
assert struct.unpack_from("<I", out, 8)[0] == zlib.adler32(out[12:]) & 0xFFFFFFFF
|
||||
# the microphone thread and every other method of the plugin are unchanged
|
||||
again, orig = Dex(out), Dex(raw)
|
||||
for cls in (AUDIO_IN, RUNNER):
|
||||
for idx, code in orig.class_methods(cls):
|
||||
if orig.method(idx)[1] != "updateAudioDevice":
|
||||
n = struct.unpack_from("<I", raw, code + 12)[0]
|
||||
assert raw[code:code + 16 + 2 * n] == out[code:code + 16 + 2 * n]
|
||||
assert patch_dex(again) == []
|
||||
@@ -157,7 +157,9 @@ def test_detect_and_triage():
|
||||
log = ("10-08 21:00:01.000 900 950 E AndroidRuntime: FATAL EXCEPTION: main\n"
|
||||
"10-08 21:00:01.000 900 950 E AndroidRuntime: java.lang.NoSuchMethodError: No virtual method "
|
||||
"getAvailableCommunicationDevices()Ljava/util/List; in class Landroid/media/AudioManager; or its super "
|
||||
"classes (declaration of 'android.media.AudioManager' appears in /system/framework/framework.jar)\n")
|
||||
"classes (declaration of 'android.media.AudioManager' appears in /system/framework/framework.jar)\n"
|
||||
"10-08 21:00:01.000 900 950 E AndroidRuntime: \tat com.vivox.sdk.AudioChangeListener."
|
||||
"checkAudioRouteAndApplyChanges(AudioChangeListener.java:80)\n")
|
||||
r = triage(log, "EXITED", "com.Incuvo.GreenHellVR")
|
||||
assert [f.id for f in r.findings] == ["vivox-api31"]
|
||||
assert r.findings[0].suggest == ["frame.vivox_audio_route"]
|
||||
Reference in new issue
Block a user