JevEvidence.java

原始文件

1package com.android.commands.monkey.ape.jev;
2
3import java.text.Normalizer;
4import java.security.MessageDigest;
5import java.util.Iterator;
6import java.util.ArrayDeque;
7import java.util.Collections;
8import java.util.HashSet;
9import java.util.Set;
10import java.util.LinkedHashSet;
11import org.json.*;
12import com.android.commands.monkey.ape.model.ModelAction;
13import com.android.commands.monkey.ape.model.State;
14import com.android.commands.monkey.ape.utils.StringCache;
15import com.android.commands.monkey.ape.tree.GUITreeNode;
16import com.android.commands.monkey.ape.tree.GUITree;
17
18/** Values derived from APE's naming; no alternative state or action identity. */
19public final class JevEvidence {
20    public static final int MAX_TEXT_CODEPOINTS=512;
21    private static final String TRUNCATED="…[truncated]";
22
23    public static String boundedText(String text) { return boundedText(text,MAX_TEXT_CODEPOINTS); }
24    public static String boundedText(String text,int limit) {
25        if(text==null)return "";
26        if(text.codePointCount(0,text.length())<=limit)return text;
27        int prefix=limit-TRUNCATED.codePointCount(0,TRUNCATED.length());
28        return text.substring(0,text.offsetByCodePoints(0,prefix))+TRUNCATED;
29    }
30    public static String boundedLabel(CharSequence text) {
31        return boundedText(normalize(StringCache.removeQuotes(text)));
32    }
33    private JevEvidence() { }
34    public static String normalize(String value) {
35        if (value == null) return "";
36        String n = Normalizer.normalize(value, Normalizer.Form.NFC);
37        StringBuilder out = new StringBuilder();
38        boolean space=false;
39        for (int offset=0;offset<n.length();) {
40            int cp=n.codePointAt(offset); offset+=Character.charCount(cp);
41            if (Character.isWhitespace(cp) || Character.isSpaceChar(cp)) { space=out.length()>0; }
42            else { if(space) out.append(' '); out.appendCodePoint(cp); space=false; }
43        }
44        return out.toString();
45    }
46    public static String digest(String value) {
47        try {
48            byte[] hash=MessageDigest.getInstance("SHA-256").digest(value.getBytes("UTF-8"));
49            StringBuilder b=new StringBuilder();
50            for(byte v:hash) b.append(String.format(java.util.Locale.ROOT,"%02x",v & 255));
51            return b.toString();
52        } catch(Exception e) { throw new IllegalStateException(e); }
53    }
54    public static JSONArray labels(GUITreeNode node) {
55        JSONArray labels=new JSONArray();
56        collect(node,0,labels); return labels;
57    }
58    private static void collect(GUITreeNode node,int depth,JSONArray labels) {
59        if(node==null || depth>3 || labels.length()>=33) return;
60        labels.put(new JSONArray().put(normalize(JevIdentity.text(node))).put(normalize(node.getContentDesc())));
61        Iterator<GUITreeNode> it=node.getChildren();
62        while(it.hasNext() && labels.length()<33) collect(it.next(),depth+1,labels);
63    }
64    private static boolean password(GUITreeNode node,int depth,int[] count) {
65        if(node==null || depth>3 || count[0]>=33) return false;
66        count[0]++;
67        if(rawPassword(node)) return true;
68        Iterator<GUITreeNode> it=node.getChildren();
69        while(it.hasNext() && count[0]<33) if(password(it.next(),depth+1,count)) return true;
70        return false;
71    }
72    public static JevRegistry.Member member(String type,GUITreeNode node) {
73        boolean secret=password(node,0,new int[1]);
74        for(GUITreeNode p=node;p!=null;p=p.getParent()) secret |= rawPassword(p);
75        JSONArray roles=roleSignature(node),labels=fullLabels(node),extraLabels=new JSONArray();
76        Set<String> seen=new HashSet<String>();
77        for(int i=0;i<roles.length();i++) {
78            JSONObject role=roles.optJSONObject(i);
79            seen.add(new JSONArray().put(role.optString("text")).put(role.optString("content_desc")).toString());
80        }
81        seen.add(new JSONArray().put("").put("").toString());
82        // Keep each label once; stateful descendant labels stay attached to their state.
83        for(int i=0;i<labels.length();i++) {
84            JSONArray pair=labels.optJSONArray(i);
85            if(seen.add(pair.toString()))extraLabels.put(pair);
86        }
87        JSONObject local=probeLocalContext(node);
88        if(local!=null && local.optJSONArray("labels").length()==0)local=null;
89        return new JevRegistry.Member(type,extraLabels,roles,ConcreteActionIdentity.canonical(node),secret,local);
90    }
91
92    /** Detailed target, followed only by compact check/selection states of descendants. */
93    public static JSONArray roleSignature(GUITreeNode node) {
94        JSONArray out=new JSONArray();
95        collectRole(node,0,out,new int[1]);
96        return out;
97    }
98    private static void collectRole(GUITreeNode node,int depth,JSONArray out,int[] scanned) {
99        if(node==null || depth>3 || out.length()>=8 || scanned[0]>=33)return;
100        scanned[0]++;
101        boolean checkable=node.isCheckable() || node.isChecked();
102        if(depth==0 || checkable || Boolean.TRUE.equals(node.getJevSelected())) {
103            try {
104                JSONObject role=new JSONObject().put("class",boundedText(node.getClassName()))
105                    .put("resource_id",boundedText(node.getResourceID()))
106                    .put("text",rawText(node)).put("content_desc",rawContentDesc(node))
107                    .put("selected",node.getJevSelected()==null?JSONObject.NULL:node.getJevSelected())
108                    .put("enabled",node.isEnabled());
109                if(depth==0) {
110                    android.graphics.Rect b=node.getBoundsInScreen();
111                    role.put("inputText",rawInputText(node)).put("password",rawPassword(node))
112                        .put("index",node.getIndex()).put("clickable",node.isClickable())
113                        .put("long_clickable",node.isLongClickable()).put("checkable",node.isCheckable())
114                        .put("scrollable",node.isScrollable()).put("checked",node.isChecked())
115                        .put("focusable",node.isFocusable()).put("focused",node.isFocused())
116                        .put("bounds",b==null?JSONObject.NULL:new JSONArray().put(b.left).put(b.top).put(b.right).put(b.bottom));
117                } else role.put("checked",checkable?node.isChecked():JSONObject.NULL);
118                out.put(role);
119            } catch(JSONException e) { throw new IllegalStateException(e); }
120        }
121        Iterator<GUITreeNode> it=node.getChildren();
122        while(it.hasNext() && out.length()<8 && scanned[0]<33) collectRole(it.next(),depth+1,out,scanned);
123    }
124    static String rawText(GUITreeNode node) {
125        if(node==null)return "";
126        if(node.getJevText()!=null)return node.getJevText();
127        return boundedLabel(node.getNodeInfo()==null?JevIdentity.text(node):node.getNodeInfo().getText());
128    }
129    private static String rawContentDesc(GUITreeNode node) {
130        if(node==null)return "";
131        if(node.getJevContentDesc()!=null)return node.getJevContentDesc();
132        return boundedLabel(node.getNodeInfo()==null?node.getContentDesc():node.getNodeInfo().getContentDescription());
133    }
134    private static String rawInputText(GUITreeNode node) {
135        return node==null?"":boundedText(node.getInputText());
136    }
137    private static boolean rawPassword(GUITreeNode node) {
138        return node.getJevPassword()==null?node.isPassword():node.getJevPassword();
139    }
140    private static JSONArray fullLabels(GUITreeNode node) {
141        JSONArray out=new JSONArray(); fullCollect(node,0,out); return out;
142    }
143    private static void fullCollect(GUITreeNode node,int depth,JSONArray out) {
144        if(node==null || depth>3 || out.length()>=33)return;
145        out.put(new JSONArray().put(rawText(node)).put(rawContentDesc(node)));
146        Iterator<GUITreeNode> it=node.getChildren(); while(it.hasNext() && out.length()<33)fullCollect(it.next(),depth+1,out);
147    }
148    /** Bounded source-page observations; labels are clues, not inferred titles or modes. */
149    public static JSONObject sourceContext(GUITree tree) {
150        JSONArray labels=new JSONArray();ArrayDeque<GUITreeNode> pending=new ArrayDeque<GUITreeNode>();
151        pending.add(tree.getRootNode());int scanned=0;boolean truncated=false;
152        while(!pending.isEmpty() && scanned<128 && labels.length()<8) {
153            GUITreeNode node=pending.removeFirst();scanned++;
154            String text=rawText(node),desc=rawContentDesc(node);
155            if(!text.isEmpty() || !desc.isEmpty())labels.put(JevRegistry.fields(
156                "resource_id",boundedText(node.getResourceID(),128),"text",boundedText(text,128),
157                "content_desc",boundedText(desc,128),"selected",node.getJevSelected()==null?JSONObject.NULL:node.getJevSelected()));
158            Iterator<GUITreeNode> children=node.getChildren();
159            while(children.hasNext()) {
160                if(scanned+pending.size()>=128){truncated=true;break;}
161                pending.addLast(children.next());
162            }
163        }
164        return JevRegistry.fields("activity",boundedText(tree.getActivityClassName()),"page_labels",labels,
165            "truncated",truncated || !pending.isEmpty());
166    }
167    /** Region's direct-parent evidence; paths express structure, not ownership. */
168    static JSONObject localContext(GUITreeNode candidate) {
169        return localContextAt(candidate,candidate==null?null:candidate.getParent());
170    }
171
172    /**
173     * Source-side context captured before a concrete action is executed.  This
174     * is diagnostic evidence only; it does not participate in matching or
175     * action selection.  Keep the production matcher identity fields beside
176     * the richer source/local observations so offline audits can join the
177     * snapshot without treating context as a new identity predicate.
178     */
179    public static JSONObject contextSnapshot(final GUITree tree, final State state, final ModelAction action, final GUITreeNode node) {
180        return com.android.commands.monkey.ape.naming.NameManager.withoutInterning(
181            new java.util.function.Supplier<JSONObject>() {public JSONObject get(){return contextSnapshotReadOnly(tree,state,action,node);}});
182    }
183    private static JSONObject contextSnapshotReadOnly(GUITree tree, State state, ModelAction action, GUITreeNode node) {
184        try {
185            Object control = node == null ? JSONObject.NULL : controlSnapshot(node);
186            JSONArray labels = node == null ? new JSONArray() : new JSONArray(member(action == null ? "" : action.getType().name(), node).labelsJson);
187            JSONObject local = node == null ? new JSONObject().put("parent", JSONObject.NULL).put("labels", new JSONArray()).put("truncated", false) : localContext(node);
188            JSONObject source = tree == null ? new JSONObject().put("activity", "").put("page_labels", new JSONArray()).put("truncated", false) : sourceContext(tree);
189            String stateId = state == null ? "" : state.getGraphId();
190            String actionId = action == null ? "" : action.getGraphId();
191            String type = action == null ? "" : action.getType().name();
192            String fullName = node == null ? "" : JevIdentity.fullName(node).toString();
193            JSONObject matcher = new JSONObject().put("state_id", stateId).put("model_action_id", actionId)
194                .put("action_type", type).put("full_name", fullName)
195                .put("action_fingerprint", action == null ? "" : action.getActionFingerprint())
196                .put("target_fingerprint", action == null ? "" : action.getTargetFingerprint())
197                .put("target_descriptor", action == null ? "" : action.getTargetDescriptor());
198            JSONObject result = new JSONObject().put("schema", 1).put("activity", tree == null ? "" : tree.getActivityClassName())
199                .put("source_state_id", stateId).put("model_action_id", actionId).put("action_type", type)
200                .put("full_name", fullName).put("control", control).put("target_subtree_labels", labels)
201                .put("local_context", local).put("source_context", source).put("matcher", matcher);
202            if(tree != null)result.put("gui_identity", guiIdentity(tree));
203            return result;
204        } catch(JSONException error) { throw new IllegalStateException(error); }
205    }
206
207    /** The concrete-node attributes used by the existing Region evidence. */
208    public static JSONObject controlSnapshot(GUITreeNode node) {
209        return JevRegistry.fields("class",boundedText(node.getClassName()),"resource_id",boundedText(node.getResourceID()),
210            "text",rawText(node),"content_desc",boundedText(node.getContentDesc()),"enabled",node.isEnabled(),
211            "clickable",node.isClickable(),"long_clickable",node.isLongClickable(),"scrollable",node.isScrollable(),
212            "checked",node.isChecked(),"selected",node.getJevSelected()==null?JSONObject.NULL:node.getJevSelected());
213    }
214    /** Bounded source-node evidence, shared by current and confirmed historical actions. */
215    static JSONObject pairAction(com.android.commands.monkey.ape.tree.GUITreeAction action,String sourcePage)throws JSONException {
216        GUITreeNode node=action==null?null:action.getGUITreeNode();
217        if(node==null || action.getGUITree()==null)return null;
218        long structureStart=JevReviewProfile.start();
219        JSONArray subtree=new JSONArray();boolean[] truncated={false};
220        pairSubtree(node,0,new JSONArray(),subtree,truncated);
221        JSONObject value=controlSnapshot(node).put("content_desc",rawContentDesc(node))
222            .put("action_type",action.getActionType().name()).put("index",node.getIndex())
223            .put("concrete_identity",ConcreteActionIdentity.of(action.getActionType().name(),node).toString())
224            .put("model_action_id",action.getModelAction()==null?JSONObject.NULL:action.getModelAction().getGraphId())
225            .put("source_state_id",action.getGUITree().getCurrentState()==null?JSONObject.NULL:action.getGUITree().getCurrentState().getGraphId())
226            .put("bounds",rect(node.getBoundsInScreen())).put("source_page_id",sourcePage)
227            .put("target_subtree",subtree).put("subtree_truncated",truncated[0])
228            .put("parent",node.getParent()==null?JSONObject.NULL:controlSnapshot(node.getParent()))
229            .put("local_context",localContext(node));
230        JevReviewProfile.end("subtree_parent_local_context",structureStart);
231        // Keep provenance explicit; never infer or restore missing labels.
232        value.put("text_source",node.getJevText()!=null?"retained_gui_text":node.getNodeInfo()!=null?"source_node_info":"retained_native_node")
233            .put("generated_image_text",node.isGeneratedImageText())
234            .put("snapshot_provenance","retained source GUI node; no inferred or reconstructed labels");
235        boolean labels=false;
236        for(int i=0;i<subtree.length();i++)labels |= pairLabels(subtree.getJSONObject(i));
237        if(node.getParent()!=null)labels |= pairLabels(value.getJSONObject("parent"));
238        labels |= value.getJSONObject("local_context").getJSONArray("labels").length()>0;
239        // A class/bounds-only anonymous control has no recoverable operation clues.
240        if(!labels)return null;
241        return value;
242    }
243
244    /**
245     * Lightweight, read-only selection descriptor. It deliberately does not
246     * allocate the prompt's subtree/parent/local-context JSON representation.
247     * Its feature set mirrors JevHistoryPairwiseProtocol.selectionFeatures on
248     * pairAction(), and recoverable uses the same label sources as pairLabels.
249     */
250    static final class PairDescriptor {
251        final Set<String> selectionFeatures;
252        final boolean recoverable;
253        PairDescriptor(Set<String> features,boolean canRecover){
254            selectionFeatures=Collections.unmodifiableSet(features);recoverable=canRecover;
255        }
256    }
257    static PairDescriptor pairDescriptor(com.android.commands.monkey.ape.tree.GUITreeAction action) {
258        GUITreeNode node=action==null?null:action.getGUITreeNode();
259        if(node==null || action.getGUITree()==null)return new PairDescriptor(new LinkedHashSet<String>(),false);
260        Set<String> features=new LinkedHashSet<String>();
261        boolean[] recoverable={false};
262        collectPairDescriptor(node,0,new int[]{0},action,features,recoverable);
263        GUITreeNode parent=node.getParent();
264        // Parent uses controlSnapshot's native (bounded) content-desc, whereas
265        // the subtree uses the retained raw content-desc. Do not conflate them.
266        if(parent!=null)recoverable[0] |= !rawText(parent).isEmpty()
267            || !boundedText(parent.getContentDesc()).isEmpty() || !boundedText(parent.getResourceID()).isEmpty();
268        // The pairAction contract also accepts visible sibling/local labels.
269        // Defer this bounded scan until subtree and parent expose no label.
270        if(!recoverable[0])try {recoverable[0]=localContext(node).getJSONArray("labels").length()>0;}
271            catch(JSONException impossible){throw new IllegalStateException(impossible);}
272        return new PairDescriptor(features,recoverable[0]);
273    }
274    private static void collectPairDescriptor(GUITreeNode node,int depth,int[] visited,
275            com.android.commands.monkey.ape.tree.GUITreeAction action,Set<String> features,boolean[] recoverable) {
276        JevDeadline.check();
277        if(node==null || depth>3 || visited[0]>=32)return;
278        visited[0]++;
279        JevHistoryPairwiseProtocol.addSelectionFeature("text",rawText(node),features);
280        JevHistoryPairwiseProtocol.addSelectionFeature("content_desc",rawContentDesc(node),features);
281        JevHistoryPairwiseProtocol.addSelectionFeature("resource_id",boundedText(node.getResourceID()),features);
282        JevHistoryPairwiseProtocol.addSelectionFeature("class",boundedText(node.getClassName()),features);
283        if(node==action.getGUITreeNode())JevHistoryPairwiseProtocol.addSelectionFeature("action_type",action.getActionType().name(),features);
284        recoverable[0] |= pairLabels(node);
285        if(depth==3 || visited[0]>=32)return;
286        Iterator<GUITreeNode> children=node.getChildren();
287        while(children.hasNext() && visited[0]<32)collectPairDescriptor(children.next(),depth+1,visited,action,features,recoverable);
288    }
289    private static boolean pairLabels(JSONObject node) {
290        return !node.optString("text").isEmpty() || !node.optString("content_desc").isEmpty() || !node.optString("resource_id").isEmpty();
291    }
292    private static boolean pairLabels(GUITreeNode node) {
293        return node!=null && (!rawText(node).isEmpty() || !rawContentDesc(node).isEmpty() || !boundedText(node.getResourceID()).isEmpty());
294    }
295    private static JSONArray rect(android.graphics.Rect b) {return new JSONArray().put(b.left).put(b.top).put(b.right).put(b.bottom);}
296    private static void pairSubtree(GUITreeNode node,int depth,JSONArray path,JSONArray rows,boolean[] truncated)throws JSONException {
297        JevDeadline.check();
298        if(rows.length()>=32){truncated[0]=true;return;}
299        rows.put(controlSnapshot(node).put("content_desc",rawContentDesc(node)).put("path",path));
300        if(depth==3){truncated[0] |= node.getChildCount()>0;return;}
301        Iterator<GUITreeNode> children=node.getChildren();
302        while(children.hasNext()) {
303            if(rows.length()>=32){truncated[0]=true;break;}
304            GUITreeNode child=children.next();JSONArray childPath=new JSONArray();
305            for(int i=0;i<path.length();i++)childPath.put(path.getInt(i));childPath.put(child.getIndex());
306            pairSubtree(child,depth+1,childPath,rows,truncated);
307        }
308    }
309
310    /** Probe evidence may use the nearest ancestor (up to two levels) that actually exposes external labels. */
311    private static JSONObject probeLocalContext(GUITreeNode candidate) {
312        GUITreeNode parent=candidate==null?null:candidate.getParent();
313        for(int hops=0;parent!=null && hops<2;hops++) {
314            JSONObject context=localContextAt(candidate,parent);
315            if(context.optJSONArray("labels").length()>0)return context;
316            if(localBoundary(parent))break;
317            parent=parent.getParent();
318        }
319        return localContextAt(candidate,candidate==null?null:candidate.getParent());
320    }
321    private static JSONObject localContextAt(GUITreeNode candidate,GUITreeNode parent) {
322        JSONArray labels=new JSONArray();
323        if(parent==null)return JevRegistry.fields("parent",JSONObject.NULL,"labels",labels,"truncated",false);
324        String cls=parent.getClassName(),resource=parent.getResourceID();
325        JSONObject container=JevRegistry.fields("class",boundedText(cls,128),"resource_id",boundedText(resource,128));
326        boolean truncated=parent.getParent()==null || localBoundary(parent);
327        if(!truncated)truncated=collectLocal(parent,candidate,new JSONArray(),labels,new int[1]);
328        truncated |= !boundedText(cls,128).equals(cls==null?"":cls) || !boundedText(resource,128).equals(resource==null?"":resource);
329        return JevRegistry.fields("parent",container,"labels",labels,"truncated",truncated);
330    }
331    private static boolean localBoundary(GUITreeNode node) {
332        String cls=node.getClassName();
333        return node.isScrollable() || cls.endsWith("ListView") || cls.endsWith("GridView")
334            || cls.endsWith("RecyclerView") || cls.endsWith("ScrollView");
335    }
336    private static boolean collectLocal(GUITreeNode node,GUITreeNode candidate,JSONArray path,JSONArray labels,int[] scanned) {
337        JevDeadline.check();
338        if(node==candidate)return false;
339        if(scanned[0]>=32 || labels.length()>=8)return true;
340        scanned[0]++;
341        if(Boolean.FALSE.equals(node.getJevVisibleToUser()))return false;
342        String text=rawText(node),desc=rawContentDesc(node),shortText=boundedText(text,128),shortDesc=boundedText(desc,128);
343        boolean truncated=!text.equals(shortText) || !desc.equals(shortDesc);
344        if(!text.isEmpty() || !desc.isEmpty())labels.put(JevRegistry.fields("path",path,"text",shortText,"content_desc",shortDesc));
345        if(path.length()==2 || path.length()>0 && localBoundary(node))return truncated || node.getChildCount()>0;
346        Iterator<GUITreeNode> children=node.getChildren();
347        while(children.hasNext()) {
348            if(scanned[0]>=32 || labels.length()>=8)return true;
349            GUITreeNode child=children.next();JSONArray childPath=new JSONArray();
350            for(int i=0;i<path.length();i++)childPath.put(path.optInt(i));
351            childPath.put(child.getIndex());
352            truncated |= collectLocal(child,candidate,childPath,labels,scanned);
353        }
354        return truncated;
355    }
356    public static JSONArray resultLabels(GUITreeNode node) {
357        JSONArray pairs=fullLabels(node), out=new JSONArray();
358        try { for(int i=0;i<pairs.length();i++) {
359            JSONArray p=pairs.getJSONArray(i);
360            for(int j=0;j<2;j++) if(!p.getString(j).isEmpty()) out.put(p.getString(j));
361        }} catch(JSONException e) {throw new IllegalStateException(e);}
362        return out;
363    }
364    /** Complete collected GUI attributes, not the bounded prompt labels or abstract State ID. */
365    static String guiIdentity(GUITree tree) {
366        try {
367            java.security.MessageDigest digest=java.security.MessageDigest.getInstance("SHA-256");
368            ArrayDeque<GUITreeNode> pending=new ArrayDeque<GUITreeNode>();pending.add(tree.getRootNode());
369            while(!pending.isEmpty()) {
370                GUITreeNode n=pending.removeFirst();
371                String row=new JSONArray().put(n.getChildCount()).put(n.getIndex()).put(n.getClassName()).put(n.getPackageName())
372                    .put(n.getResourceID()).put(n.getText()).put(n.getContentDesc()).put(n.getInputText())
373                    .put(n.getJevText()).put(n.getJevContentDesc()).put(n.getJevSelected()).put(n.getJevPassword())
374                    .put(n.isEnabled()).put(n.isClickable()).put(n.isLongClickable()).put(n.isCheckable()).put(n.isChecked())
375                    .put(n.isScrollable()).put(n.isFocusable()).put(n.isFocused()).put(n.isPassword())
376                    .put(n.getJevVisibleToUser()).put(n.getJevDrawingOrder()).put(String.valueOf(n.getBoundsInScreen())).toString();
377                digest.update(row.getBytes(JevProtocol.UTF8));digest.update((byte)'\n');
378                Iterator<GUITreeNode> children=n.getChildren();while(children.hasNext())pending.addLast(children.next());
379            }
380            return java.util.Arrays.toString(digest.digest());
381        } catch(java.security.NoSuchAlgorithmException e){throw new IllegalStateException(e);}
382    }
383    public static boolean resultUnsafe(GUITreeNode node) { return password(node,0,new int[1]); }
384
385    /** A fresh read used only to reject replies after the actual window changed. */
386    public static String accessibilityIdentity(android.view.accessibility.AccessibilityNodeInfo root) {
387        if(root==null)return null;
388        StringBuilder out=new StringBuilder();int[] count={0};
389        try {if(!accessibilityIdentity(root,out,count))return null;return digest(out.toString());}
390        catch(RuntimeException unavailable){return null;}
391    }
392    private static boolean accessibilityIdentity(android.view.accessibility.AccessibilityNodeInfo node,StringBuilder out,int[] count) {
393        if(++count[0]>4096)return false;
394        android.graphics.Rect bounds=new android.graphics.Rect();node.getBoundsInScreen(bounds);
395        out.append(new JSONArray().put(String.valueOf(node.getClassName())).put(String.valueOf(node.getPackageName()))
396            .put(node.getViewIdResourceName()).put(String.valueOf(node.getText())).put(String.valueOf(node.getContentDescription()))
397            .put(node.isEnabled()).put(node.isClickable()).put(node.isLongClickable()).put(node.isScrollable())
398            .put(node.isCheckable()).put(node.isChecked()).put(node.isSelected()).put(node.isFocusable()).put(node.isFocused())
399            .put(node.isPassword()).put(node.isVisibleToUser()).put(bounds.toString()).put(node.getChildCount())).append('\n');
400        for(int i=0;i<node.getChildCount();i++) {
401            android.view.accessibility.AccessibilityNodeInfo child=node.getChild(i);
402            if(child==null)return false;
403            try {if(!accessibilityIdentity(child,out,count))return false;}finally{child.recycle();}
404        }
405        return true;
406    }
407}