File size: 3,488 Bytes
b7fc81a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
"""Explicit execution plans for Native134. No device is touched on import.

Desktop backend is PyAutoGUI plus injected Unicode paste. Android backend is
an injected argv-only ADB runner plus Unicode type callback. Lifecycle callbacks
are explicit, so interact/answer/failure cannot silently become task success.
"""
from __future__ import annotations
import re


def plan(codec, action, platform, size, *, installed_apps=None):
    a=codec.adapt_target(action,platform).canonical_action
    if len(size)!=2 or any(type(d) is not int or d<=0 for d in size): raise ValueError('invalid_screen_size')
    def pixel(name):
        return [min(d-1,int(v*d/1000+.5)) for v,d in zip(a[name],size)]
    name=a['action']; args=[]
    if name in ('terminate','answer','interact','wait'):
        field={'terminate':'status','answer':'text','interact':'text','wait':'time'}[name]
        return [(name,[a[field]])]
    if name=='type': return [('type_unicode',[a['text']])]
    if platform=='desktop':
        if name=='key': return [('hotkey',a['keys'])]
        if name=='mouse_move': return [('moveTo',pixel('coordinate'))]
        if name=='left_click_drag': return [('dragTo',pixel('coordinate')+[.5])]
        if name in ('scroll','hscroll'):
            before=[('moveTo',pixel('coordinate'))] if 'coordinate' in a else []
            return before+[(name,[a['pixels']])]
        buttons={'left_click':('left',1),'right_click':('right',1),'middle_click':('middle',1),
                 'double_click':('left',2),'triple_click':('left',3)}
        button,n=buttons[name]
        return [('click',pixel('coordinate')+[button,n])]
    if name=='click': args=['input','tap',*pixel('coordinate')]
    elif name=='swipe': args=['input','swipe',*pixel('coordinate'),*pixel('coordinate2'),800]
    elif name=='long_press':
        args=['input','swipe',*pixel('coordinate'),*pixel('coordinate'),int(a['time']*1000+.5)]
    elif name=='system_button':
        args=['input','keyevent',{'Back':4,'Home':3,'Menu':82,'Enter':66}[a['button']]]
    elif name=='key':
        key=a['text'].upper()
        if not key.startswith('KEYCODE_'): key='KEYCODE_'+key
        if not re.fullmatch(r'KEYCODE_[A-Z0-9_]+',key): raise ValueError('invalid_android_key')
        args=['input','keyevent',key]
    elif name=='open':
        apps=installed_apps or {}
        package=apps.get(a['text'])
        if package is None and a['text'] in apps.values(): package=a['text']
        if package is None: raise ValueError('app_not_in_verified_installed_mapping')
        if not re.fullmatch(r'[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)+',package): raise ValueError('invalid_app_package')
        args=['monkey','-p',package,'-c','android.intent.category.LAUNCHER','1']
    else: raise ValueError('unimplemented_action')
    return [('adb_shell',[str(x) for x in args])]


def execute(operations, *, desktop=None, adb_shell=None, type_unicode=None, callbacks=None):
    """Callers inject existing VM/ADB connections; never build shell strings."""
    callbacks=callbacks or {}
    for name,args in operations:
        if name in ('terminate','answer','interact','wait'): callbacks[name](*args)
        elif name=='adb_shell': adb_shell(args)
        elif name=='type_unicode': type_unicode(*args)
        elif name=='click': desktop.click(args[0],args[1],button=args[2],clicks=args[3],interval=.1)
        elif name=='dragTo': desktop.dragTo(args[0],args[1],duration=args[2],button='left')
        else: getattr(desktop,name)(*args)