Files
Artemis-Eapil/tests/native_actions_fixture/control_windows_jvm_harness.py
T

533 lines
36 KiB
Python

"""Explicit offline native-window integration checks; requires an existing JDK only.
Run with ``uv run python tests/native_actions_fixture/control_windows_jvm_harness.py``.
Compiles the repository's actual NativeControls/ControlWindowRoots against small
Android fakes. No device, service, network, APK, or configured model is accessed.
Generated sources/classes live only in a self-cleaning temporary directory.
"""
from pathlib import Path
from tempfile import TemporaryDirectory
from time import perf_counter
from touch_jvm_harness import find_tool, run_command
STUBS = {
"android/os/Build.java": """
package android.os;
public final class Build {public static final class VERSION {public static int SDK_INT=36;}}
""",
"android/os/Looper.java": """
package android.os;
public final class Looper {private static final Looper MAIN=new Looper();
public static Looper getMainLooper(){return MAIN;}public static Looper myLooper(){return MAIN;}}
""",
"android/os/Handler.java": """
package android.os;
public final class Handler {public Handler(Looper l){}public boolean post(Runnable r){r.run();return true;}
public void removeCallbacks(Runnable r){}}
""",
"android/os/Bundle.java": """
package android.os;import java.util.*;
public final class Bundle {private final Map<String,Object> values=new HashMap<>();
public void putFloat(String k,float v){values.put(k,v);}public void putInt(String k,int v){values.put(k,v);}
public void putCharSequence(String k,CharSequence v){values.put(k,v);}public Object get(String k){return values.get(k);}}
""",
"android/graphics/Rect.java": """
package android.graphics;
public final class Rect {public int left,top,right,bottom;public Rect(){}
public Rect(int l,int t,int r,int b){left=l;top=t;right=r;bottom=b;}
public boolean isEmpty(){return left>=right||top>=bottom;}
public static boolean intersects(Rect a,Rect b){return a.left<b.right&&b.left<a.right&&a.top<b.bottom&&b.top<a.bottom;}
public String flattenToString(){return left+" "+top+" "+right+" "+bottom;}
@Override public boolean equals(Object o){if(!(o instanceof Rect))return false;Rect r=(Rect)o;
return left==r.left&&top==r.top&&right==r.right&&bottom==r.bottom;}
@Override public int hashCode(){return left+31*top+961*right+29791*bottom;}}
""",
"android/graphics/Region.java": """
package android.graphics;import java.awt.geom.Area;import java.awt.geom.Rectangle2D;
// Independent standard-JDK geometry implements the Android Region operations used by the real helper.
public final class Region {
public enum Op {INTERSECT,DIFFERENCE,UNION}
private Area area=new Area();public Region(){}public Region(Rect r){set(r);}
private static Area rect(Rect r){return r.isEmpty()?new Area():new Area(new Rectangle2D.Double(
r.left,r.top,(double)r.right-r.left,(double)r.bottom-r.top));}
public boolean set(Rect r){area=rect(r);return !isEmpty();}
public boolean set(Region r){area=new Area(r.area);return !isEmpty();}
public void setEmpty(){area=new Area();}public boolean isEmpty(){return area.isEmpty();}
public boolean op(Rect r,Op op){Region other=new Region(r);return op(other,op);}
public boolean op(Region r,Op op){Area other=new Area(r.area);
if(op==Op.INTERSECT)area.intersect(other);else if(op==Op.DIFFERENCE)area.subtract(other);else area.add(other);
return !isEmpty();}
@Override public boolean equals(Object value){if(!(value instanceof Region))return false;
Area difference=new Area(area);difference.exclusiveOr(((Region)value).area);return difference.isEmpty();}
@Override public int hashCode(){return 0;}
}
""",
"org/json/JSONArray.java": """
package org.json;import java.util.*;
public final class JSONArray {private final List<Object> values=new ArrayList<>();
public JSONArray put(Object v){values.add(v);return this;}public int length(){return values.size();}
public Object get(int i){return values.get(i);}public JSONObject getJSONObject(int i){return (JSONObject)get(i);}
@Override public String toString(){return values.toString();}}
""",
"org/json/JSONObject.java": """
package org.json;import java.util.*;
public final class JSONObject {public static final Object NULL=new Object(){public String toString(){return "null";}};
private final Map<String,Object> values=new LinkedHashMap<>();
public JSONObject put(String k,Object v){values.put(k,v==null?NULL:v);return this;}
public boolean has(String k){return values.containsKey(k);}public Object opt(String k){return values.get(k);}
public Object get(String k){if(!has(k))throw new IllegalArgumentException(k);return opt(k);}
public String getString(String k){return (String)get(k);}public int getInt(String k){return ((Number)get(k)).intValue();}
public boolean getBoolean(String k){return (Boolean)get(k);}
public JSONObject getJSONObject(String k){return (JSONObject)get(k);}
public JSONArray getJSONArray(String k){return (JSONArray)get(k);}
public JSONObject optJSONObject(String k){Object v=opt(k);return v instanceof JSONObject?(JSONObject)v:null;}
public JSONArray optJSONArray(String k){Object v=opt(k);return v instanceof JSONArray?(JSONArray)v:null;}
public boolean optBoolean(String k,boolean d){Object v=opt(k);return v instanceof Boolean?(Boolean)v:d;}
public Iterator<String> keys(){return values.keySet().iterator();}
@Override public String toString(){return values.toString();}}
""",
"android/view/accessibility/AccessibilityNodeInfo.java": """
package android.view.accessibility;
import android.graphics.Rect;import android.os.Bundle;import java.util.*;
public final class AccessibilityNodeInfo {
public static final int ACTION_CLICK=16,ACTION_LONG_CLICK=32,ACTION_FOCUS=1,ACTION_SCROLL_FORWARD=4096,
ACTION_SCROLL_BACKWARD=8192,ACTION_SET_SELECTION=131072,ACTION_SET_TEXT=2097152;
public static final String ACTION_ARGUMENT_PROGRESS_VALUE="progress",ACTION_ARGUMENT_SELECTION_START_INT="start",
ACTION_ARGUMENT_SELECTION_END_INT="end",ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE="text";
public static int live,acquired,recycled,doubleRecycle;
public static final class Data {
public final int identity,window;public String resource="",text="",pkg="app",cls="FrameLayout";
public boolean packageThrows,childThrows,editable,scrollSupport,disableAfterText,visible=true,enabled=true,refresh=true;
public int left=10,top=100,right=110,bottom=200,selectionStart=0,selectionEnd=0;
public int actionsPerformed;public Data parent;public final List<Data> children=new ArrayList<>();
public Data(int id,int win){identity=id;window=win;}
public Data add(Data child){children.add(child);if(child!=null)child.parent=this;return this;}
}
private final Data data;private boolean released;
public AccessibilityNodeInfo(Data d){data=d;live++;acquired++;}
public static AccessibilityNodeInfo obtain(AccessibilityNodeInfo n){n.valid();return new AccessibilityNodeInfo(n.data);}
private void valid(){if(released)throw new IllegalStateException("recycled node reused");}
public void recycle(){if(released){doubleRecycle++;throw new IllegalStateException("double recycle");}
released=true;live--;recycled++;}
public int getWindowId(){valid();return data.window;}
public CharSequence getPackageName(){valid();if(data.packageThrows)throw new IllegalStateException("package failed");return data.pkg;}
public CharSequence getViewIdResourceName(){valid();return data.resource;}
public CharSequence getClassName(){valid();return data.cls;}public CharSequence getText(){valid();return data.text;}
public CharSequence getContentDescription(){valid();return "";}
public int getChildCount(){valid();return data.children.size();}
public AccessibilityNodeInfo getChild(int index){valid();if(data.childThrows)throw new IllegalStateException("child failed");
Data d=data.children.get(index);return d==null?null:new AccessibilityNodeInfo(d);}
public AccessibilityNodeInfo getParent(){valid();return data.parent==null?null:new AccessibilityNodeInfo(data.parent);}
public void getBoundsInScreen(Rect r){valid();r.left=data.left;r.top=data.top;r.right=data.right;r.bottom=data.bottom;}
public boolean refresh(){valid();return data.refresh;}
public boolean performAction(int id,Bundle args){valid();data.actionsPerformed++;
if(id==ACTION_SET_TEXT){data.text=(String)args.get(ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
if(data.disableAfterText)data.enabled=false;}
if(id==ACTION_SET_SELECTION){data.selectionStart=(Integer)args.get(ACTION_ARGUMENT_SELECTION_START_INT);
data.selectionEnd=(Integer)args.get(ACTION_ARGUMENT_SELECTION_END_INT);}return true;}
public boolean isVisibleToUser(){valid();return data.visible;}public boolean isEnabled(){valid();return data.enabled;}
public boolean isEditable(){valid();return data.editable;}public boolean isPassword(){valid();return false;}
public boolean isChecked(){valid();return false;}public boolean isCheckable(){valid();return false;}
public boolean isSelected(){valid();return false;}public boolean isFocused(){valid();return false;}
public boolean isScrollable(){valid();return true;}public boolean isShowingHintText(){valid();return false;}
public int getTextSelectionStart(){valid();return data.selectionStart;}public int getTextSelectionEnd(){valid();return data.selectionEnd;}
public RangeInfo getRangeInfo(){valid();return null;}
public List<AccessibilityAction> getActionList(){valid();List<AccessibilityAction> actions=new ArrayList<>();
actions.add(new AccessibilityAction(ACTION_CLICK));if(data.editable){actions.add(new AccessibilityAction(ACTION_SET_TEXT));
actions.add(new AccessibilityAction(ACTION_SET_SELECTION));}if(data.scrollSupport)actions.add(new AccessibilityAction(ACTION_SCROLL_FORWARD));
return actions;}
@Override public boolean equals(Object o){return o instanceof AccessibilityNodeInfo
&& data.identity==((AccessibilityNodeInfo)o).data.identity&&data.window==((AccessibilityNodeInfo)o).data.window;}
@Override public int hashCode(){return data.identity*31+data.window;}
public static final class RangeInfo {public static final int RANGE_TYPE_INT=0;
public float getMin(){return 0;}public float getMax(){return 100;}public float getCurrent(){return 50;}public int getType(){return 0;}}
public static final class AccessibilityAction {private final int id;public AccessibilityAction(int i){id=i;}public int getId(){return id;}
public static final AccessibilityAction ACTION_SHOW_ON_SCREEN=new AccessibilityAction(16908342),
ACTION_SET_PROGRESS=new AccessibilityAction(16908349),ACTION_SCROLL_UP=new AccessibilityAction(16908344),
ACTION_SCROLL_DOWN=new AccessibilityAction(16908346),ACTION_SCROLL_LEFT=new AccessibilityAction(16908345),
ACTION_SCROLL_RIGHT=new AccessibilityAction(16908347),ACTION_IME_ENTER=new AccessibilityAction(16908372);}
}
""",
"android/view/accessibility/AccessibilityWindowInfo.java": """
package android.view.accessibility;import android.graphics.Rect;import android.graphics.Region;
public final class AccessibilityWindowInfo {
public static final int TYPE_APPLICATION=1,TYPE_INPUT_METHOD=2,TYPE_SYSTEM=3;
public static int live,recycled,doubleRecycle;
public static final class Data {
public final int id,layer;public int type=TYPE_APPLICATION,display;
public boolean active,rootThrows,metadataThrows,boundsThrow,regionThrows;
public int[][] regions;
public int left=0,top=0,right=1080,bottom=2400;
public AccessibilityNodeInfo.Data root;public Data(int i,int l,boolean a,AccessibilityNodeInfo.Data r){id=i;layer=l;active=a;root=r;}
}
private final Data data;private boolean released;
public AccessibilityWindowInfo(Data d){data=d;live++;}
private void valid(){if(released)throw new IllegalStateException("recycled window reused");
if(data.metadataThrows)throw new IllegalStateException("metadata unavailable");}
public int getId(){valid();return data.id;}public int getType(){valid();return data.type;}
public int getLayer(){valid();return data.layer;}public int getDisplayId(){valid();return data.display;}
public boolean isActive(){valid();return data.active;}
public void getBoundsInScreen(Rect r){valid();if(data.boundsThrow)throw new IllegalStateException("bounds failed");
r.left=data.left;r.top=data.top;r.right=data.right;r.bottom=data.bottom;}
public void getRegionInScreen(Region r){valid();if(data.regionThrows)throw new IllegalStateException("region failed");
if(data.regions==null){r.set(new Rect(data.left,data.top,data.right,data.bottom));return;}
r.setEmpty();for(int[] part:data.regions)r.op(new Rect(part[0],part[1],part[2],part[3]),Region.Op.UNION);}
public AccessibilityNodeInfo getRoot(){valid();if(data.rootThrows)throw new IllegalStateException("root failed");
return data.root==null?null:new AccessibilityNodeInfo(data.root);}
public void recycle(){if(released){doubleRecycle++;throw new IllegalStateException("double window recycle");}
released=true;live--;recycled++;}
}
""",
"android/accessibilityservice/AccessibilityService.java": """
package android.accessibilityservice;
import android.view.accessibility.*;import java.util.*;
public final class AccessibilityService {
public AccessibilityNodeInfo.Data active;public final List<AccessibilityWindowInfo.Data> windows=new ArrayList<>();
public boolean windowsThrow,windowsNull,activeThrows;public int windowReads;public Runnable beforeWindowRead;
public AccessibilityNodeInfo getRootInActiveWindow(){if(activeThrows)throw new IllegalStateException("active root failed");
return active==null?null:new AccessibilityNodeInfo(active);}
public List<AccessibilityWindowInfo> getWindows(){windowReads++;if(beforeWindowRead!=null)beforeWindowRead.run();
if(windowsThrow)throw new IllegalStateException("windows unavailable");
if(windowsNull)return null;List<AccessibilityWindowInfo> result=new ArrayList<>();
IdentityHashMap<AccessibilityWindowInfo.Data,AccessibilityWindowInfo> seen=new IdentityHashMap<>();
for(AccessibilityWindowInfo.Data d:windows){if(d==null){result.add(null);continue;}
AccessibilityWindowInfo w=seen.get(d);if(w==null){w=new AccessibilityWindowInfo(d);seen.put(d,w);}result.add(w);}return result;}
}
""",
"com/artemis/helper/ControlWindowsTest.java": """
package com.artemis.helper;
import android.accessibilityservice.AccessibilityService;import android.view.accessibility.*;
import android.os.Build;import java.util.concurrent.atomic.AtomicBoolean;import org.json.*;
public final class ControlWindowsTest {
static int checks,scenarios,nextId=1;static String scenario;
static void check(boolean v,String m){checks++;if(!v)throw new AssertionError(scenario+": "+m);}
static void eq(Object a,Object e,String m){check(e.equals(a),m+" actual="+a+" expected="+e);}
static AccessibilityNodeInfo.Data node(int window,String resource){AccessibilityNodeInfo.Data d=new AccessibilityNodeInfo.Data(nextId++,window);d.resource=resource;return d;}
static AccessibilityWindowInfo.Data window(AccessibilityNodeInfo.Data root,int layer,boolean active){return new AccessibilityWindowInfo.Data(root.window,layer,active,root);}
static AccessibilityService app(){AccessibilityService s=new AccessibilityService();s.active=node(217,"empty-shell");
s.windows.add(window(s.active,0,true));return s;}
static AccessibilityNodeInfo.Data add(AccessibilityService s,int id,int layer,String resource){
AccessibilityNodeInfo.Data d=node(id,resource);s.windows.add(window(d,layer,false));return d;}
static void rect(AccessibilityNodeInfo.Data node,int l,int t,int r,int b){node.left=l;node.top=t;node.right=r;node.bottom=b;}
static void rect(AccessibilityWindowInfo.Data window,int l,int t,int r,int b){window.left=l;window.top=t;window.right=r;window.bottom=b;}
static JSONObject args(String resource){return new JSONObject().put("selector",new JSONObject().put("resource_id",resource));}
static JSONObject inspect(AccessibilityService s,String resource){return NativeControls.inspect(s,args(resource));}
static JSONObject click(AccessibilityService s,String resource){return NativeControls.act(s,args(resource).put("action","click"));}
static void code(JSONObject r,String expected){eq(r.getString("code"),expected,"receipt code");}
static void begin(String name){quiet();scenario=name;scenarios++;}
static void quiet(){check(AccessibilityNodeInfo.live==0,"all node handles recycled: "+AccessibilityNodeInfo.live);
check(AccessibilityWindowInfo.live==0,"all window handles recycled: "+AccessibilityWindowInfo.live);
check(AccessibilityNodeInfo.doubleRecycle==0,"no node double recycle");
check(AccessibilityWindowInfo.doubleRecycle==0,"no window double recycle");}
public static void main(String[] ignored){
begin("real empty active shell with inactive upper player");AccessibilityService s=app();
AccessibilityNodeInfo.Data player=add(s,216,1,"player");player.add(node(216,"tv_timeline_date")).add(node(216,"tab_play_or_pause"));
JSONObject r=inspect(s,"tv_timeline_date");code(r,"OK");eq(r.getString("execution"),"observed","read only receipt");
eq(r.getJSONObject("node").getString("resource_id"),"tv_timeline_date","upper player resolved");
r=click(s,"tab_play_or_pause");code(r,"OK");eq(r.getString("execution"),"completed","write receipt");
eq(player.children.get(1).actionsPerformed,1,"exactly one upper-window action");
code(inspect(s,"empty-shell"),"OK");
begin("active modal excludes its lower application");s=app();s.windows.clear();s.windows.add(window(s.active,3,true));
AccessibilityNodeInfo.Data lower=add(s,216,1,"behind-dialog");code(inspect(s,"behind-dialog"),"NOT_FOUND");
code(click(s,"behind-dialog"),"NOT_FOUND");eq(lower.actionsPerformed,0,"no lower-window input");code(inspect(s,"empty-shell"),"OK");
begin("other packages, system and IME never become candidate controls");s=app();
AccessibilityNodeInfo.Data other=add(s,216,1,"outside");other.pkg="other.app";
AccessibilityNodeInfo.Data system=add(s,218,2,"outside");s.windows.get(2).type=AccessibilityWindowInfo.TYPE_SYSTEM;
s.windows.get(2).rootThrows=true;AccessibilityNodeInfo.Data ime=add(s,219,3,"outside");
s.windows.get(3).type=AccessibilityWindowInfo.TYPE_INPUT_METHOD;s.windows.get(3).rootThrows=true;
code(click(s,"outside"),"NOT_FOUND");eq(other.actionsPerformed+system.actionsPerformed+ime.actionsPerformed,0,"no foreign input");
begin("different display cannot share layer ordering");s=app();add(s,216,1,"outside");s.windows.get(1).display=7;
code(inspect(s,"outside"),"NOT_FOUND");
begin("API 24 window metadata remains usable");Build.VERSION.SDK_INT=24;s=app();add(s,216,1,"old-api");
code(inspect(s,"old-api"),"OK");Build.VERSION.SDK_INT=36;
begin("matches across active and upper roots are globally ambiguous");s=app();s.active.resource="duplicate";
AccessibilityNodeInfo.Data duplicate=add(s,216,1,"duplicate");r=click(s,"duplicate");code(r,"AMBIGUOUS");
eq(r.getInt("candidate_count"),2,"global count");eq(r.getJSONArray("candidates").length(),2,"global candidates");
eq(s.active.actionsPerformed+duplicate.actionsPerformed,0,"ambiguity performs no action");
begin("same-layer application still participates in uniqueness");s=app();s.active.resource="same-layer";
add(s,216,0,"same-layer");code(inspect(s,"same-layer"),"AMBIGUOUS");
begin("duplicate window handles and root snapshots do not invent ambiguity");s=app();add(s,216,1,"unique");
s.windows.add(s.windows.get(1));s.windows.add(new AccessibilityWindowInfo.Data(216,1,false,s.windows.get(1).root));
s.windows.add(s.windows.get(0));code(inspect(s,"unique"),"OK");code(click(s,"unique"),"OK");
eq(s.windows.get(1).root.actionsPerformed,1,"duplicate Region metadata permits only one native action");
begin("eligible unreadable roots invalidate an otherwise unique active match");s=app();
add(s,216,1,"anything");s.windows.get(1).root=null;code(click(s,"empty-shell"),"READ_FAILED");
eq(s.active.actionsPerformed,0,"unreadable candidate never permits fake uniqueness");code(inspect(s,"absent"),"READ_FAILED");
begin("candidate getRoot exception does not become absence");s=app();add(s,216,1,"anything");
s.windows.get(1).rootThrows=true;code(inspect(s,"absent"),"READ_FAILED");
begin("candidate package missing or unreadable fails closed");s=app();AccessibilityNodeInfo.Data bad=add(s,216,1,"anything");
bad.pkg=null;code(inspect(s,"absent"),"READ_FAILED");bad.pkg="";code(inspect(s,"absent"),"READ_FAILED");
bad.pkg="app";bad.packageThrows=true;code(inspect(s,"absent"),"READ_FAILED");
begin("candidate root identity changed during discovery");s=app();add(s,216,1,"anything");
s.windows.get(1).root=node(999,"anything");code(inspect(s,"absent"),"READ_FAILED");
begin("candidate child cannot be read");s=app();bad=add(s,216,1,"upper");bad.add(null);
code(inspect(s,"absent"),"READ_FAILED");bad.children.clear();bad.add(node(216,"child"));bad.childThrows=true;
code(inspect(s,"absent"),"READ_FAILED");
begin("scope resolves in upper window, target remains confined to its descendants");s=app();
AccessibilityNodeInfo.Data scope=add(s,216,1,"container");scope.add(node(216,"inside"));s.active.add(node(217,"outside"));
JSONObject scoped=args("inside").put("scope",new JSONObject().put("resource_id","container"));
code(NativeControls.inspect(s,scoped),"OK");scoped.put("selector",new JSONObject().put("resource_id","outside"));
code(NativeControls.inspect(s,scoped),"NOT_FOUND");
begin("scopes themselves must remain globally unique");s=app();s.active.resource="container";add(s,216,1,"container");
code(NativeControls.inspect(s,args("inside").put("scope",new JSONObject().put("resource_id","container"))),"AMBIGUOUS");
begin("candidate array remains capped across windows");s=app();s.active.resource="duplicate";
for(int i=0;i<12;i++)add(s,300+i,1,"duplicate");r=inspect(s,"duplicate");code(r,"AMBIGUOUS");
eq(r.getInt("candidate_count"),13,"all matches counted");eq(r.getJSONArray("candidates").length(),10,"candidate snapshot cap");
begin("node budget is shared across roots, not reset for each window");s=app();
AccessibilityNodeInfo.Data upper=add(s,216,1,"upper");for(int i=0;i<3000;i++){
s.active.add(node(217,"active-child"));upper.add(node(216,"upper-child"));}
code(inspect(s,"absent"),"LIMIT_EXCEEDED");
begin("search-limit failure cannot dispatch an early unique match");s.active.resource="unique";
code(click(s,"unique"),"LIMIT_EXCEEDED");eq(s.active.actionsPerformed,0,"bounded search proves no fake uniqueness");
begin("window enumeration has a hard upper bound with complete cleanup");s=app();
for(int i=0;i<65;i++)add(s,300+i,1,"upper");code(inspect(s,"absent"),"LIMIT_EXCEEDED");
begin("missing active root is unavailable, never absent");s=app();s.active=null;
code(inspect(s,"absent"),"UNAVAILABLE");
begin("empty, missing and unavailable metadata cannot prove uniqueness or absence");s=app();s.windows.clear();
code(inspect(s,"empty-shell"),"UNAVAILABLE");code(inspect(s,"absent"),"UNAVAILABLE");s.windowsNull=true;
code(inspect(s,"empty-shell"),"UNAVAILABLE");s.windowsNull=false;s.windowsThrow=true;code(inspect(s,"empty-shell"),"UNAVAILABLE");
begin("missing active window cannot broaden to an unrelated observed application");s=app();s.windows.clear();add(s,216,1,"upper");
code(inspect(s,"upper"),"UNAVAILABLE");code(inspect(s,"empty-shell"),"UNAVAILABLE");
begin("active metadata is not current");s=app();s.windows.get(0).active=false;add(s,216,1,"upper");
code(inspect(s,"upper"),"UNAVAILABLE");
begin("active metadata unavailable remains conservative");s=app();s.windows.get(0).metadataThrows=true;add(s,216,1,"upper");
code(inspect(s,"upper"),"UNAVAILABLE");
begin("declared candidate metadata missing after active metadata is established");s=app();s.windows.add(null);
code(inspect(s,"absent"),"READ_FAILED");
begin("single known application window permits one visible native action");s=app();
code(click(s,"empty-shell"),"OK");eq(s.active.actionsPerformed,1,"single-window input");
begin("invisible targets remain observable but cannot receive native actions");s=app();s.active.visible=false;
code(inspect(s,"empty-shell"),"OK");code(click(s,"empty-shell"),"NOT_VISIBLE");
eq(s.active.actionsPerformed,0,"invisible target never written");
begin("nonfocusable same-package higher modal blocks retained active root writes");s=app();add(s,216,1,"modal");
code(inspect(s,"empty-shell"),"OK");r=click(s,"empty-shell");code(r,"OCCLUDED");
eq(r.getString("execution"),"not_started","blocked before input");eq(s.active.actionsPerformed,0,"no modal penetration");
begin("any higher package or window type blocks full coverage");
for(int type:new int[]{1,2,3}){s=app();AccessibilityNodeInfo.Data blocker=add(s,216,1,"blocker");
blocker.pkg="other.app";s.windows.get(1).type=type;code(click(s,"empty-shell"),"OCCLUDED");
eq(s.active.actionsPerformed,0,"foreign/system/IME overlap blocked");}
begin("higher nonoverlapping status bar leaves the app target actionable");s=app();
add(s,216,1,"status-bar");s.windows.get(1).type=AccessibilityWindowInfo.TYPE_SYSTEM;s.windows.get(1).bottom=48;
code(click(s,"empty-shell"),"OK");eq(s.active.actionsPerformed,1,"nonoverlapping system bar allowed");
begin("empty higher bounds do not claim occlusion");s=app();add(s,216,1,"empty-window");
s.windows.get(1).right=0;code(click(s,"empty-shell"),"OK");
begin("higher windows on another display do not occlude this display");s=app();add(s,216,1,"other-display");
s.windows.get(1).display=7;code(click(s,"empty-shell"),"OK");
begin("own window disappearance between resolution and action is unavailable");
final AccessibilityService removed=app();removed.beforeWindowRead=()->{if(removed.windowReads==2)removed.windows.clear();};
code(click(removed,"empty-shell"),"UNAVAILABLE");eq(removed.active.actionsPerformed,0,"missing context blocks input");
begin("a new overlapping window is checked immediately before action");
final AccessibilityService newlyCovered=app();newlyCovered.beforeWindowRead=()->{
if(newlyCovered.windowReads==2)add(newlyCovered,216,1,"new-modal");};
code(click(newlyCovered,"empty-shell"),"OCCLUDED");eq(newlyCovered.active.actionsPerformed,0,"new modal blocks input");
begin("latest metadata read failure never permits input");
final AccessibilityService latestFailure=app();latestFailure.beforeWindowRead=()->{
if(latestFailure.windowReads==2)latestFailure.windowsThrow=true;};
code(click(latestFailure,"empty-shell"),"UNAVAILABLE");eq(latestFailure.active.actionsPerformed,0,"failed latest query blocks input");
begin("unknown target window bounds are unavailable");s=app();s.windows.get(0).boundsThrow=true;
code(click(s,"empty-shell"),"UNAVAILABLE");eq(s.active.actionsPerformed,0,"unknown bounds block input");
begin("unknown active display cannot prove complete resolution");s=app();s.windows.get(0).display=-1;
code(inspect(s,"empty-shell"),"UNAVAILABLE");code(inspect(s,"absent"),"UNAVAILABLE");
begin("unknown potential occluder display cannot authorize input");s=app();add(s,216,1,"system-window");
s.windows.get(1).type=AccessibilityWindowInfo.TYPE_SYSTEM;s.windows.get(1).display=-1;
code(click(s,"empty-shell"),"UNAVAILABLE");eq(s.active.actionsPerformed,0,"unknown display blocks input");
begin("empty target or own-window bounds cannot authorize input");s=app();s.active.right=s.active.left;
code(click(s,"empty-shell"),"UNAVAILABLE");s=app();s.windows.get(0).right=0;
code(click(s,"empty-shell"),"UNAVAILABLE");
begin("post-edit caret action rechecks latest windows and retains unknown partial receipt");
final AccessibilityService edit=app();edit.active.editable=true;edit.active.text="abc";
edit.beforeWindowRead=()->{if(edit.windowReads==3)add(edit,216,1,"new-overlay");};
r=NativeControls.act(edit,args("empty-shell").put("action","replace_selection").put("text","Z")
.put("selection_start",0).put("selection_end",1));code(r,"OCCLUDED");
eq(r.getString("execution"),"unknown","accepted text with blocked caret stays unknown");
eq(edit.active.text,"Zbc","accepted edit retained");eq(edit.active.actionsPerformed,1,"caret not dispatched through overlay");
begin("real code14 editor selection toolbar and handles leave editable area exposed");
s=new AccessibilityService();s.active=node(231,"activity");s.windows.add(window(s.active,0,true));
rect(s.windows.get(0),0,0,720,1600);AccessibilityNodeInfo.Data editor=node(231,"editor");
editor.editable=true;editor.text="A😀B中文";rect(editor,24,534,696,638);s.active.add(editor);
add(s,232,4,"selection-toolbar");rect(s.windows.get(1),32,446,554,542);
add(s,233,2,"selection-handle-left");rect(s.windows.get(2),1,611,76,691);
add(s,234,3,"selection-handle-right");rect(s.windows.get(3),77,611,165,691);
add(s,226,5,"status-bar");s.windows.get(4).type=AccessibilityWindowInfo.TYPE_SYSTEM;rect(s.windows.get(4),0,0,720,48);
add(s,218,1,"keyboard");s.windows.get(5).type=AccessibilityWindowInfo.TYPE_INPUT_METHOD;rect(s.windows.get(5),0,1002,720,1600);
r=NativeControls.act(s,args("editor").put("action","replace_selection").put("text"," X ")
.put("selection_start",1).put("selection_end",3));code(r,"OK");
eq(r.getString("execution"),"completed","partial occlusion permits native edit");eq(editor.text,"A X B中文","UTF-16 replacement retained");
eq(editor.actionsPerformed,2,"text and caret each accepted");eq(editor.selectionStart,4,"caret restored after inserted whitespace");
begin("partial higher overlap permits a native action on the exposed part");s=app();add(s,216,1,"partial-overlay");
rect(s.windows.get(1),0,0,60,300);code(click(s,"empty-shell"),"OK");eq(s.active.actionsPerformed,1,"exposed part remains actionable");
begin("multiple higher windows jointly covering the target deny input");s=app();add(s,216,1,"cover-left");
rect(s.windows.get(1),0,0,60,300);add(s,218,2,"cover-right");rect(s.windows.get(2),60,0,200,300);
code(click(s,"empty-shell"),"OCCLUDED");eq(s.active.actionsPerformed,0,"union coverage blocks input");
begin("one pixel gap in joint coverage remains exposed");s=app();add(s,216,1,"cover-left");
rect(s.windows.get(1),0,0,60,300);add(s,218,2,"cover-right");rect(s.windows.get(2),61,0,200,300);
code(click(s,"empty-shell"),"OK");eq(s.active.actionsPerformed,1,"integer region gap preserved");
begin("target area outside its own window cannot bypass full coverage inside");s=app();
rect(s.windows.get(0),10,100,60,200);add(s,216,1,"cover-visible-half");rect(s.windows.get(1),10,100,60,200);
code(click(s,"empty-shell"),"OCCLUDED");eq(s.active.actionsPerformed,0,"own-window clipping precedes occlusion");
begin("overlap only outside the own-window clip does not block exposed area");s=app();
rect(s.windows.get(0),10,100,60,200);add(s,216,1,"outside-own-window");rect(s.windows.get(1),60,100,110,200);
code(click(s,"empty-shell"),"OK");eq(s.active.actionsPerformed,1,"outside clip is irrelevant");
begin("a target completely outside its own window is unavailable");s=app();rect(s.windows.get(0),0,0,5,50);
code(click(s,"empty-shell"),"UNAVAILABLE");eq(s.active.actionsPerformed,0,"invalid clip cannot authorize input");
begin("screen-sized container can scroll between system and IME edge windows");s=app();
rect(s.active,0,0,720,1600);rect(s.windows.get(0),0,0,720,1600);s.active.scrollSupport=true;
add(s,216,2,"status-bar");s.windows.get(1).type=AccessibilityWindowInfo.TYPE_SYSTEM;rect(s.windows.get(1),0,0,720,48);
add(s,218,1,"keyboard");s.windows.get(2).type=AccessibilityWindowInfo.TYPE_INPUT_METHOD;rect(s.windows.get(2),0,1002,720,1600);
code(NativeControls.act(s,args("empty-shell").put("action","scroll_forward")),"OK");
eq(s.active.actionsPerformed,1,"visible middle permits native scroll");
begin("API30 own L-shaped region does not expose a target inside its bounding-box hole");Build.VERSION.SDK_INT=30;s=app();
rect(s.active,50,100,100,150);rect(s.windows.get(0),0,0,200,300);
s.windows.get(0).regions=new int[][]{{0,0,20,300},{0,0,200,50}};
code(click(s,"empty-shell"),"OCCLUDED");eq(s.active.actionsPerformed,0,"own L-shape hole blocks input");Build.VERSION.SDK_INT=36;
begin("API30 own disconnected region cannot authorize a target in its gap");s=app();
rect(s.active,50,100,90,200);rect(s.windows.get(0),0,0,140,300);
s.windows.get(0).regions=new int[][]{{0,0,40,300},{100,0,140,300}};
code(click(s,"empty-shell"),"OCCLUDED");eq(s.active.actionsPerformed,0,"own disconnected gap blocks input");
begin("API30 higher region hole leaves the target actionable despite bounding-box overlap");s=app();
add(s,216,1,"hollow-overlay");rect(s.windows.get(1),0,0,200,300);
s.windows.get(1).regions=new int[][]{{0,0,200,100},{0,200,200,300},{0,100,10,200},{110,100,200,200}};
code(click(s,"empty-shell"),"OK");eq(s.active.actionsPerformed,1,"actual overlay hole stays exposed");
begin("API30 disconnected higher regions jointly covering all target area deny input");s=app();
add(s,216,1,"alternating-a");rect(s.windows.get(1),10,100,85,200);
s.windows.get(1).regions=new int[][]{{10,100,35,200},{60,100,85,200}};
add(s,218,2,"alternating-b");rect(s.windows.get(2),35,100,110,200);
s.windows.get(2).regions=new int[][]{{35,100,60,200},{85,100,110,200}};
code(click(s,"empty-shell"),"OCCLUDED");eq(s.active.actionsPerformed,0,"union of disconnected regions blocks input");
begin("API29 uses conservative rectangular bounds without calling unsupported Region getter");
Build.VERSION.SDK_INT=29;s=app();add(s,216,1,"hollow-overlay");rect(s.windows.get(1),0,0,200,300);
s.windows.get(1).regions=new int[][]{{0,0,200,100},{0,200,200,300}};s.windows.get(1).regionThrows=true;
s.windows.get(0).regionThrows=true;code(click(s,"empty-shell"),"OCCLUDED");Build.VERSION.SDK_INT=36;
begin("API24 rectangular subtraction still permits partially exposed targets");Build.VERSION.SDK_INT=24;
s=app();add(s,216,1,"partial-overlay");rect(s.windows.get(1),0,0,60,300);
s.windows.get(0).regionThrows=true;s.windows.get(1).regionThrows=true;
code(click(s,"empty-shell"),"OK");Build.VERSION.SDK_INT=36;
begin("API30 failed Region metadata cannot silently fall back to rectangular bounds");s=app();
s.windows.get(0).regionThrows=true;code(click(s,"empty-shell"),"UNAVAILABLE");eq(s.active.actionsPerformed,0,"own Region failure blocks input");
s=app();add(s,216,1,"system-window");s.windows.get(1).type=AccessibilityWindowInfo.TYPE_SYSTEM;
s.windows.get(1).regionThrows=true;code(click(s,"empty-shell"),"UNAVAILABLE");
eq(s.active.actionsPerformed,0,"higher Region failure blocks input");
begin("post-edit disabled control prevents the caret action and preserves partial unknown");s=app();
s.active.editable=true;s.active.text="abc";s.active.disableAfterText=true;
r=NativeControls.act(s,args("empty-shell").put("action","replace_selection").put("text","Z")
.put("selection_start",0).put("selection_end",1));code(r,"DISABLED");eq(r.getString("execution"),"unknown","disabled caret is partial");
eq(s.active.text,"Zbc","accepted edit remains");eq(s.active.actionsPerformed,1,"disabled caret not dispatched");
begin("pre-cancelled discovery does not acquire input or roots");s=app();
try(ControlWindowRoots roots=ControlWindowRoots.capture(s,new AtomicBoolean(true))){
eq(roots.error,"CANCELLED","cancelled discovery");eq(roots.size(),0,"no root acquired");}
quiet();System.out.println("PASS "+checks+" checks across "+scenarios+" native-window scenarios");
}
}
""",
}
def main() -> int:
started = perf_counter()
try:
java, javac = find_tool("java"), find_tool("javac")
helper = (
Path(__file__).resolve().parents[2]
/ "packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper"
)
actual = [helper / name for name in ("NativeControls.java", "ControlWindowRoots.java")]
if not all(path.is_file() for path in actual):
raise RuntimeError("The repository native-control Java sources are missing")
print("Offline Android-window simulation against actual Java; no device input.", flush=True)
with TemporaryDirectory(prefix="artemis-control-windows-jvm-") as temporary:
workspace = Path(temporary)
classes = workspace / "classes"
classes.mkdir()
generated = []
for name, source in STUBS.items():
destination = workspace / "src" / name
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(source.lstrip("\n"), encoding="utf-8")
generated.append(str(destination))
run_command(
[
javac,
"-encoding",
"UTF-8",
"-source",
"8",
"-target",
"8",
"-Xlint:-options",
"-d",
str(classes),
*generated,
*(str(path) for path in actual),
],
timeout=60,
)
run_command([java, "-cp", str(classes), "com.artemis.helper.ControlWindowsTest"], 30)
print(f"Native-window harness completed in {perf_counter() - started:.2f}s.")
return 0
except Exception as error:
print(f"FAIL: {error}")
return 1
if __name__ == "__main__":
raise SystemExit(main())