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:
3030
3131package org .testar .monkey .alayer .android .spy_visualization ;
3232
33- import org .testar .monkey .Pair ;
3433import org .testar .monkey .alayer .*;
3534import org .testar .monkey .alayer .android .enums .AndroidTags ;
3635
4140import javax .swing .tree .TreePath ;
4241import javax .swing .tree .TreeSelectionModel ;
4342import java .awt .*;
44- import java .util .LinkedList ;
4543
4644public 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