Skip to content

Commit 57f7a2b

Browse files
committed
Small refactor to spy compare state tree
1 parent e83117c commit 57f7a2b

File tree

4 files changed

+252
-91
lines changed

4 files changed

+252
-91
lines changed

android/src/org/testar/monkey/alayer/android/spy_visualization/MobileVisualizationAndroid.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/***************************************************************************************************
22
*
3-
* Copyright (c) 2020 - 2022 Open Universiteit - www.ou.nl
4-
* Copyright (c) 2020 - 2022 Universitat Politecnica de Valencia - www.upv.es
3+
* Copyright (c) 2020 - 2025 Open Universiteit - www.ou.nl
4+
* Copyright (c) 2020 - 2025 Universitat Politecnica de Valencia - www.upv.es
55
*
66
* Redistribution and use in source and binary forms, with or without
77
* modification, are permitted provided that the following conditions are met:
@@ -112,7 +112,7 @@ public void actionPerformed(ActionEvent actionEvent) {
112112
rightside.add(updateButton);
113113

114114
//Sets the initial state tree of the SUT
115-
treeVizInstance = new TreeVisualizationAndroid(this, (usedState.root()));
115+
treeVizInstance = new TreeVisualizationAndroid(this, usedState);
116116
treeVisualizationPanel.add(treeVizInstance);
117117
rightside.add(treeVisualizationPanel);
118118

@@ -139,7 +139,6 @@ public void actionPerformed(ActionEvent actionEvent) {
139139
private void updateScreen() {
140140
// Updates screenshot and overlay
141141
String screenshotPath = AndroidProtocolUtil.getStateshotSpyMode(usedState);
142-
System.out.println("SCREENSHOT PATH SPY MODE: " + screenshotPath);
143142
imagePanel.updateSc(screenshotPath, treeVizInstance.tree, deriveActionsFunction.apply(usedState));
144143

145144
frame.revalidate();
@@ -152,7 +151,7 @@ public void updateVisualization(State state) {
152151
this.newState = state;
153152

154153
// Updates the tree of android widgets if changes occur in the SUT
155-
boolean updated = treeVizInstance.createCompareTree(this.newState.root());
154+
boolean updated = treeVizInstance.compareUpdateTree(this.newState);
156155
if (updated) {
157156
this.usedState = state;
158157

android/src/org/testar/monkey/alayer/android/spy_visualization/OverlayVisualization.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public class OverlayVisualization extends JLayeredPane {
5656
public ArrayList<OverlayBox> boxTrackSetDisplayed = new ArrayList<>();
5757
private LinkedList<DefaultMutableTreeNode> queue = new LinkedList<>();
5858
private ArrayList<DefaultMutableTreeNode> listOfNodes = new ArrayList<>();
59-
private Set<Widget> setOfActionableWidgets = Collections.emptySet();
59+
private Set<String> setOfActionableWidgets = Collections.emptySet();
6060
private int originalHeight;
6161
private int originalWidth;
6262
private int width = 440;
@@ -94,7 +94,10 @@ public void updateSc(String screenshotPath, JTree tree, Set<Action> derivedActio
9494
queue = new LinkedList<>();
9595

9696
setOfActionableWidgets = derivedActions.stream()
97-
.map(a -> a.get(Tags.OriginWidget, null))
97+
.map(a -> {
98+
Widget widget = a.get(Tags.OriginWidget, null);
99+
return widget == null ? null : widget.get(AndroidTags.AndroidXpath);
100+
})
98101
.filter(Objects::nonNull)
99102
.collect(Collectors.toSet());
100103

@@ -174,9 +177,9 @@ public static class OverlayBox extends JLabel{
174177
public DefaultMutableTreeNode node;
175178
public boolean displayed = false;
176179
public OverlayBox instanceOverlayBox = this;
177-
private Set<Widget> setOfActionableWidgets;
180+
private Set<String> setOfActionableWidgets;
178181

179-
public OverlayBox(Set<Widget> setOfActionableWidgets) {
182+
public OverlayBox(Set<String> setOfActionableWidgets) {
180183
this.setOfActionableWidgets = setOfActionableWidgets;
181184
}
182185

@@ -192,7 +195,7 @@ private void myPaint(Graphics g) {
192195
String className = widget.get(AndroidTags.AndroidClassName);
193196
Boolean clickable = widget.get(AndroidTags.AndroidClickable) && !(className.equals("android.widget.EditText"));
194197
Boolean typeable = className.equals("android.widget.EditText");
195-
boolean actionable = setOfActionableWidgets.contains(widget);
198+
boolean actionable = setOfActionableWidgets.contains(widget.get(AndroidTags.AndroidXpath));
196199

197200
if (clickable || typeable) {
198201
Color color = actionable ? (clickable ? Color.GREEN // click

android/src/org/testar/monkey/alayer/android/spy_visualization/TreeVisualizationAndroid.java

Lines changed: 117 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/***************************************************************************************************
22
*
3-
* Copyright (c) 2020 - 2022 Universitat Politecnica de Valencia - www.upv.es
4-
* Copyright (c) 2020 - 2022 Open Universiteit - www.ou.nl
3+
* Copyright (c) 2020 - 2025 Universitat Politecnica de Valencia - www.upv.es
4+
* Copyright (c) 2020 - 2025 Open Universiteit - www.ou.nl
55
*
66
* Redistribution and use in source and binary forms, with or without
77
* modification, are permitted provided that the following conditions are met:
@@ -30,7 +30,6 @@
3030

3131
package org.testar.monkey.alayer.android.spy_visualization;
3232

33-
import org.testar.monkey.Pair;
3433
import org.testar.monkey.alayer.*;
3534
import org.testar.monkey.alayer.android.enums.AndroidTags;
3635

@@ -41,7 +40,6 @@
4140
import javax.swing.tree.TreePath;
4241
import javax.swing.tree.TreeSelectionModel;
4342
import java.awt.*;
44-
import java.util.LinkedList;
4543

4644
public class TreeVisualizationAndroid extends JPanel implements TreeSelectionListener {
4745
private final JPanel infoPaneLeft = new JPanel();
@@ -54,25 +52,19 @@ public class TreeVisualizationAndroid extends JPanel implements TreeSelectionLis
5452

5553
private String selectedNodePath;
5654

57-
private LinkedList<Pair<DefaultMutableTreeNode, Integer>> toBeReplacedA = null;
58-
private LinkedList<Pair<DefaultMutableTreeNode, Integer>> toBeReplacedWithB = null;
59-
60-
6155
/** Initializer for the tree component of the spy mode visualization (right hand side screen). */
62-
public TreeVisualizationAndroid(MobileVisualizationAndroid mobileVisualizationAndroid, Widget widget) {
56+
public TreeVisualizationAndroid(MobileVisualizationAndroid mobileVisualizationAndroid, State state) {
6357
super(new GridLayout(1,0));
6458

6559
this.mobileVisualizationAndroid = mobileVisualizationAndroid;
6660

6761
//Create the nodes.
68-
DefaultMutableTreeNode top =
69-
new DefaultMutableTreeNode(widget.get(Tags.Desc));
70-
createNodes(top, widget);
62+
DefaultMutableTreeNode top = new DefaultMutableTreeNode(state.get(Tags.Desc));
63+
createNodes(top, state);
7164

7265
//Create a tree that allows one selection at a time.
7366
tree = new JTree(top);
74-
tree.getSelectionModel().setSelectionMode
75-
(TreeSelectionModel.SINGLE_TREE_SELECTION);
67+
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
7668

7769
//Listen for when the selection changes.
7870
tree.addTreeSelectionListener(this);
@@ -127,8 +119,7 @@ public TreeVisualizationAndroid(MobileVisualizationAndroid mobileVisualizationAn
127119
* detailed information of the clicked tree object.
128120
*/
129121
public void valueChanged(TreeSelectionEvent e) {
130-
DefaultMutableTreeNode node = (DefaultMutableTreeNode)
131-
tree.getLastSelectedPathComponent();
122+
DefaultMutableTreeNode node = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent();
132123

133124
if (tempTreeNode == null) {
134125
tempTreeNode = node;
@@ -155,84 +146,134 @@ public void valueChanged(TreeSelectionEvent e) {
155146
}
156147
}
157148

158-
159-
/** Creates a new tree based on the newly retrieved state (root node).
160-
* Additionally, calls method which determines which nodes need to be replaced (have changed in the new state)
161-
* and updates these parts of the tree.
149+
/**
150+
* Compare the existing tree with a new state and update it.
151+
*
152+
* @param root Widget root of the *new* state.
153+
* @return true if nothing changed; false if any visual or structural change was applied.
162154
*/
163-
public boolean createCompareTree(Widget root) {
164-
// create tree corresponding to the new state
165-
DefaultMutableTreeNode newTop =
166-
new DefaultMutableTreeNode(root.get(Tags.Desc));
155+
public boolean compareUpdateTree(State root) {
156+
DefaultMutableTreeNode newTop = new DefaultMutableTreeNode(root.get(Tags.Desc));
167157
createNodes(newTop, root);
168158

169-
toBeReplacedA = new LinkedList<Pair<DefaultMutableTreeNode, Integer>>();
170-
toBeReplacedWithB = new LinkedList<Pair<DefaultMutableTreeNode, Integer>>();
159+
DefaultTreeModel currentModel = (DefaultTreeModel) tree.getModel();
160+
DefaultMutableTreeNode currentRoot = (DefaultMutableTreeNode) currentModel.getRoot();
171161

172-
boolean identical = identicalTrees((DefaultMutableTreeNode)(tree.getModel().getRoot()),newTop, 0);
162+
// First time or null root, then just set the model
163+
if (currentRoot == null) {
164+
tree.setModel(new DefaultTreeModel(newTop));
165+
return true;
166+
}
173167

174-
if (!identical) {
175-
DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
176-
for (int i = 0; i < toBeReplacedA.size(); i++) {
177-
model.insertNodeInto(toBeReplacedWithB.get(i).left(), (DefaultMutableTreeNode) toBeReplacedA.get(i).left().getParent(),toBeReplacedA.get(i).right());
178-
model.removeNodeFromParent(toBeReplacedA.get(i).left());
179-
}
168+
TreeDiff diff = new TreeDiff();
169+
syncTrees(currentRoot, newTop, diff);
180170

171+
if (diff.structureChanged) {
172+
// Something in the hierarchy really changed; rebuild the model.
173+
tree.setModel(new DefaultTreeModel(newTop));
181174
return true;
182175
}
176+
177+
if (!diff.changedNodes.isEmpty()) {
178+
// Only state changes: notify JTree that these nodes changed.
179+
for (DefaultMutableTreeNode n : diff.changedNodes) {
180+
currentModel.nodeChanged(n);
181+
}
182+
return true;
183+
}
184+
185+
// No differences at all
183186
return false;
184187
}
185188

186-
public TreePath stringToTreePath(String stringPath) {
187-
//Need to use the xPath of the last clicked component to find the representation in the Tree.
188-
return null;
189-
}
189+
/**
190+
* Recursively compare two subtrees.
191+
*
192+
* - If structure (child count or widget identity) differs anywhere below, we mark structureChanged = true
193+
* and stop trying to do fine-grained updates.
194+
* - If only state differs, we update the existing node's userObject and queue nodeChanged.
195+
*/
196+
private void syncTrees(DefaultMutableTreeNode existing, DefaultMutableTreeNode updated, TreeDiff diff) {
197+
// if we've already decided structure changed, no need to continue
198+
if (diff.structureChanged) {
199+
return;
200+
}
190201

202+
int existingChildren = existing.getChildCount();
203+
int updatedChildren = updated.getChildCount();
191204

192-
/** Given two trees, return true if they are structurally identical, false otherwise and track at which point in
193-
* the tree the sub trees are no longer identical.
194-
*/
195-
boolean identicalTrees(DefaultMutableTreeNode a, DefaultMutableTreeNode b, int childNumber) {
196-
//1. both subtrees have no childeren
197-
if (a.getChildCount() == 0 && b.getChildCount() == 0)
198-
return true;
205+
if (existingChildren != updatedChildren) {
206+
diff.structureChanged = true;
207+
return;
208+
}
209+
210+
for (int i = 0; i < existingChildren; i++) {
211+
DefaultMutableTreeNode existingChild = (DefaultMutableTreeNode) existing.getChildAt(i);
212+
DefaultMutableTreeNode updatedChild = (DefaultMutableTreeNode) updated.getChildAt(i);
213+
214+
Object existingObj = existingChild.getUserObject();
215+
Object updatedObj = updatedChild.getUserObject();
199216

200-
//2. both non-empty -> compare them, return false if not equal
201-
if (a.getChildCount() == b.getChildCount()){
202-
203-
boolean equality = true;
204-
for (int i = 0; i < a.getChildCount(); i++) {
205-
Widget aChild = (Widget) ((DefaultMutableTreeNode)a.getChildAt(i)).getUserObject();
206-
Widget bChild = (Widget) ((DefaultMutableTreeNode)b.getChildAt(i)).getUserObject();
207-
if (!(aChild.get(AndroidTags.AndroidXpath).equals(bChild.get(AndroidTags.AndroidXpath))) ||
208-
!(aChild.get(Tags.Title).equals(bChild.get(Tags.Title))) ||
209-
!(aChild.get(AndroidTags.AndroidBounds).equals(bChild.get(AndroidTags.AndroidBounds))) ||
210-
!(aChild.get(AndroidTags.AndroidChecked).equals(bChild.get(AndroidTags.AndroidChecked))) ||
211-
!(aChild.get(AndroidTags.AndroidSelected).equals(bChild.get(AndroidTags.AndroidSelected)))) {
212-
213-
toBeReplacedA.add(new Pair<>((DefaultMutableTreeNode)a.getChildAt(i),i));
214-
toBeReplacedWithB.add(new Pair<>((DefaultMutableTreeNode)b.getChildAt(i),i));
215-
equality = false;
216-
} else {
217-
equality = equality && identicalTrees((DefaultMutableTreeNode)a.getChildAt(i), (DefaultMutableTreeNode)b.getChildAt(i), i);
218-
if (!equality) {
219-
if (toBeReplacedA.size() == 0) {
220-
toBeReplacedA.add(new Pair<>((DefaultMutableTreeNode)a.getChildAt(i),childNumber));
221-
toBeReplacedWithB.add(new Pair<>((DefaultMutableTreeNode)b.getChildAt(i),childNumber));
222-
break;
223-
}
224-
}
217+
// Root has a String Desc; children are Widgets.
218+
if (!(existingObj instanceof Widget) || !(updatedObj instanceof Widget)) {
219+
// if this happens below the root, treat as structural change
220+
if (existing != (DefaultMutableTreeNode) tree.getModel().getRoot()) {
221+
diff.structureChanged = true;
222+
return;
225223
}
224+
continue;
225+
}
226+
227+
Widget existingWidget = (Widget) existingObj;
228+
Widget updatedWidget = (Widget) updatedObj;
229+
230+
// Identity changed? Then structure changed
231+
if (!sameIdentity(existingWidget, updatedWidget)) {
232+
diff.structureChanged = true;
233+
return;
234+
}
235+
236+
// Identity same but state changed, then update node in place
237+
if (!sameState(existingWidget, updatedWidget)) {
238+
existingChild.setUserObject(updatedWidget);
239+
diff.changedNodes.add(existingChild);
240+
}
241+
242+
// recurse for children
243+
syncTrees(existingChild, updatedChild, diff);
244+
if (diff.structureChanged) {
245+
return;
226246
}
227-
return equality;
228247
}
248+
}
229249

230-
// When childcount of trees are not equal return false.
231-
toBeReplacedA.add(new Pair<>(a,childNumber));
232-
toBeReplacedWithB.add(new Pair<>(b,childNumber));
233-
return false;
250+
private static final class TreeDiff {
251+
boolean structureChanged = false;
252+
java.util.List<DefaultMutableTreeNode> changedNodes = new java.util.ArrayList<>();
253+
}
254+
255+
private static <T> boolean safeEquals(T a, T b) {
256+
return a == b || (a != null && a.equals(b));
257+
}
258+
259+
/**
260+
* Checks if two widgets represent the same UI element (identity).
261+
* We use AndroidXpath as identity because it encodes the path in the view hierarchy.
262+
*/
263+
private static boolean sameIdentity(Widget a, Widget b) {
264+
return safeEquals(a.get(AndroidTags.AndroidXpath), b.get(AndroidTags.AndroidXpath));
234265
}
235266

267+
/**
268+
* Checks if the UI state that we care about in the tree has changed.
269+
* Adjust this if you want more/less properties to count as "state".
270+
*/
271+
private static boolean sameState(Widget a, Widget b) {
272+
return safeEquals(a.get(Tags.Title), b.get(Tags.Title))
273+
&& safeEquals(a.get(AndroidTags.AndroidBounds), b.get(AndroidTags.AndroidBounds))
274+
&& safeEquals(a.get(AndroidTags.AndroidChecked), b.get(AndroidTags.AndroidChecked))
275+
&& safeEquals(a.get(AndroidTags.AndroidSelected), b.get(AndroidTags.AndroidSelected));
276+
}
236277

237278
/** Displays the additional info when a widget is clicked in the tree. */
238279
private void displayWidgetInfo(Widget nodeWidget) {
@@ -277,9 +318,9 @@ private void displayWidgetInfo(Widget nodeWidget) {
277318

278319
infoPaneLeft.add(new JLabel("Hint content: ")).setFont(new Font("SansSerif", Font.BOLD, fontSize));
279320
if (hintWidget.equals("")) {
280-
infoPaneRight.add(new JLabel(" ")).setFont(new Font("SansSerif", Font.PLAIN, fontSize));
321+
infoPaneRight.add(new JLabel(" ")).setFont(new Font("SansSerif", Font.PLAIN, fontSize));
281322
} else {
282-
infoPaneRight.add(new JLabel(hintWidget)).setFont(new Font("SansSerif", Font.PLAIN, fontSize));
323+
infoPaneRight.add(new JLabel(hintWidget)).setFont(new Font("SansSerif", Font.PLAIN, fontSize));
283324
}
284325

285326
infoPaneLeft.add(new JLabel("Access ID: ")).setFont(new Font("SansSerif", Font.BOLD, fontSize));
@@ -289,7 +330,6 @@ private void displayWidgetInfo(Widget nodeWidget) {
289330
infoPaneRight.add(new JLabel(accessibilityIdWidget)).setFont(new Font("SansSerif", Font.PLAIN, fontSize));
290331
}
291332

292-
293333
//TODO: THE XPATH AT STARTUP DOESNT FIT IN THE SCREEN. HOWEVER IF ONE UPDATE IN THE EMULATOR HAS OCCURED
294334
// SCROLLING IS ADDED. FIGURE OUT WHY SCROLLING IS NOT ENABLED FROM THE START.
295335
infoPaneLeft.add(new JLabel("XPath: ")).setFont(new Font("SansSerif", Font.BOLD, fontSize));
@@ -314,15 +354,12 @@ private void displayWidgetInfo(Widget nodeWidget) {
314354
infoPaneRight.add(new JLabel(resourceIdWidget)).setFont(new Font("SansSerif", Font.PLAIN, fontSize));
315355
}
316356

317-
318-
319357
infoPaneLeft.add(new JLabel("Clickable: ")).setFont(new Font("SansSerif", Font.BOLD, fontSize));
320358
infoPaneRight.add(new JLabel(String.valueOf(clickableWidget))).setFont(new Font("SansSerif", Font.PLAIN, fontSize));
321359

322360
infoPaneLeft.add(new JLabel("Index: ")).setFont(new Font("SansSerif", Font.BOLD, fontSize));
323361
infoPaneRight.add(new JLabel(String.valueOf(indexWidget))).setFont(new Font("SansSerif", Font.PLAIN, fontSize));
324362

325-
326363
infoPaneLeft.add(new JLabel("Bounds: ")).setFont(new Font("SansSerif", Font.BOLD, fontSize));
327364
infoPaneRight.add(new JLabel(String.valueOf(boundsWidget))).setFont(new Font("SansSerif", Font.PLAIN, fontSize));
328365

@@ -353,7 +390,6 @@ private void displayWidgetInfo(Widget nodeWidget) {
353390
infoPaneLeft.add(new JLabel("Current Activity: ")).setFont(new Font("SansSerif", Font.BOLD, fontSize));
354391
infoPaneRight.add(new JLabel(String.valueOf(activityWidget))).setFont(new Font("SansSerif", Font.PLAIN, fontSize));
355392

356-
357393
} else { //null node
358394
infoPaneLeft.add(new JLabel("nodeinfo:"));
359395
infoPaneRight.add(new JLabel("NULL"));

0 commit comments

Comments
 (0)