capsule: Follow shortest path.

Previously for recrawls, we assumed that the app would launch with the
same root Layout as the first time we crawled it, and would not be able
to get to the intended Layout if this is not the case. However, often
there is an intro, agreement, or login screen that is only shown once.
This cl introduces a find_shortest_path method that uses BFS to find a
route between the starting Layout and intended Layout. It allows us to
go between any two Layouts using previously explored paths. If it finds
an unintended path, it removes that edge from the graph and adds the
new edge, and also tries to find a new path from the current layout.

Also, introduce a touch() method to improve readibility and always
catch exceptions for weird coordinates.

Change-Id: I6b65e6a79d6affe677d8ed4a8a08c0d525b4d885
diff --git a/capsule/README.md b/capsule/README.md
index c161a5c..324d7e3 100644
--- a/capsule/README.md
+++ b/capsule/README.md
@@ -90,11 +90,12 @@
 
 ``$ export PYTHONPATH=$PYTHONPATH:$JIRI_ROOT/release/projects/luma_third_party/AndroidViewClient/``
 
-### Facebook Login
+### Facebook/Google Login
 
-Capsule automate Facebook logins to allow the crawler to get through login
-procedures. If the Facebook app is installed on the device and logged into an
-account, Capsule will prioritize clicking on a Facebook login button.
+Capsule automates Facebook and Google logins to allow the crawler to get through
+login procedures. Capsule prioritizes clicking on Facebook or Google login
+buttons, so installing/logging into Facebook and logging into a Google account
+allows the program to get beyond login screens.
 
 ## Contributors
 
diff --git a/capsule/capsule.py b/capsule/capsule.py
index 7899114..e1d7f73 100644
--- a/capsule/capsule.py
+++ b/capsule/capsule.py
@@ -83,7 +83,7 @@
   package_list = []
 
   try:
-    kwargs1 = {'verbose': True, 'ignoresecuredevice': True}
+    kwargs1 = {'verbose': True, 'ignoresecuredevice': True, 'timeout': 20}
     kwargs2 = {'startviewserver': True, 'forceviewserveruse': True,
                'autodump': False, 'ignoreuiautomatorkilled': True}
     device, serialno = ViewClient.connectToDeviceOrExit(**kwargs1)
@@ -141,6 +141,7 @@
       should_crawl = True
       if '.apk' in package:
         package_name = crawlpkg.extract_between(package, '/', '.apk', -1)
+        subprocess.call([ADB_PATH, '-s', serialno, 'install', '-r', package])
 
       else:
         # We have the package name but not the .apk file.
@@ -151,7 +152,7 @@
                                                   'shell', 'pm',
                                                   'list packages', '-3'])
         if package_name not in installed_pkgs:
-          print 'Cannot find the package on the device.'
+          print 'Cannot find the package on the device.' + package_name
           should_crawl = False
 
       if os.path.exists(os.path.dirname(os.path.abspath(__file__)) + '/data/' +
@@ -160,13 +161,13 @@
 
       if should_crawl:
         print 'Crawling ' + package_name
+
         # Launch the app.
         subprocess.call([ADB_PATH, '-s', serialno, 'shell', 'monkey', '-p',
                          package_name, '-c', 'android.intent.category.LAUNCHER',
                          '1'])
         time.sleep(5)
-        if '.apk' in package:
-          subprocess.call([ADB_PATH, '-s', serialno, 'install', '-r', package])
+
         crawlpkg.crawl_package(vc, device, serialno, package_name)
 
         if uninstall:
diff --git a/capsule/crawlpkg.py b/capsule/crawlpkg.py
index ea7a285..1686c4a 100644
--- a/capsule/crawlpkg.py
+++ b/capsule/crawlpkg.py
@@ -101,6 +101,22 @@
     return None
 
 
+def touch(device, view):
+  """Touches the corner of a view."""
+
+  # We don't use the AndroidViewClient view.touch() method because it touches
+  # the center of the view, which may be offscreen (or push a button on the nav
+  # bar).
+  try:
+    device.touch(view.getX(), view.getY())
+    print('Clicked {} {}, ({},{})'.format(view.getUniqueId(), view.getClass(),
+                                          view.getX(), view.getY()))
+  except UnicodeEncodeError:
+    print '***Unicode coordinates'
+  except TypeError:
+    print '***String coordinates'
+
+
 def return_to_app_activity(package_name, device, vc):
   """Tries to press back a number of times to return to the app."""
 
@@ -127,7 +143,7 @@
   while 'com.android.packageinstaller' in activity_str:
     print 'Allowing a permission.'
     perform_vc_dump(vc)
-    vc.findViewById('id/permission_allow_button').touch()
+    touch(device, vc.findViewById('id/permission_allow_button'))
     time.sleep(2)
     activity_str = device.shell('dumpsys window windows '
                                 '| grep -E \'mCurrentFocus\'')
@@ -210,19 +226,17 @@
     first_frag = frag_list[0]
   else:
     first_frag = 'NoFrags'
-
-  directory = (
-      os.path.dirname(os.path.abspath(__file__)) + '/data/' + package_name)
+  directory = (os.path.dirname(os.path.abspath(__file__)) + '/data/' +
+               package_name)
   if not os.path.exists(directory):
     os.makedirs(directory)
   file_num = 0
-  dump_file = os.path.join(
-      directory, activity + '-' + first_frag + '-' + str(file_num) + '.json')
+  dump_file = os.path.join(directory, activity + '-' + first_frag + '-' +
+                           str(file_num) + '.json')
   while os.path.exists(dump_file):
     file_num += 1
-    dump_file = os.path.join(
-        directory,
-        activity + '-' + first_frag + '-' + str(file_num) + '.json')
+    dump_file = os.path.join(directory, activity + '-' + first_frag + '-' +
+                             str(file_num) + '.json')
 
   layout_info = {}
   layout_info['hierarchy'] = {}
@@ -242,7 +256,10 @@
     layout_info['hierarchy'][view.getUniqueId()] = dict_copy
 
   with open(dump_file, 'w') as out_file:
-    json.dump(layout_info, out_file, indent=2)
+    try:
+      json.dump(layout_info, out_file, indent=2)
+    except TypeError:
+      print 'Non-JSON serializable object in Layout.'
 
   screen_name = activity + '-' + first_frag + '-' + str(file_num) + '.png'
   screen_path = os.path.join(directory, screen_name)
@@ -260,8 +277,8 @@
 
 def save_ui_flow_relationships(layout_to_save, package_name):
   """Dumps to file the click dictionary and preceding Layouts."""
-  directory = (
-      os.path.dirname(os.path.abspath(__file__)) + '/data/' + package_name)
+  directory = (os.path.dirname(os.path.abspath(__file__)) + '/data/' +
+               package_name)
   click_file = os.path.join(directory, layout_to_save.get_name() +
                             '-clicks.json')
   click_info = {}
@@ -300,8 +317,13 @@
           view.getWidth() > 0 and
           view.getY() >= STATUS_BAR_HEIGHT and view.getY() <= MAX_Y
           and view.getHeight() > 0):
-        print (view.getId() + ' ' + view.getClass() + ' ' +  str(view.getXY()) +
-               '-- will be clicked')
+        if view.getText():
+          print (view.getId() + ' ' + view.getClass() + ' ' +
+                 str(view.getXY()) + ' ' + view.getText() +
+                 '-- will be clicked')
+        else:
+          print (view.getId() + ' ' + view.getClass() + ' ' +
+                 str(view.getXY()) + '-- will be clicked')
         l.clickable.append(view)
     except AttributeError:
       print 'Could not get view attributes.'
@@ -346,7 +368,8 @@
       curr_layout.preceding.append(prev_name)
   else:
     print 'Lost track of last clicked!'
-
+  print 'Prev layout: ' + prev_layout.get_name()
+  print 'Curr layout: ' + curr_layout.get_name()
   if curr_layout.depth == -1 or curr_layout.depth > prev_layout.depth + 1:
     curr_layout.depth = prev_layout.depth + 1
 
@@ -403,100 +426,119 @@
   return FAILED_FINDING_NAME
 
 
-def find_path_from_root_to_layout(layout, layout_map):
-  """Given a Layout, finds the path of UI elements to that Layout."""
+def find_shortest_path(graph, start, end, prevpath=()):
+  """Use BFS to find the shortest path from the start to end node."""
 
-  path = []
-  curr_path_layout = layout
-  # If there is a splash screen or intro screen that is stored, we could have
-  # multiple Layouts that do not have preceding Layouts.
+  # Modified from http://stackoverflow.com/a/8922151/1076508 to prevent cycles
+  # and account for already visited nodes.
 
-  while curr_path_layout.preceding:
-    print 'Looking for path from ' + layout.get_name()
-    path_layouts = [p[0] for p in path]
-    succeeding_layout = curr_path_layout
-    # TODO(afergan): Using the first element in preceding doesn't ensure
-    # shortest path. Is it worth keeping track of the depth of every Layout to
-    # create the shortest path?
-    curr_path_layout = None
-    for pre in succeeding_layout.preceding:
-      if pre not in path_layouts:
-        curr_path_layout = layout_map.get(pre)
-        break
-      else:
-        return path
+  queue = [[start]]
+  visited = set(prevpath)
 
-    view = find_view_to_lead_to_layout(curr_path_layout, succeeding_layout)
-
-    # This should not happen since if we store the predecessor of one Layout, we
-    # also store which view of the predecessor leads to that Layout. However,
-    # if it does, we can try exploring other preceding layouts
-    if view == FAILED_FINDING_NAME:
-      return []
-    else:
-      print ('Inserting ' + view + ', ' + curr_path_layout.get_name()
-             + ' to path')
-      path.insert(0, (curr_path_layout.get_name(), view))
-
-  return path
+  while queue:
+    path = queue.pop(0)
+    node = path[-1]
+    # Path found.
+    if node == end:
+      return path
+    # Enumerate all adjacent nodes, construct a new path and push it into the
+    # queue.
+    for adjacent in graph.get(node, []):
+      if adjacent not in visited:
+        new_path = list(path)
+        new_path.append(adjacent)
+        queue.append(new_path)
+        visited.add(adjacent)
 
 
 def follow_path_to_layout(path, goal, package_name, device, layout_map,
-                          still_exploring, vc):
+                          layout_graph, still_exploring, vc):
   """Attempt to follow path all the way to the desired layout."""
-  if not path:
-    return is_active_layout(layout_map.values()[0], package_name, device, vc)
 
-  for p in path:
+  # We need to look at the length of path for each iteration since the path can
+  # change when we get off course.
+  i = 0
+  while i < len(path) - 1:
     # We can be lenient here and only evaluate if the activity and fragments are
     # the same (and allow the layout hierarchy to have changed a little bit),
     # since we then evaluate if the clickable view we want is in the Layout.
-    if not is_active_layout(layout_map.get(p[0]), package_name, device, vc):
-      print 'Toto, I\'ve a feeling we\'re not on the right path anymore.'
-      p_idx = path.index(p)
-      if p_idx > 0:
-        activity = obtain_activity_name(package_name, device, vc)
+    p = path[i]
+    p_layout = layout_map.get(p)
+    if is_active_layout(p_layout, package_name, device, vc):
+      print 'Got to ' + p
+      click_id = find_view_to_lead_to_layout(p_layout,
+                                             layout_map.get(path[i+1]))
+      if i > 0 and (layout_map.get(path[i-1]).depth + 1 <
+                    layout_map.get(path[i]).depth):
+        layout_map.get(path[i]).depth = layout_map.get(path[i-1]).depth + 1
 
-        if activity is EXITED_APP:
-          activity = return_to_app_activity(package_name, device, vc)
-
-        vc_dump = perform_vc_dump(vc)
-        curr_layout = obtain_curr_layout(activity, package_name, vc_dump,
-                                         layout_map, still_exploring, device)
-        prev_layout = layout_map.get(path[p_idx - 1][0])
-        prev_clicked = layout_map.get(path[p_idx - 1][1])
-        link_ui_layouts(prev_layout, curr_layout, prev_clicked, package_name)
-
-      return False
-
-    click_id = p[1]
-    if click_id == BACK_BUTTON:
-      perform_press_back(device)
-    else:
-      vc_dump = perform_vc_dump(vc)
-      if vc_dump:
-        click_target = next((view for view in vc_dump
-                             if view.getUniqueId() == click_id), None)
-        if click_target:
-          print 'Clicking on ' + click_target.getUniqueId()
-          try:
-            device.touch(click_target.getX(), click_target.getY())
-          except UnicodeEncodeError:
-            print '***Unicode coordinates'
-          except TypeError:
-            print '***String coordinates'
-
-      else:
-        print ('Could not find the right view to click on, was looking '
-               'for ' + click_id)
+      if click_id == FAILED_FINDING_NAME:
+        print ('Could not find the right view to click on, was looking for ' +
+               click_id)
         return False
+      if click_id == BACK_BUTTON:
+        perform_press_back(device)
+      else:
+        vc_dump = perform_vc_dump(vc)
+        if vc_dump:
+          click_target = next((view for view in vc_dump
+                               if view.getUniqueId() == click_id), None)
+          if click_target:
+            prev_clicked = click_target.getUniqueId()
+            touch(device, click_target)
+        else:
+          return False
+    else:
+      print 'Toto, I\'ve a feeling we\'re not on the right path anymore.'
+      # Remove the edge from the graph so that we don't follow it again (but
+      # don't remove it from our data collection.
+      try:
+        layout_graph[p].remove(path[i+1])
+        print 'Removed edge from ' + p + ' to ' + path[i+1]
+      except KeyError:
+        print ('??? Could not find edge from ' + p + ' to ' + path[i+1] +
+               ' to remove from graph.')
 
-  # Make sure that we end up at the Layout that we want.
-  return is_active_layout(goal, package_name, device, vc)
+      # Figure out where we are & link it to the previous layout, but then try
+      # to still get to the intended Layout.
+      activity = obtain_activity_name(package_name, device, vc)
+
+      if activity is EXITED_APP:
+        activity = return_to_app_activity(package_name, device, vc)
+
+      vc_dump = perform_vc_dump(vc)
+      curr_layout = obtain_curr_layout(activity, package_name, vc_dump,
+                                       layout_map, still_exploring, device)
+
+      prev_layout = layout_map.get(p)
+      path_to_curr = path[:i]
+
+      if not prev_layout.is_duplicate_layout(curr_layout):
+        link_ui_layouts(prev_layout, curr_layout, prev_clicked, package_name)
+        path_to_curr.append(curr_layout.get_name())
+
+      new_path = find_shortest_path(layout_graph, curr_layout.get_name(),
+                                    goal.get_name(), path_to_curr)
+      if new_path:
+        print 'Back on track -- found new route to ' + goal.get_name()
+        path = new_path
+      else:
+        print 'Stopping here. Could not find a way to ' + goal.get_name()
+        return
+
+    i += 1
+
+  # We made it all the way through!
+  if i == len(path) - 1:
+    print 'Got to end of path.'
+    if layout_map.get(path[i-1]).depth + 1 < layout_map.get(path[i]).depth:
+      layout_map.get(path[i]).depth = layout_map.get(path[i-1]).depth + 1
+    # Make sure that we end up at the Layout that we want.
+    return is_active_layout(goal, package_name, device, vc)
 
 
-def crawl_until_exit(vc, device, package_name, layout_map, still_exploring,
-                     start_layout, logged_in):
+def crawl_until_exit(vc, device, package_name, layout_map, layout_graph,
+                     still_exploring, start_layout, logged_in):
   """Main crawler loop. Evaluates layouts, stores new data, and clicks views."""
   print 'Logged in: ' + str(logged_in)
   curr_layout = start_layout
@@ -532,121 +574,124 @@
       if not prev_layout.is_duplicate_layout(curr_layout):
         print 'At a diff layout!'
         link_ui_layouts(prev_layout, curr_layout, prev_clicked, package_name)
+        prev_name = prev_layout.get_name()
+        if prev_name in layout_graph:
+          print 'New set: ' + prev_name + ' ' + curr_layout.get_name()
+          layout_graph.get(prev_name).add(curr_layout.get_name())
+        else:
+          layout_graph[prev_name] = {curr_layout.get_name()}
+          print 'Adding to set: ' + prev_name + ' ' + curr_layout.get_name()
+
+        print len(layout_graph)
+        for key, val in layout_graph.iteritems():
+          print key + ' ' + str(val)
       print 'Layout depth: ' + str(curr_layout.depth)
       print 'Num clickable: ' + str(len(curr_layout.clickable))
 
       if curr_layout.clickable:
-        try:
-          found_login = False
-          if not logged_in:
-            for click in curr_layout.clickable:
-              clickid = click.getUniqueId().lower()
-              if click.getText():
-                clicktext = click.getText().lower()
+        found_login = False
+        if not logged_in:
+          for click in curr_layout.clickable:
+            clickid = click.getUniqueId().lower()
+            if click.getText():
+              clicktext = click.getText().lower()
+              print 'Click text: ' + clicktext
+            else:
+              clicktext = ''
+            if (click.getClass() == 'com.facebook.widget.LoginButton'
+                or any('facebook' in x for x in [clickid, clicktext])
+                or ('fb' in clickid and any(s in clickid for s in
+                                            ['login', 'log_in', 'signin',
+                                             'sign_in']))):
+              found_login = True
+              print 'Trying to log into Facebook.'
+              # Sometimes touch() doesn't work
+              curr_layout.clickable.remove(click)
+              device.shell('input tap ' + str(click.getX()) +
+                           ' ' + str(click.getY()))
+              consec_back_presses = 0
+              prev_clicked = click.getUniqueId()
+              time.sleep(10)
+              # Make sure the new screen is loaded by waiting for the dump.
+              perform_vc_dump(vc)
+              activity_str = obtain_focus_and_allow_permissions(device, vc)
+              print activity_str
+              if 'com.facebook.katana' in activity_str:
+                logged_in = True
+                print 'Logged in!'
+                # Because the Facebook authorization dialog is primarily a
+                # WebView, we must click on x, y coordinates of the Continue
+                # button instead of looking at the hierarchy.
+                device.shell('input tap ' + str(int(.5 * MAX_X)) + ' ' +
+                             str(int(.82 * MAX_Y)))
+                consec_back_presses = 0
+                perform_vc_dump(vc)
+                activity_str = obtain_focus_and_allow_permissions(device, vc)
+
+                # Authorize app to post to Facebook (or any other action).
+                num_taps = 0
+                while ('ProxyAuthDialog' in activity_str and
+                       num_taps < MAX_FB_AUTH_TAPS):
+                  print 'Facebook authorization #' + str(num_taps)
+                  device.shell('input tap ' + str(int(.90 * MAX_X)) + ' ' +
+                               str(int(.95 * MAX_Y)))
+                  num_taps += 1
+                  time.sleep(3)
+                  activity_str = obtain_focus_and_allow_permissions(
+                      device, vc)
               else:
-                clicktext = ''
+                print 'Could not log into Facebook.'
+                print (activity_str + ' ' +
+                       str(obtain_frag_list(package_name, device)))
+              break
+            elif (('gplus' in clickid or 'google' in clickid) and
+                  any(s in clickid for s in ['login', 'log_in', 'signin',
+                                             'sign_in'])):
+              found_login = True
+              print 'Trying to log into Google+.'
+              curr_layout.clickable.remove(click)
+              touch(device, click)
+              consec_back_presses = 0
+              prev_clicked = click.getUniqueId()
+              time.sleep(4)
+              # Make sure the new screen is loaded by waiting for the dump.
+              perform_vc_dump(vc)
 
-              if (click.getClass() == 'com.facebook.widget.LoginButton'
-                  or any('facebook' in x for x in [clickid, clicktext])
-                  or ('fb' in clickid and any(s in clickid for s in
-                                              ['login', 'log_in', 'signin',
-                                               'sign_in']))):
-                found_login = True
-                print 'Trying to log into Facebook.'
-                # Sometimes .touch() doesn't work
-                curr_layout.clickable.remove(click)
-                device.shell('input tap ' + str(click.getX()) +
-                             ' ' + str(click.getY()))
-                consec_back_presses = 0
-                prev_clicked = click.getUniqueId()
-                time.sleep(10)
-                # Make sure the new screen is loaded by waiting for the dump.
-                perform_vc_dump(vc)
-                activity_str = obtain_focus_and_allow_permissions(device, vc)
-                print activity_str
-                if 'com.facebook.katana' in activity_str:
-                  logged_in = True
-                  print 'Logged in!'
-                  # Because the Facebook authorization dialog is primarily a
-                  # WebView, we must click on x, y coordinates of the Continue
-                  # button instead of looking at the hierarchy.
-                  device.shell('input tap ' + str(int(.5 * MAX_X)) + ' ' +
-                               str(int(.82 * MAX_Y)))
-                  consec_back_presses = 0
-                  perform_vc_dump(vc)
-                  activity_str = obtain_focus_and_allow_permissions(device, vc)
+              # Some apps want to access contacts to get user information.
+              activity_str = obtain_focus_and_allow_permissions(device, vc)
 
-                  # Authorize app to post to Facebook (or any other action).
-                  num_taps = 0
-                  while ('ProxyAuthDialog' in activity_str and
-                         num_taps < MAX_FB_AUTH_TAPS):
-                    print 'Facebook authorization #' + str(num_taps)
-                    device.shell('input tap ' + str(int(.90 * MAX_X)) + ' ' +
-                                 str(int(.95 * MAX_Y)))
-                    num_taps += 1
-                    time.sleep(3)
-                    activity_str = obtain_focus_and_allow_permissions(
-                        device, vc)
-                else:
-                  print 'Could not log into Facebook.'
-                  print (activity_str + ' ' +
-                         str(obtain_frag_list(package_name, device)))
-                break
-              elif (('gplus' in clickid or 'google' in clickid) and
-                    any(s in clickid for s in ['login', 'log_in', 'signin',
-                                               'sign_in'])):
-                found_login = True
-                print 'Trying to log into Google+.'
-                curr_layout.clickable.remove(click)
-                device.shell('input tap ' + str(click.getX()) + ' ' +
-                             str(click.getY()))
-                consec_back_presses = 0
-                prev_clicked = click.getUniqueId()
-                time.sleep(4)
-                # Make sure the new screen is loaded by waiting for the dump.
-                perform_vc_dump(vc)
-
-                # Some apps want to access contacts to get user information.
-                activity_str = obtain_focus_and_allow_permissions(device, vc)
-
-                print activity_str
-                if 'com.google.android.gms' in activity_str:
-                  print 'Logging into G+'
-                  # Some apps ask to pick the Google user before logging in.
-                  if 'AccountChipAccountPickerActivity' in activity_str:
-                    print 'Selecting user.'
-                    v = vc.findViewById('id/account_profile_picture')
-                    if v:
-                      device.touch(v.getX(), v.getY())
-                      print 'Selected user.'
-                      time.sleep(4)
-                      perform_vc_dump(vc)
-                    activity_str = obtain_focus_and_allow_permissions(
-                        device, vc)
-                    print activity_str
-                  if 'GrantCredentialsWithAclActivity' in activity_str:
-                    print 'Granting credentials.'
+              print activity_str
+              if 'com.google.android.gms' in activity_str:
+                print 'Logging into G+'
+                # Some apps ask to pick the Google user before logging in.
+                if 'AccountChipAccountPickerActivity' in activity_str:
+                  print 'Selecting user.'
+                  v = vc.findViewById('id/account_profile_picture')
+                  if v:
+                    touch(device, v)
+                    print 'Selected user.'
+                    time.sleep(4)
                     perform_vc_dump(vc)
-                    v = vc.findViewById('id/accept_button')
-                    if v:
-                      print 'granting'
-                      device.touch(v.getX(), v.getY())
-                      time.sleep(4)
-                break
+                  activity_str = obtain_focus_and_allow_permissions(
+                      device, vc)
+                  print activity_str
+                if 'GrantCredentialsWithAclActivity' in activity_str:
+                  print 'Granting credentials.'
+                  perform_vc_dump(vc)
+                  v = vc.findViewById('id/accept_button')
+                  if v:
+                    print 'Granting'
+                    touch(device, v)
+                    time.sleep(4)
+              break
 
-          if not found_login:
-            c = curr_layout.clickable[0]
-            print('Clicking {} {}, ({},{})'.format(c.getUniqueId(),
-                                                   c.getClass(), c.getX(),
-                                                   c.getY()))
-            device.touch(c.getX(), c.getY())
-            consec_back_presses = 0
-            prev_clicked = c.getUniqueId()
-            curr_layout.clickable.remove(c)
-        except UnicodeEncodeError:
-          print '***Unicode coordinates'
-        except TypeError:
-          print '***String coordinates'
+        if not found_login:
+          c = curr_layout.clickable[0]
+          touch(device, c)
+          consec_back_presses = 0
+          prev_clicked = c.getUniqueId()
+          curr_layout.clickable.remove(c)
+
       else:
         print 'Removing ' + curr_layout.get_name() + ' from still_exploring.'
         still_exploring.pop(curr_layout.get_name(), 0)
@@ -683,7 +728,7 @@
             # We have nothing left to click, and the back button doesn't change
             # layouts.
             print 'Pressing back keeps at the current layout'
-            return
+            break
           else:
             link_ui_layouts(prev_layout, curr_layout, 'back button',
                             package_name)
@@ -703,9 +748,11 @@
   set_device_dimens(vc, device)
   # Layout map stores all Layouts that we have seen, while the still_exploring
   # consists of only Layouts that have not been exhaustively explored yet (or
-  # found to be unreachable.)
+  # found to be unreachable.) The Layout graph stores all of the connections
+  # between different screens.
   layout_map = {}
   still_exploring = {}
+  layout_graph = {}
 
   # Stores if we have logged in during this crawl/session. If the app has
   # previously logged into an app or service (and can skip the authorization
@@ -720,37 +767,34 @@
   if not package_name:
     package_name = obtain_package_name(device, vc)
 
-  # Store the root Layout
-  print 'Storing root'
-  vc_dump = perform_vc_dump(vc)
-  if not vc_dump:
-    return
   activity = obtain_activity_name(package_name, device, vc)
   if activity == EXITED_APP:
     return
-  root_layout = obtain_curr_layout(activity, package_name, vc_dump, layout_map,
-                                   still_exploring, device)
-  root_layout.depth = 0
+  vc_dump = perform_vc_dump(vc)
+  if not vc_dump:
+    return
+
+  first_layout = obtain_curr_layout(activity, package_name, vc_dump, layout_map,
+                                    still_exploring, device)
+  first_layout.depth = 0
+
+  print 'Root is ' + first_layout.get_name()
+  num_crawls = 0
 
   logged_in = crawl_until_exit(vc, device, package_name, layout_map,
-                               still_exploring, root_layout, logged_in)
-
-  print 'Root is ' + root_layout.get_name()
-  print 'We have seen ' + str(len(layout_map)) + ' unique layouts.'
-
-  num_crawls = 0
+                               layout_graph, still_exploring, first_layout,
+                               logged_in)
 
   # Recrawl Layouts that aren't completely explored.
   while (still_exploring and num_crawls < MAX_CRAWLS and
          len(layout_map) < MAX_LAYOUTS):
     print 'Crawl #' + str(num_crawls)
     num_crawls += 1
+    print 'We have seen ' + str(len(layout_map)) + ' unique layouts.'
     print 'We still have ' + str(len(still_exploring)) + ' layouts to explore.'
     print 'Still need to explore: ' + str(still_exploring.keys())
     l = still_exploring.values()[0]
     print 'Now trying to explore '+  l.get_name()
-    path = find_path_from_root_to_layout(l, layout_map)
-    print 'Route from root to ' + l.get_name()
 
     # Restart the app with its initial screen.
     subprocess.call([ADB_PATH, '-s', SERIAL_NO, 'shell', 'am force-stop',
@@ -760,31 +804,37 @@
                      '1'])
     time.sleep(5)
 
-    if path:
-      for p in path:
-        print p[0] + ' ' + p[1]
-      reached_layout = follow_path_to_layout(path, l, package_name, device,
-                                             layout_map, still_exploring, vc)
-    else:
-      reached_layout = is_active_layout(l, package_name, device, vc)
-      if reached_layout:
-        print 'At root layout: ' + str(reached_layout)
-      else:
-        print 'No path to ' + l.get_name()
-        still_exploring.pop(l.get_name(), 0)
-    vc_dump = perform_vc_dump(vc)
     activity = obtain_activity_name(package_name, device, vc)
+    if activity == EXITED_APP:
+      return
+    starting_layout = obtain_curr_layout(activity, package_name, vc_dump,
+                                         layout_map, still_exploring, device)
+    starting_layout.depth = 0
+    path = find_shortest_path(layout_graph, starting_layout.get_name(),
+                              l.get_name())
+    if path:
+      print ('Shortest path from ' + starting_layout.get_name() + ' to ' +
+             l.get_name() + ': ' + str(path))
 
-    if reached_layout:
-      print 'Reached the layout we were looking for.'
+      reached_layout = follow_path_to_layout(path, l, package_name, device,
+                                             layout_map, layout_graph,
+                                             still_exploring, vc)
+      if reached_layout:
+        print 'Reached the layout we were looking for.'
+      else:
+        print ('Did not reach intended layout, removing ' + l.get_name() +
+               ' from still_exploring.')
+        still_exploring.pop(l.get_name(), 0)
+      activity = obtain_activity_name(package_name, device, vc)
     else:
-      print ('Did not reach intended layout, removing ' + l.get_name() +
-             ' from still_exploring.')
+      print 'No path to ' + l.get_name() + '. Removing from still_exploring'
       still_exploring.pop(l.get_name(), 0)
 
     if activity == EXITED_APP:
       break
 
+    vc_dump = perform_vc_dump(vc)
+
     if vc_dump:
       curr_layout = obtain_curr_layout(activity, package_name, vc_dump,
                                        layout_map, still_exploring, device)
@@ -795,7 +845,8 @@
         # unexplored views, start crawling again.
         print 'Crawling again'
         logged_in = crawl_until_exit(vc, device, package_name, layout_map,
-                                     still_exploring, curr_layout, logged_in)
+                                     layout_graph, still_exploring, curr_layout,
+                                     logged_in)
         print ('Done with the crawl. Still ' + str(len(l.clickable)) +
                ' views to click for this Layout.')
       else: