capsule: Set layout depth.

For each layout, set the depth as distance from the root Layout.
Also, a few fixes to following the path to return to a Layout, to
improve following a path if we leave the app, and prevent loops where we
don't get to intended Layout but end on one with the same activity name
and fragment list.

Change-Id: I7d19e0dc7f0ccc6b002c73b9de7358fd88d2078f
diff --git a/capsule/capsule.py b/capsule/capsule.py
index 75e27f8..7899114 100644
--- a/capsule/capsule.py
+++ b/capsule/capsule.py
@@ -16,7 +16,7 @@
 
 ADB_PATH = obtainAdbPath()
 HELP_MSG = ('Capsule usage:\n'
-            "python capsule.py DEVICE_SERIAL [flag] <argument>\n"
+            'python capsule.py DEVICE_SERIAL [flag] <argument>\n'
             'No command line flags -- crawl current package\n'
             '-d or --dir /PATH_TO_APKS/  -- install and run APKS from a '
             'directory.\n'
@@ -79,7 +79,7 @@
   # to do bulk crawling since we do not need to worry about the device memory
   # filling up.
   uninstall = False
-  should_crawl = False
+  recrawl = False
   package_list = []
 
   try:
@@ -125,7 +125,7 @@
       elif opt in ('-h', '--help'):
         print HELP_MSG
       elif opt in ('-r', '--recrawl'):
-        should_crawl = True
+        recrawl = True
       else:
         print ('Unhandled option. Use -h or --help for a listing of '
                'commands')
@@ -138,6 +138,7 @@
       # Possibly install, then launch and crawl the app. device.shell() does
       # not support the install or launch.
       package_name = ''
+      should_crawl = True
       if '.apk' in package:
         package_name = crawlpkg.extract_between(package, '/', '.apk', -1)
 
@@ -151,17 +152,14 @@
                                                   'list packages', '-3'])
         if package_name not in installed_pkgs:
           print 'Cannot find the package on the device.'
-          break
-      if not os.path.exists(os.path.dirname(os.path.abspath(__file__))
-                            + '/data/' + package_name):
-        should_crawl = True
-      else:
-        print ('Package has already been crawled and should_crawl is set to ' +
-               str(should_crawl))
+          should_crawl = False
+
+      if os.path.exists(os.path.dirname(os.path.abspath(__file__)) + '/data/' +
+                        package_name) and not recrawl:
+        should_crawl = False
 
       if should_crawl:
         print 'Crawling ' + package_name
-        should_crawl = True
         # Launch the app.
         subprocess.call([ADB_PATH, '-s', serialno, 'shell', 'monkey', '-p',
                          package_name, '-c', 'android.intent.category.LAUNCHER',
diff --git a/capsule/crawlpkg.py b/capsule/crawlpkg.py
index 7430b82..ea7a285 100644
--- a/capsule/crawlpkg.py
+++ b/capsule/crawlpkg.py
@@ -49,6 +49,9 @@
 MAX_CONSEC_BACK_PRESSES = 10
 MAX_FB_AUTH_TAPS = 5
 
+NEGATIVE_WORDS = ['no', 'cancel', 'back', 'negative', 'neg' 'deny', 'previous',
+                  'prev', 'exit', 'delete', 'end']
+
 
 def extract_between(text, sub1, sub2, nth=1):
   """Extracts a substring from text between two given substrings."""
@@ -264,6 +267,7 @@
   click_info = {}
   click_info['click_dict'] = layout_to_save.click_dict
   click_info['preceding'] = layout_to_save.preceding
+  click_info['depth'] = layout_to_save.depth
   with open(click_file, 'w') as out_file:
     json.dump(click_info, out_file, indent=2)
 
@@ -290,20 +294,39 @@
   l = Layout(activity, frag_list, vc_dump, screenshot, num)
 
   for view in l.hierarchy:
-    # TODO(afergan): For now, only click on certain views, and allow custom
-    # views. Evaluate later if this is worth it or if we should just click
-    # on everything attributed as clickable.
     try:
       if (view.isClickable() and view.getVisibility() == VISIBLE and
           view.getX() >= 0 and view.getX() <= MAX_X and
           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')
+        print (view.getId() + ' ' + view.getClass() + ' ' +  str(view.getXY()) +
+               '-- will be clicked')
         l.clickable.append(view)
     except AttributeError:
       print 'Could not get view attributes.'
+
+  # For views that cancel or bring us back, click on them last. However, do not
+  # hold this against views with the unique id id/no_id/##.
+  for clickview in l.clickable:
+    if 'no_id' in clickview.getUniqueId().lower():
+      clickid = ''
+    else:
+      clickid = clickview.getUniqueId().lower()
+
+    if clickview.getText():
+      print 'Text: ' + clickview.getText()
+      clicktext = clickview.getText().lower()
+      if any(x in [clicktext, clickid] for x in NEGATIVE_WORDS):
+        print ('Going to the end of the list b/c of text or ID: ' + clicktext +
+               ' ' + clickid)
+        l.clickable.remove(clickview)
+        l.clickable.append(clickview)
+    elif any(x in clickid for x in NEGATIVE_WORDS):
+      print 'Going to the end of the list b/c of ID: ' + clickid
+      l.clickable.remove(clickview)
+      l.clickable.append(clickview)
+
   return l
 
 
@@ -318,10 +341,15 @@
   if prev_clicked:
     print 'Previous clicked: ' + prev_clicked
     prev_layout.click_dict[prev_clicked] = curr_layout.get_name()
-    curr_layout.preceding.append(prev_layout.get_name())
+    prev_name = prev_layout.get_name()
+    if prev_name not in curr_layout.preceding:
+      curr_layout.preceding.append(prev_name)
   else:
     print 'Lost track of last clicked!'
 
+  if curr_layout.depth == -1 or curr_layout.depth > prev_layout.depth + 1:
+    curr_layout.depth = prev_layout.depth + 1
+
   # TODO(afergan): Remove this later. For debugging, we print the clicks after
   # each click to a new layout is recorded. However, this results in a lot of
   # repeated writes to the same file. In the future, we can just write each
@@ -370,7 +398,8 @@
         layout2.get_name())]
   except ValueError:
     print '*** Could not find a view to link to the succeeding Layout!'
-
+    print (str(layout1.click_dict) + ' does not have a path to ' +
+           layout2.get_name())
   return FAILED_FINDING_NAME
 
 
@@ -429,7 +458,8 @@
         activity = obtain_activity_name(package_name, device, vc)
 
         if activity is EXITED_APP:
-          return False
+          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)
@@ -449,7 +479,13 @@
                              if view.getUniqueId() == click_id), None)
         if click_target:
           print 'Clicking on ' + click_target.getUniqueId()
-          device.touch(click_target.getX(), click_target.getY())
+          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)
@@ -496,7 +532,7 @@
       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)
-
+      print 'Layout depth: ' + str(curr_layout.depth)
       print 'Num clickable: ' + str(len(curr_layout.clickable))
 
       if curr_layout.clickable:
@@ -504,20 +540,25 @@
           found_login = False
           if not logged_in:
             for click in curr_layout.clickable:
-              click_id = click.getUniqueId().lower()
-              if (click.getClass() == 'com.facebook.widget.LoginButton' or
-                  ('facebook' in click_id) or ('fb' in click_id and
-                                               any(s in click_id for s in
-                                                   ['login', 'log_in', 'signin',
-                                                    'sign_in']))):
+              clickid = click.getUniqueId().lower()
+              if click.getText():
+                clicktext = click.getText().lower()
+              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()
-                curr_layout.clickable.remove(click)
                 time.sleep(10)
                 # Make sure the new screen is loaded by waiting for the dump.
                 perform_vc_dump(vc)
@@ -525,6 +566,7 @@
                 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.
@@ -545,20 +587,21 @@
                     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)))
-              elif (('gplus' in click_id or 'google' in click_id) and
-                    any(s in click_id for s in ['login', 'log_in', 'signin',
-                                                'sign_in'])):
+                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+.'
-                device.touch(str(click.getX()), str(click.getY()))
+                curr_layout.clickable.remove(click)
+                device.shell('input tap ' + str(click.getX()) + ' ' +
+                             str(click.getY()))
                 consec_back_presses = 0
                 prev_clicked = click.getUniqueId()
-                curr_layout.clickable.remove(click)
                 time.sleep(4)
                 # Make sure the new screen is loaded by waiting for the dump.
                 perform_vc_dump(vc)
@@ -575,7 +618,7 @@
                     v = vc.findViewById('id/account_profile_picture')
                     if v:
                       device.touch(v.getX(), v.getY())
-                      print 'selected user.'
+                      print 'Selected user.'
                       time.sleep(4)
                       perform_vc_dump(vc)
                     activity_str = obtain_focus_and_allow_permissions(
@@ -589,6 +632,7 @@
                       print 'granting'
                       device.touch(v.getX(), v.getY())
                       time.sleep(4)
+                break
 
           if not found_login:
             c = curr_layout.clickable[0]
@@ -601,6 +645,8 @@
             curr_layout.clickable.remove(c)
         except UnicodeEncodeError:
           print '***Unicode coordinates'
+        except TypeError:
+          print '***String coordinates'
       else:
         print 'Removing ' + curr_layout.get_name() + ' from still_exploring.'
         still_exploring.pop(curr_layout.get_name(), 0)
@@ -684,6 +730,8 @@
     return
   root_layout = obtain_curr_layout(activity, package_name, vc_dump, layout_map,
                                    still_exploring, device)
+  root_layout.depth = 0
+
   logged_in = crawl_until_exit(vc, device, package_name, layout_map,
                                still_exploring, root_layout, logged_in)
 
@@ -723,7 +771,7 @@
         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)
 
diff --git a/capsule/layout.py b/capsule/layout.py
index 4aa42e6..c467acd 100644
--- a/capsule/layout.py
+++ b/capsule/layout.py
@@ -24,6 +24,7 @@
     self.clickable = []
     self.preceding = []
     self.click_dict = {}
+    self.depth = -1
 
   def get_name(self):
     """Returns the identifying name of the Layout."""