apkcrawler: Fix nesting & gpylint.

Remove recursion because of view graph cycles and conform code to Google style
guide rules.

Change-Id: Icc0d4906c678d2dbb21f5da3e95138f6bb454c92
diff --git a/crawlui.py b/crawlui.py
index b9ac9e3..c5ae36c 100644
--- a/crawlui.py
+++ b/crawlui.py
@@ -1,15 +1,15 @@
 """A module for installing and crawling the UI of Android application."""
 
+import os
 import re
 import subprocess
-import sys
-import os
-import time
+
+from view import View
 
 # Linux ADB path
 ADB_PATH = os.path.expanduser('~') + '/Android/Sdk/platform-tools/adb'
 # OS X ADB path
-#ADB_PATH = '/usr/local/bin/adb'
+# ADB_PATH = '/usr/local/bin/adb'
 
 MAX_WIDTH = 1080
 # TODO(afergan): For layouts longer than the width of the screen, scroll down
@@ -22,31 +22,29 @@
 INVISIBLE = 0x4
 GONE = 0x8
 
-from com.dtmilano.android.viewclient import ViewClient
-from subprocess import check_output
-from view import View
-
 view_root = []
 view_array = []
 
+
 def perform_press_back():
   subprocess.call([ADB_PATH, 'shell', 'input', 'keyevent', '4'])
 
+
 def get_activity_name():
   """Gets the current running activity of the package."""
   # TODO(afergan): Make sure we are still running the correct package and have
   # not exited or redirected to a different app.
 
   proc = subprocess.Popen([ADB_PATH, 'shell', 'dumpsys window windows '
-                          '| grep -E \'mCurrentFocus\''],
-                          stdout = subprocess.PIPE, stderr = subprocess.PIPE)
-  activity_str, err = proc.communicate()
+                                              '| grep -E \'mCurrentFocus\''],
+                          stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+  activity_str, _ = proc.communicate()
 
   # If a popup menu has captured the focus, the focus will be in the format
   # mCurrentFocus=Window{8f1328e u0 PopupWindow:53a5957}
   if 'PopupWindow' in activity_str:
     popup_str = activity_str[activity_str.find('PopupWindow'):].split('}')[0]
-    return popup_str.replace(':','')
+    return popup_str.replace(':', '')
 
   # The current focus returns a string in the format
   # mCurrentFocus=Window{35f66c3 u0 com.google.zagat/com.google.android.apps.
@@ -54,29 +52,31 @@
   # We only want the text between the final period and the closing bracket.
   return activity_str.split('.')[-1].split('}')[0]
 
+
 def get_fragment_name(package_name):
   """Gets the current top fragment of the package."""
   proc = subprocess.Popen([ADB_PATH, 'shell', 'dumpsys activity ',
-                          package_name, ' | grep -E '
-                          '\'Local FragmentActivity\''], stdout =
-                          subprocess.PIPE, stderr = subprocess.PIPE)
-  fragment_str, err = proc.communicate()
+                           package_name, ' | grep -E '
+                                         '\'Local FragmentActivity\''],
+                          stdout=subprocess.PIPE,
+                          stderr=subprocess.PIPE)
+  fragment_str, _ = proc.communicate()
 
-  fragment_name = re.search('Local FragmentActivity (.*?) State:',fragment_str)
+  fragment_name = re.search('Local FragmentActivity (.*?) State:', fragment_str)
   if fragment_name is None:
     return 'NoFrag'
-  return re.search('Local FragmentActivity (.*?) State:',fragment_str).group(1)
+  return re.search('Local FragmentActivity (.*?) State:', fragment_str).group(1)
 
-def save_screenshot(package_name):
+
+def save_screenshot(package_name, activity, fragment):
+  """Store the screenshot with a unique filename."""
   directory = (os.path.dirname(os.path.abspath(__file__)) + '/data/'
                + package_name)
   if not os.path.exists(directory):
     os.makedirs(directory)
   screenshot_num = 0
-  activity = get_activity_name()
-  fragment = get_fragment_name(package_name)
-  while os.path.exists(directory + '/' + activity + '-' + fragment + '-' + str(
-                       screenshot_num) + '.png'):
+  while os.path.exists(directory + '/' + activity + '-' + fragment + '-' +
+                       str(screenshot_num) + '.png'):
     screenshot_num += 1
   screen_name = activity + '-' + fragment + '-' + str(screenshot_num) + '.png'
   print screen_name
@@ -88,6 +88,7 @@
   # programatically.
   return [screen_path, screenshot_num]
 
+
 def find_view_idx(package_name, vc_dump):
   for i in range(len(view_array)):
     if view_array[i].is_duplicate(get_activity_name(),
@@ -95,7 +96,9 @@
       return i
   return -1
 
-def create_view(package_name, vc_dump):
+
+def create_view(package_name, vc_dump, activity, fragment):
+  """Store the current view in the View data structure."""
   v = View(get_activity_name(), get_fragment_name(package_name))
   v.hierarchy = vc_dump
 
@@ -106,76 +109,87 @@
 
     # TODO(afergan): Should we include clickable ImageViews? Seems like a lot of
     # false clicks for this so far...
+    if component.isClickable():
+      print (component['class'] + ' ' + str(component.getX()) + ' '
+             + str(component.getY()))
     if (component.isClickable() and component.getVisibility() == VISIBLE and
-        component.getX() >= 2 and component.getY() <= MAX_WIDTH and
+        component.getX() >= 0 and component.getY() <= MAX_WIDTH and
         component['layout:layout_width'] > 0 and
-        component.getY() >= (NAVBAR_HEIGHT + 2) and
+        component.getY() >= (NAVBAR_HEIGHT) and
         component.getY() <= MAX_HEIGHT and
         component['layout:layout_height'] > 0 and
         ('Button' in component['class'] or
-        (component ['class'] ==
-        'com.android.settings.dashboard.DashboardTileView')  or
-        component['class'] == 'android.widget.ImageView' or
-        'ActionMenuItemView' in component['class'] or
-        'TextView' in component['class'] or
-        'android' not in component['class'] or
-        'Spinner' in component['class'] or
-        component.parent == 'android.widget.ListView')):
-          print component['class'] + '-- will be clicked'
-          v.clickable.append(component)
+         'TextView' in component['class'] or
+         'ActionMenuItemView' in component['class'] or
+         'Spinner' in component['class'] or
+         (component['class'] ==
+          'com.android.settings.dashboard.DashboardTileView')  or
+         component['class'] == 'android.widget.ImageView' or
+         component.parent == 'android.widget.ListView'
+         'android' not in component['class'])):
+      print component['class'] + '-- will be clicked'
+      v.clickable.append(component)
 
-  screenshot_info = save_screenshot(package_name)
+  screenshot_info = save_screenshot(package_name, activity, fragment)
   v.screenshot = screenshot_info[0]
   v.num = screenshot_info[1]
 
   return v
 
-def crawl_activity(package_name, vc, device):
-  vc_dump = vc.dump(window='-1')
-
-  # Returning to a view that has already been seen.
-  view_idx = find_view_idx(package_name, vc_dump)
-  if view_idx >= 0:
-    print('**FOUND DUPLICATE')
-    curr_view = view_array[view_idx]
-  else:
-    print('**NEW VIEW')
-    curr_view = create_view(package_name, vc_dump)
-    view_array.append(curr_view)
-
-  print 'Num clickable: ' + str(len(curr_view.clickable))
-
-  if len(curr_view.clickable) > 0:
-    c = curr_view.clickable[0]
-    print c
-    print ('Clickable: ' + c['uniqueId'] + ' ' + c['class'] + ' ' +
-      str(c.getX()) + ' ' + str(c.getY()))
-    subprocess.call([ADB_PATH, 'shell', 'input', 'tap', str(c.getX()),
-                   str(c.getY())])
-    print str(len(curr_view.clickable)) + ' elements left to click'
-    del curr_view.clickable[0]
-
-  else:
-    print '!!! Clicking back button'
-    if curr_view == view_root:
-      return
-    perform_press_back()
-
-  crawl_activity(package_name, vc, device)
 
 def crawl_package(apk_dir, package_name, vc, device, debug):
-  if (not(debug)):
+  """Main crawler loop. Evaluate views, store new views, and click on items."""
+
+  if not debug:
     # Install the app.
     subprocess.call([ADB_PATH, 'install', '-r', apk_dir + package_name
-                    + '.apk'])
-    #Launch the app.
+                     + '.apk'])
+    # Launch the app.
     subprocess.call([ADB_PATH, 'shell', 'monkey', '-p', package_name, '-c',
-                    'android.intent.category.LAUNCHER', '1'])
+                     'android.intent.category.LAUNCHER', '1'])
 
-  #Store the root View
+  # Store the root View
   print 'Storing root'
   vc_dump = vc.dump(window='-1')
-  view_root = create_view(package_name, vc_dump)
+  activity = get_activity_name()
+  fragment = get_fragment_name(package_name)
+  global view_root
+  view_root = create_view(package_name, vc_dump, get_activity_name(),
+                          get_fragment_name(package_name))
   view_array.append(view_root)
 
-  crawl_activity(package_name, vc, device)
\ No newline at end of file
+  while True:
+
+    if device.isKeyboardShown():
+      perform_press_back()
+    activity = get_activity_name()
+    fragment = get_fragment_name(package_name)
+    # Determine if this is a View that has already been seen.
+    view_idx = find_view_idx(package_name, vc_dump)
+    if view_idx >= 0:
+      print '**FOUND DUPLICATE'
+      curr_view = view_array[view_idx]
+    else:
+      print '**NEW VIEW'
+      curr_view = create_view(package_name, vc_dump, activity, fragment)
+      view_array.append(curr_view)
+
+    print 'Num clickable: ' + str(len(curr_view.clickable))
+
+    if curr_view.clickable:
+      c = curr_view.clickable[0]
+      print c
+      print ('Clickable: ' + c['uniqueId'] + ' ' + c['class'] + ' ' +
+             str(c.getX()) + ' ' + str(c.getY()))
+      subprocess.call([ADB_PATH, 'shell', 'input', 'tap', str(c.getX()),
+                       str(c.getY())])
+      print str(len(curr_view.clickable)) + ' elements left to click'
+      del curr_view.clickable[0]
+
+    else:
+      print '!!! Clicking back button'
+      perform_press_back()
+      if curr_view == view_root:
+        return
+
+    vc_dump = vc.dump(window='-1')
diff --git a/main.py b/main.py
index 75c51b4..f5a976f 100644
--- a/main.py
+++ b/main.py
@@ -1,8 +1,9 @@
 """The main module for the APK Crawler application."""
 
-import sys
-import subprocess
 import os
+import sys
+
+from com.dtmilano.android.viewclient import ViewClient
 import crawlui
 
 # os.environ['ANDROID_ADB_SERVER_PORT'] = '5554'
@@ -14,17 +15,15 @@
 # PyDev sets PYTHONPATH, use it
 try:
   for p in os.environ['PYTHONPATH'].split(':'):
-    if not p in sys.path:
+    if p not in sys.path:
       sys.path.append(p)
-except:
-  pass
+except KeyError:
+  print 'Please set the environment variable PYTHONPATH'
 
 try:
   sys.path.append(os.path.join(os.environ['ANDROID_VIEW_CLIENT_HOME'], 'src'))
-except:
-  pass
-
-from com.dtmilano.android.viewclient import ViewClient
+except KeyError:
+  print 'Please set the environment variable ANDROID_VIEW_CLIENT_HOME'
 
 
 if __name__ == '__main__':
@@ -35,7 +34,7 @@
   device, serialno = ViewClient.connectToDeviceOrExit(**kwargs1)
   vc = ViewClient(device, serialno, **kwargs2)
 
-  if (not(DEBUG)):
+  if not DEBUG:
     package_list = os.listdir(APK_DIR)
     for package in package_list:
       app_name = package.split('.apk')[0]
@@ -46,4 +45,4 @@
     package = 'com.google.zagat.apk'
     app_name = package.split('.apk')[0]
     print app_name
-    crawlui.crawl_package(APK_DIR, app_name, vc, device, DEBUG)
\ No newline at end of file
+    crawlui.crawl_package(APK_DIR, app_name, vc, device, DEBUG)
diff --git a/view.py b/view.py
index 834baec..98e7474 100644
--- a/view.py
+++ b/view.py
@@ -1,8 +1,14 @@
+"""View class definition."""
+
 from collections import Counter
 
-class View:
-  """Base class for all views. Includes the view hierarchy, screenshot, and
-     information about clickable components and their resulting views."""
+
+class View(object):
+  """Base class for all views.
+
+  Includes the view hierarchy, screenshot, and information about clickable
+  components and their resulting views.
+  """
 
   def __init__(self, activity, fragment):
     self.activity = activity
@@ -21,10 +27,7 @@
     return len(self.hierarchy)
 
   def is_duplicate(self, cv_activity, cv_fragment, cv_hierarchy):
-    """Determine if the passed-in current view is identical to this View.
-       Right now we do it by ensuring that the activity & fragment names are
-       the same and that there is the same list of components in the view
-       hierarchies."""
+    """Determine if the passed-in current view is identical to this View."""
 
     if self.activity != cv_activity or self.fragment != cv_fragment:
       return False
@@ -42,10 +45,10 @@
 
   def print_info(self):
 
-    print "Activity: " + self.activity
-    print "Fragment: " + self.fragment
-    print "Num: " + str(self.num)
-    print "Screenshot path:" + self.screenshot
-    print "Hierarchy: "
+    print 'Activity: ' + self.activity
+    print 'Fragment: ' + self.fragment
+    print 'Num: " + str(self.num)'
+    print 'Screenshot path:' + self.screenshot
+    print 'Hierarchy: '
     for component in self.hierarchy:
-      print component
\ No newline at end of file
+      print component