mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 01:00:18 +02:00
Third review follow-up (PackageParser.buildClassName). Confirmed the targetActivity resource id 0x01010202 in Open Saber Plus's manifest. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
129 lines
6.5 KiB
Python
129 lines
6.5 KiB
Python
"""Binary-manifest VR inspection and minimal launcher repair (stdlib only)."""
|
|
import struct
|
|
import frame_apk
|
|
|
|
MAIN = 'android.intent.action.MAIN'
|
|
LAUNCHER = 'android.intent.category.LAUNCHER'
|
|
VR = {'com.oculus.intent.category.VR', 'org.khronos.openxr.intent.category.IMMERSIVE_HMD'}
|
|
|
|
|
|
def inspect(data):
|
|
elements = iter(frame_apk.manifest_elements(data))
|
|
stack, filters, samsung = [], [], False
|
|
current, owner, package = None, None, ''
|
|
for kind, hs, off, size in frame_apk._chunks(data, 8, len(data)):
|
|
if kind == 0x0102:
|
|
tag, attrs = next(elements)
|
|
value = attrs.get('name', (None, None, None))[2]
|
|
if tag == 'manifest':
|
|
package = attrs.get('package', (None, None, None))[2] or ''
|
|
if tag in ('activity', 'activity-alias'):
|
|
owner = attrs.get('targetActivity', (None, None, None))[2] if tag == 'activity-alias' else value
|
|
if owner and '.' not in owner: # PackageParser.buildClassName: bare names are relative too
|
|
owner = '.' + owner
|
|
owner = package + owner if owner and owner.startswith('.') else owner
|
|
if tag == 'intent-filter' and stack and stack[-1] in ('activity', 'activity-alias'):
|
|
current = {'actions': set(), 'categories': set(), 'templates': [], 'alias': stack[-1] == 'activity-alias', 'activity': owner}
|
|
if current is not None and stack and stack[-1] == 'intent-filter':
|
|
if tag == 'action':
|
|
current['actions'].add(value)
|
|
if tag == 'category':
|
|
current['categories'].add(value)
|
|
current['templates'].append((off, size, hs))
|
|
if tag == 'meta-data' and stack and stack[-1] == 'application':
|
|
samsung |= value == 'com.samsung.android.vr.application.mode' and attrs.get('value', (0, 0, None))[2] == 'vr_only'
|
|
stack.append(tag)
|
|
elif kind == 0x0103 and stack:
|
|
tag = stack.pop()
|
|
if tag == 'intent-filter' and current is not None:
|
|
current['end'] = off
|
|
filters.append(current)
|
|
current = None
|
|
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.
|
|
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']):
|
|
aimed = {f['activity'] for f in mains if f['alias'] and (LAUNCHER in f['categories'] or VR & f['categories'])}
|
|
targets = [f for f in real if f['templates']] # the patch copies an existing <category>
|
|
targets = [f for f in targets if f['activity'] in aimed] or targets
|
|
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):
|
|
_, hs, size, count, styles, flags, start, style_start = struct.unpack_from('<HHIIIIII', chunk)
|
|
strings_end = style_start or size
|
|
encoded = text.encode('utf-8' if flags & 0x100 else 'utf-16-le')
|
|
# LAUNCHER is short enough for both single-unit length encodings.
|
|
new = (bytes([len(text), len(encoded)]) + encoded + b'\0' if flags & 0x100
|
|
else struct.pack('<H', len(text)) + encoded + b'\0\0')
|
|
string_data = chunk[start:strings_end] + new
|
|
string_data += bytes(-len(string_data) % 4)
|
|
header = bytearray(chunk[:hs])
|
|
new_start = start + 4
|
|
new_styles = new_start + len(string_data) if style_start else 0
|
|
body = (chunk[hs:hs + count * 4] + struct.pack('<I', strings_end - start)
|
|
+ chunk[hs + count * 4:start] + string_data + (chunk[style_start:] if style_start else b''))
|
|
struct.pack_into('<IIIII', header, 8, count + 1, styles, flags & ~1, new_start, new_styles)
|
|
struct.pack_into('<I', header, 4, len(header) + len(body))
|
|
return bytes(header) + body, count
|
|
|
|
|
|
def add_launcher_category(axml_bytes):
|
|
info, filters = inspect(axml_bytes)
|
|
if info['launchable']:
|
|
return axml_bytes
|
|
if not filters:
|
|
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)
|
|
_, _, po, ps = pool
|
|
new_pool, index = _append_string(axml_bytes[po:po + ps], LAUNCHER)
|
|
off, size, hs = target['templates'][0]
|
|
start = bytearray(axml_bytes[off:off + size])
|
|
attr_start, attr_size, count = struct.unpack_from('<HHH', start, hs + 8)
|
|
strings = frame_apk._string_pool(axml_bytes, po)
|
|
resmap = []
|
|
for kind, header_size, offset, chunk_size in chunks:
|
|
if kind == 0x180:
|
|
resmap = struct.unpack_from('<%dI' % ((chunk_size - header_size) // 4),
|
|
axml_bytes, offset + header_size)
|
|
for i in range(count):
|
|
a = hs + attr_start + i * attr_size
|
|
name = struct.unpack_from('<I', start, a + 4)[0]
|
|
if (name < len(resmap) and resmap[name] == 0x01010003) or strings[name] == 'name':
|
|
struct.pack_into('<I', start, a + 8, index)
|
|
struct.pack_into('<HBBI', start, a + 12, 8, 0, 3, index)
|
|
break
|
|
else:
|
|
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:
|
|
if off == target['end']:
|
|
result += start + end
|
|
result += new_pool if off == po else axml_bytes[off:off + size]
|
|
struct.pack_into('<I', result, 4, len(result))
|
|
result = bytes(result)
|
|
if not inspect(result)[0]['launchable']:
|
|
raise frame_apk.ApkError('launcher repair failed verification')
|
|
return result
|
|
|
|
|
|
def detection(data, names):
|
|
info, _ = inspect(data)
|
|
issues = []
|
|
if 'lib/arm64-v8a/libvrapi.so' in names:
|
|
issues.append("Uses Meta's legacy VrApi, which the Frame doesn't have; it won't run.")
|
|
if any(n.endswith('/libovrplatformloader.so') for n in names):
|
|
issues.append("Uses Meta's platform SDK; if it checks your Quest store licence it will quit.")
|
|
if 'lib/arm64-v8a/libopenxr_loader.so' in names:
|
|
info['vr'] = True
|
|
issues.append('Uses OpenXR (good).')
|
|
info['vr_issues'] = issues
|
|
return info
|