mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 01:00:18 +02:00
Patch the activity the launcher alias targets, not the first MAIN filter
Second review follow-up: with several activities (e.g. a splash activity ahead of the game), the alias fallback now prefers the real activity named by the alias's android:targetActivity. Reads targetActivity by resource id, updates the error text and docs/vr-apks.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
268afccf05
commit
9e9e5950d0
5 files changed
+37
-10
No files matched your search
+5
-2
@@ -100,8 +100,11 @@ VR apps default to immersive mode without the flatscreen marker. The upload
|
||||
selector or CLI `--flat` / `--vr` overrides that choice. Compatibility notes
|
||||
identify legacy VrApi, Meta platform SDK and OpenXR libraries.
|
||||
|
||||
If a VR MAIN intent filter lacks LAUNCHER, Frame Control inserts it, repacks
|
||||
and v2-signs the APK locally before copying it; `meta.json` records
|
||||
Lepton only starts an `<activity>` whose MAIN intent filter has LAUNCHER; it
|
||||
ignores `<activity-alias>`, which is where Godot 4 exports put LAUNCHER. When no
|
||||
real activity qualifies, Frame Control adds LAUNCHER to the VR activity's MAIN
|
||||
filter, or to the activity the launcher alias targets, then repacks and v2-signs
|
||||
the APK locally before copying it; `meta.json` records
|
||||
`"patched": ["launcher"]`. Unchanged ZIP members retain their compressed
|
||||
bytes; stored libraries are aligned to 16 KiB. The RSA signing identity lives
|
||||
in Frame Control's per-user app-data directory as `apk-signing-key.json`
|
||||
|
||||
@@ -21,11 +21,11 @@ from test_frame_apk import pool
|
||||
DEFAULT = 'android.intent.category.DEFAULT'
|
||||
|
||||
|
||||
def manifest(utf8=False, launcher=False, category=None, samsung=False, split=False, alias=False):
|
||||
def manifest(utf8=False, launcher=False, category=None, samsung=False, split=False, alias=False, splash=False):
|
||||
strings = ['manifest', 'package', 'org.test.vr', 'application', 'activity', 'intent-filter',
|
||||
'action', 'category', 'name', vr.MAIN, category or next(iter(sorted(vr.VR))),
|
||||
vr.LAUNCHER, 'http://schemas.android.com/apk/res/android', 'meta-data', 'value',
|
||||
'com.samsung.android.vr.application.mode', 'vr_only', 'activity-alias', DEFAULT, next(iter(sorted(vr.VR)))]
|
||||
'com.samsung.android.vr.application.mode', 'vr_only', 'activity-alias', DEFAULT, next(iter(sorted(vr.VR))), 'targetActivity', '.Splash', '.Game']
|
||||
def start(tag, attrs=()):
|
||||
body = struct.pack('<IIHHHHHH', 0xffffffff, strings.index(tag), 20, 20, len(attrs), 0, 0, 0)
|
||||
for name, value in attrs:
|
||||
@@ -39,7 +39,10 @@ def manifest(utf8=False, launcher=False, category=None, samsung=False, split=Fal
|
||||
b = pool(strings, utf8) + start('manifest', [('package', 'org.test.vr')]) + start('application')
|
||||
if samsung:
|
||||
b += leaf('meta-data', [('name', strings[15]), ('value', 'vr_only')])
|
||||
b += start('activity') + start('intent-filter') + leaf('action', [('name', vr.MAIN)])
|
||||
if splash: # a helper activity with its own MAIN filter, ahead of the game's
|
||||
b += start('activity', [('name', '.Splash')]) + start('intent-filter') + leaf('action', [('name', vr.MAIN)])
|
||||
b += leaf('category', [('name', DEFAULT)]) + end('intent-filter') + end('activity')
|
||||
b += start('activity', [('name', '.Game')] if splash else []) + start('intent-filter') + leaf('action', [('name', vr.MAIN)])
|
||||
b += leaf('category', [('name', strings[10])])
|
||||
if split:
|
||||
b += end('intent-filter') + start('intent-filter')
|
||||
@@ -47,7 +50,7 @@ def manifest(utf8=False, launcher=False, category=None, samsung=False, split=Fal
|
||||
b += leaf('category', [('name', vr.LAUNCHER)])
|
||||
b += end('intent-filter') + end('activity')
|
||||
if alias: # Godot 4: LAUNCHER only on an alias of the activity ('vr' or 'flat')
|
||||
b += start('activity-alias') + start('intent-filter') + leaf('action', [('name', vr.MAIN)])
|
||||
b += start('activity-alias', [('targetActivity', '.Game')] if splash else []) + start('intent-filter') + leaf('action', [('name', vr.MAIN)])
|
||||
if alias == 'vr':
|
||||
b += leaf('category', [('name', next(iter(sorted(vr.VR))))])
|
||||
b += leaf('category', [('name', vr.LAUNCHER)])
|
||||
@@ -101,6 +104,19 @@ class VRTests(unittest.TestCase):
|
||||
self.assertFalse(info['repairable'])
|
||||
self.assertEqual(info['vr'], is_vr)
|
||||
|
||||
def test_alias_target_activity_is_patched(self):
|
||||
original = manifest(alias='flat', category=DEFAULT, splash=True)
|
||||
info, filters = vr.inspect(original)
|
||||
self.assertTrue(info['repairable'])
|
||||
self.assertEqual(filters[0]['activity'], 'org.test.vr.Game')
|
||||
owner, launchers = None, []
|
||||
for tag, attrs in frame_apk.manifest_elements(vr.add_launcher_category(original)):
|
||||
if tag in ('activity', 'activity-alias'):
|
||||
owner = (tag, attrs.get('name', (0, 0, None))[2])
|
||||
if tag == 'category' and attrs['name'][2] == vr.LAUNCHER:
|
||||
launchers.append(owner)
|
||||
self.assertEqual(launchers, [('activity', '.Game'), ('activity-alias', None)])
|
||||
|
||||
def test_alias_with_launchable_activity_is_left_alone(self):
|
||||
original = manifest(launcher=True, alias='vr')
|
||||
info, _ = vr.inspect(original)
|
||||
|
||||
+1
-1
@@ -322,7 +322,7 @@ def patch(src, dst, add=None):
|
||||
original = frame_apk._read(z, 'AndroidManifest.xml', frame_apk.MAX_MANIFEST)
|
||||
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')
|
||||
raise FrameError('APK has no MAIN/LAUNCHER activity that Frame Control can patch')
|
||||
repack(src, dst, replace={'AndroidManifest.xml': manifest}, add=add)
|
||||
result = apk_info(dst)
|
||||
result.pop('icon_png', None)
|
||||
|
||||
+2
-1
@@ -10,7 +10,8 @@ import zipfile
|
||||
|
||||
# android: attribute resource ids; names can be stripped by shrinkers, ids can't.
|
||||
ATTR = {0x01010001: 'label', 0x01010002: 'icon', 0x01010003: 'name',
|
||||
0x01010024: 'value', 0x0101021b: 'versionCode', 0x0101021c: 'versionName', 0x0101020c: 'minSdkVersion'}
|
||||
0x01010024: 'value', 0x0101021b: 'versionCode', 0x0101021c: 'versionName', 0x0101020c: 'minSdkVersion',
|
||||
0x01010202: 'targetActivity'}
|
||||
T_REF, T_STRING, T_INT_DEC, T_INT_HEX = 0x01, 0x03, 0x10, 0x11
|
||||
# APKs can come from websites (install links), so nothing read from one may be
|
||||
# unbounded. zipfile stops at a member's declared size, so checking it is enough.
|
||||
|
||||
+9
-2
@@ -10,13 +10,18 @@ VR = {'com.oculus.intent.category.VR', 'org.khronos.openxr.intent.category.IMMER
|
||||
def inspect(data):
|
||||
elements = iter(frame_apk.manifest_elements(data))
|
||||
stack, filters, samsung = [], [], False
|
||||
current = None
|
||||
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
|
||||
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'}
|
||||
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)
|
||||
@@ -38,7 +43,9 @@ def inspect(data):
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user