Repair alias-only launchers for flat apps too; never fail on a VR alias

Review follow-up. inspect() now returns 'repairable' and the filters it can
patch: the real activity's VR MAIN filter, or, when LAUNCHER/VR sits only on an
<activity-alias>, any real MAIN filter with a category to copy. install() and
patch() repair on 'repairable' instead of 'vr_activity', so a flat Godot 4
export is fixed and a VR category only on the alias no longer aborts install.
Tests cover both shapes, an already-launchable activity with an alias, and an
end-to-end patch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-28 20:40:16 +10:00
1 parent 077eab2b79
commit e1c0ac983c
3 files changed
+57 -23

No files matched your search

+6 -5
View File
@@ -149,7 +149,7 @@ def install(apk_path, flatscreen=None, name=None, source=None, icon_png=None, xr
# changes calls SteamVR would otherwise reject.
add = xr_compat_files(apk_path) if (info['vr'] if xr_compat is None else xr_compat) else {}
with _install_lock:
if add or (not info['launchable'] and info['vr_activity']):
if add or info['repairable']:
with tempfile.TemporaryDirectory(prefix='frame-vr-') as tmp:
patched = os.path.join(tmp, 'app.apk')
info['patched'] = patch(apk_path, patched, add)['patched']
@@ -320,8 +320,8 @@ def patch(src, dst, add=None):
info = apk_info(src)
with zipfile.ZipFile(src) as z:
original = frame_apk._read(z, 'AndroidManifest.xml', frame_apk.MAX_MANIFEST)
manifest = add_launcher_category(original) if info['vr_activity'] else original
if not info['launchable'] and not info['vr_activity']:
manifest = add_launcher_category(original) if info['repairable'] else original
if not info['launchable'] and not info['repairable']:
raise FrameError('APK has no MAIN/LAUNCHER or patchable VR activity')
repack(src, dst, replace={'AndroidManifest.xml': manifest}, add=add)
result = apk_info(dst)
@@ -341,8 +341,9 @@ def main():
if cmd == 'info':
info = apk_info(args[0])
print(frame_apk_versions.describe(info))
if info.get('vr'):
print('VR app' + ('' if info.get('launchable') else '; Frame Control adds the LAUNCHER entry Lepton needs'))
fix = '; Frame Control adds the LAUNCHER entry Lepton needs' if info.get('repairable') else ''
if info.get('vr') or fix:
print(('VR app' if info.get('vr') else 'Android app') + fix)
for note in info.get('vr_issues', []):
print(note)
return
+9 -5
View File
@@ -35,9 +35,13 @@ def inspect(data):
mains = [f for f in filters if MAIN in f['actions']]
vr_filters = [f for f in mains if VR & f['categories']]
# Lepton's apk-info-extractor ignores <activity-alias>; Godot 4 puts LAUNCHER only there.
return {'launchable': any(LAUNCHER in f['categories'] for f in mains if not f['alias']),
'vr_activity': bool(vr_filters), 'vr': bool(vr_filters) or samsung}, \
[f for f in vr_filters if not f['alias']]
real = [f for f in mains if not f['alias']]
targets = [f for f in real if VR & f['categories']]
if not targets and any(LAUNCHER in f['categories'] or VR & f['categories'] for f in mains if f['alias']):
targets = [f for f in real if f['templates']] # the patch copies an existing <category>
launchable = any(LAUNCHER in f['categories'] for f in real)
return {'launchable': launchable, 'repairable': not launchable and bool(targets),
'vr_activity': bool(vr_filters), 'vr': bool(vr_filters) or samsung}, targets
def _append_string(chunk, text):
@@ -64,7 +68,7 @@ def add_launcher_category(axml_bytes):
if info['launchable']:
return axml_bytes
if not filters:
raise frame_apk.ApkError('no VR activity with a MAIN intent filter to patch')
raise frame_apk.ApkError('no activity with a MAIN intent filter that Frame Control can patch')
target = filters[0]
chunks = list(frame_apk._chunks(axml_bytes, 8, len(axml_bytes)))
pool = next(c for c in chunks if c[0] == 1)
@@ -87,7 +91,7 @@ def add_launcher_category(axml_bytes):
struct.pack_into('<HBBI', start, a + 12, 8, 0, 3, index)
break
else:
raise frame_apk.ApkError('VR category has no name attribute')
raise frame_apk.ApkError('category has no name attribute')
end = struct.pack('<HHI', 0x0103, hs, hs + 8) + start[8:hs] + start[hs:hs + 8]
result = bytearray(axml_bytes[:8])
for _, _, off, size in chunks: