capsule: Find paths to all Views.

This cl builds the infrastructure to return to Views that aren't
completely explored. For each view, it finds a path from the root to
that view by stepping backwards and finding which UI element in the
preceding view leads to that view.

This will allow us to return to a view by restarting an app and clicking
on the appropriate elements from the list.

Also, change the view_array to a map.

Change-Id: Iea8e9bca7588b692e187241bc0a741bc6e7851d5
diff --git a/capsule/crawlui.py b/capsule/crawlui.py
index 4f9e741..dc689fd 100644
--- a/capsule/crawlui.py
+++ b/capsule/crawlui.py
@@ -27,7 +27,7 @@
 # Return a unique string if the package is not the focused window. Since
 # activities cannot have spaces, we ensure that no activity will be named this.
 EXITED_APP = 'exited app'
-
+FAILED_FINDING_NAME = 'failed finding name'
 # How many times we should try pressing the back button to return to the app
 # before giving up.
 NUM_BACK_PRESSES = 3
@@ -187,12 +187,14 @@
     json.dump(click_info, out_file, indent=2)
 
 
-def find_view_idx(activity, frag_list, vc_dump, view_array):
-  """Finds the index of the current View in the view array (-1 if new View)."""
-  for i in range(len(view_array)):
-    if view_array[i].is_duplicate(activity, frag_list, vc_dump):
-      return i
-  return -1
+def find_view_in_map(activity, frag_list, vc_dump, view_map):
+  """Finds the  current View in the view array (empty if new View)."""
+  # TODO(afergan): Consider creating another map indexed by the values compared
+  # in is_duplicate so that this comparison is O(1).
+  for val in view_map.values():
+    if val.is_duplicate(activity, frag_list, vc_dump):
+      return val
+  return None
 
 
 def create_view(package_name, vc_dump, activity, frag_list):
@@ -216,66 +218,95 @@
   return v
 
 
-def link_ui_views(last_view, curr_view, last_clicked, package_name,
-                  view_array):
-  """Stores the relationship between last_view and curr_view."""
+def link_ui_views(prev_view, curr_view, prev_clicked, package_name):
+  """Stores the relationship between prev_view and curr_view."""
 
   # We store in the View information that the last view links to the current
   # view, and that the current view can be reached from the last view. We use
   # the id of the last clicked element as the dictionary key so that we know
   # which element leads from view to view.
 
-  if last_clicked:
-    print 'Last clicked: ' + last_clicked
-    last_view.click_dict[last_clicked] = curr_view.get_name()
-    curr_view.preceding.append(last_view.get_name())
+  if prev_clicked:
+    print 'Previous clicked: ' + prev_clicked
+    prev_view.click_dict[prev_clicked] = curr_view.get_name()
+    curr_view.preceding.append(prev_view.get_name())
   else:
     print 'Lost track of last clicked!'
-  view_array.append(curr_view)
+
   # TODO(afergan): Remove this later. For debugging, we print the clicks after
   # each click to a new view is recorded. However, this results in a lot of
   # repeated writes to the same file. In the future, we can just write each
   # file once we're done crawling the app.
-  save_ui_flow_relationships(last_view, package_name)
+  save_ui_flow_relationships(prev_view, package_name)
   save_ui_flow_relationships(curr_view, package_name)
 
 
-def obtain_curr_view(activity, package_name, vc_dump, view_array, device):
+def obtain_curr_view(activity, package_name, vc_dump, view_map, device):
   """Extracts UI info and return the current View."""
 
   # Gets the current UI info. If we have seen this UI before, return the
   # existing View. If not, create a new View and save it to the view array.
 
   frag_list = obtain_frag_list(package_name, device)
-  view_idx = find_view_idx(activity, frag_list, vc_dump, view_array)
+  view = find_view_in_map(activity, frag_list, vc_dump, view_map)
 
-  if view_idx >= 0:
+  if view:
     print 'Found duplicate'
-    return view_array[view_idx]
+    return view
   else:
     print 'New view'
     new_view = create_view(package_name, vc_dump, activity, frag_list)
-    view_array.append(new_view)
+    view_map[new_view.get_name()] = new_view
     return new_view
 
 
-def crawl_package(vc, device, debug, package_name=None):
+def find_component_to_lead_to_view(view1, view2):
+  """Given 2 Views, return the component of view 1 that leads to view 2."""
+
+  try:
+    return view1.click_dict.keys()[view1.click_dict.values().index(
+        view2.get_name())]
+  except ValueError:
+    print '*** Could not find a component to link to the succeeding View!'
+
+  return FAILED_FINDING_NAME
+
+
+def find_path_from_root_to_view(view, view_map, view_root):
+  """Given a View, finds the path of UI elements to that View."""
+
+  path = []
+  curr_path_view = view
+  while not curr_path_view.is_duplicate_view(view_root):
+    succeeding_view = curr_path_view
+    # TODO(afergan): Using the first element in preceding doesn't ensure
+    # shortest path. Is it worth keeping track of the depth of every View to
+    # create the shortest path?
+    curr_path_view = view_map.get(succeeding_view.preceding[0])
+    if curr_path_view is None:
+      print '*** Could not find a previous view'
+      return []
+
+    component = find_component_to_lead_to_view(curr_path_view, succeeding_view)
+
+    # TODO(afergan): This should not happen since if we store the predecessor of
+    # one View, we also store which component of the predecessor leads to that
+    # View. However, if it does, we can try exploring other preceding views. I'm
+    # leaving it as a TODO for now, because we could end up in an infinite loop,
+    # plus if it happens it means our data is corrupt anyway.
+    if component == FAILED_FINDING_NAME:
+      return []
+    else:
+      print 'Inserting ' + component + ' to path'
+      path.insert(0, (curr_path_view.get_name(), component))
+
+  return path
+
+
+def crawl_until_exit(vc, device, package_name, view_map, view_root):
   """Main crawler loop. Evaluates views, store new views, and click on items."""
-  set_device_dimens(vc, device)
-  view_array = []
 
-  last_clicked = ''
-  if debug or not package_name:  # These should be equal
-    package_name = obtain_package_name(device)
-
-  # Store the root View
-  print 'Storing root'
-  vc_dump = vc.dump(window='-1')
-  activity = obtain_activity_name(package_name, device)
-  view_root = obtain_curr_view(activity, package_name, vc_dump, view_array,
-                               device)
   curr_view = view_root
-
   while True:
 
     # If last click opened the keyboard, assume we're in the same view and just
@@ -293,17 +324,19 @@
         print 'Current view is not app and we cannot return'
         break
       else:
-        last_clicked = BACK_BUTTON
+        prev_clicked = BACK_BUTTON
 
-    last_view = curr_view
-    vc_dump = vc.dump(window='-1')
-    curr_view = obtain_curr_view(activity, package_name, vc_dump, view_array,
+    prev_view = curr_view
+    try:
+      vc_dump = vc.dump(window='-1')
+    except IOError:
+      print ('*** Socket timeout!')
+    curr_view = obtain_curr_view(activity, package_name, vc_dump, view_map,
                                  device)
     print 'Curr view: ' + curr_view.get_name()
-    if not last_view.is_duplicate_view(curr_view):
+    if not prev_view.is_duplicate_view(curr_view):
       print 'At a diff view!'
-      link_ui_views(last_view, curr_view, last_clicked, package_name,
-                    view_array)
+      link_ui_views(prev_view, curr_view, prev_clicked, package_name)
 
     print 'Num clickable: ' + str(len(curr_view.clickable))
 
@@ -312,14 +345,14 @@
       print('Clicking {} {}, ({},{})'.format(c.getUniqueId(), c.getClass(),
                                              c.getX(), c.getY()))
       c.touch()
-      last_clicked = c.getUniqueId()
+      prev_clicked = c.getUniqueId()
       del curr_view.clickable[-1]
 
     else:
       print 'Clicking back button'
       perform_press_back(device)
-      last_view = curr_view
-      last_clicked = BACK_BUTTON
+      prev_view = curr_view
+      prev_clicked = BACK_BUTTON
       activity = obtain_activity_name(package_name, device)
 
       if activity is EXITED_APP:
@@ -331,12 +364,37 @@
       # Make sure we have changed views.
       vc_dump = vc.dump(window='-1')
       curr_view = obtain_curr_view(activity, package_name, vc_dump,
-                                   view_array, device)
-      if last_view.is_duplicate_view(curr_view):
+                                   view_map, device)
+      if prev_view.is_duplicate_view(curr_view):
         # We have nothing left to click, and the back button doesn't change
         # views.
         print 'Pressing back keeps at the current view'
         break
       else:
-        link_ui_views(last_view, curr_view, 'back button', package_name,
-                      view_array)
+        link_ui_views(prev_view, curr_view, 'back button', package_name)
+
+
+def crawl_package(vc, device, package_name=None):
+  """Crawl entire package. Explore blindly, then return to unexplored views."""
+
+  set_device_dimens(vc, device)
+  view_map = {}
+
+  if not package_name:
+    package_name = obtain_package_name(device)
+
+  # Store the root View
+  print 'Storing root'
+  vc_dump = vc.dump(window='-1')
+  activity = obtain_activity_name(package_name, device)
+  view_root = obtain_curr_view(activity, package_name, vc_dump, view_map,
+                               device)
+  crawl_until_exit(vc, device, package_name, view_map, view_root)
+
+  print 'Root is ' + view_root.get_name()
+
+  for v in view_map.values():
+    if v is not view_root:
+      print 'The path from root to ' + v.get_name() + ' is ' + ' -> '.join(
+          ', '.join(p)
+          for p in find_path_from_root_to_view(v, view_map, view_root))
diff --git a/capsule/main.py b/capsule/main.py
index 38f20bc..fe82fe9 100644
--- a/capsule/main.py
+++ b/capsule/main.py
@@ -41,7 +41,7 @@
   vc = ViewClient(device, serialno, **kwargs2)
 
   if DEBUG:
-    crawlui.crawl_package(vc, device, DEBUG)
+    crawlui.crawl_package(vc, device)
   else:
     package_list = os.listdir(APK_DIR)
     for package in package_list:
@@ -61,5 +61,5 @@
       subprocess.call([ADB_PATH, 'shell', 'monkey', '-p', package_name, '-c',
                        'android.intent.category.LAUNCHER', '1'])
 
-      crawlui.crawl_package(vc, device, DEBUG, package_name)
+      crawlui.crawl_package(vc, device, package_name)
       subprocess.call([ADB_PATH, 'uninstall', package_name])
diff --git a/capsule/view.py b/capsule/view.py
index 1f684e0..4baa6d1 100644
--- a/capsule/view.py
+++ b/capsule/view.py
@@ -32,24 +32,24 @@
   def num_components(self):
     return len(self.hierarchy)
 
-  def is_duplicate(self, cv_activity, cv_frag_list, cv_hierarchy):
+  def is_duplicate(self, activity, frag_list, hierarchy):
     """Determines if the passed-in information is identical to this View."""
 
     # Since the fragment names are hashable, this is the most efficient method
     # to compare two unordered lists according to
     # http://stackoverflow.com/questions/7828867/how-to-efficiently-compare-two-unordered-lists-not-sets-in-python
     # We also use it below to compare hierarchy ids.
-    if (self.activity != cv_activity or
-        Counter(self.frag_list) != Counter(cv_frag_list)):
+    if (self.activity != activity or
+        Counter(self.frag_list) != Counter(frag_list)):
       return False
 
-    if self.num_components() != len(cv_hierarchy):
+    if self.num_components() != len(hierarchy):
       return False
 
     hierarchy_ids = [h['uniqueId'] for h in self.hierarchy]
-    curr_view_ids = [cv['uniqueId'] for cv in cv_hierarchy]
+    view_ids = [h['uniqueId'] for h in hierarchy]
 
-    return Counter(hierarchy_ids) == Counter(curr_view_ids)
+    return Counter(hierarchy_ids) == Counter(view_ids)
 
   def is_duplicate_view(self, other_view):
     """Determines if the passed-in View is identical to this View."""