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}