capsule: Fix small bugs and refactor.

Fix a few bugs (the device size was stored as a string and not integer value,
device.shell() was not storing screenshots, and printing the view num/component
was messed up) and improve the crawl_package flow.

Change-Id: I4dac941c568f8c69826d6223275c1cbda6c49a57
diff --git a/capsule/crawlui.py b/capsule/crawlui.py
index 518bdcf..4f9e741 100644
--- a/capsule/crawlui.py
+++ b/capsule/crawlui.py
@@ -8,9 +8,10 @@
 import os
 import re
 import subprocess
+import time
 
-from view import View
 from com.dtmilano.android.common import obtainAdbPath
+from view import View
 
 MAX_HEIGHT = 0
 MAX_WIDTH = 0
@@ -21,6 +22,8 @@
 INVISIBLE = 0x4
 GONE = 0x8
 
+ADB_PATH = obtainAdbPath()
+BACK_BUTTON = 'back button'
 # 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'
@@ -47,8 +50,8 @@
   vc_dump = vc.dump(window='-1')
   # Returns a string similar to "Physical size: 1440x2560"
   size = device.shell('wm size')
-  MAX_HEIGHT = extract_between(size, 'x', '\r')
-  MAX_WIDTH = extract_between(size, ': ', 'x')
+  MAX_HEIGHT = int(extract_between(size, 'x', '\r'))
+  MAX_WIDTH = int(extract_between(size, ': ', 'x'))
   NAVBAR_HEIGHT = (
       vc_dump[0].getY() - int(vc_dump[0]['layout:getLocationOnScreen_y()']))
 
@@ -57,17 +60,21 @@
   device.press('KEYCODE_BACK')
 
 
-def attempt_return_to_app(package_name, device):
+def return_to_app_activity(package_name, device):
   """Tries to press back a number of times to return to the app."""
 
-  # Returns whether or not we were successful after NUM_PRESSES attempts.
-  for _ in range(0, NUM_BACK_PRESSES):
+  # Returns the name of the activity, or EXITED_APP if it could not return.
+  for press_num in range(0, NUM_BACK_PRESSES):
     perform_press_back(device)
     activity = obtain_activity_name(package_name, device)
     if activity != EXITED_APP:
-      return True
+      print 'Returned to app'
+      return activity
 
-  return False
+    time.sleep(5)
+    print 'Failed returning to app, attempt #' + str(press_num + 1)
+
+  return EXITED_APP
 
 
 def obtain_activity_name(package_name, device):
@@ -84,14 +91,14 @@
     popup_str = extract_between(activity_str, 'PopupWindow', '}')
     return 'PopupWindow' + popup_str.replace(':', '')
 
-  if package_name not in activity_str:
-    return EXITED_APP
+  if package_name in activity_str:
+    # The current focus returns a string in the format
+    # mCurrentFocus=Window{35f66c3 u0 com.google.zagat/com.google.android.apps.
+    # zagat.activities.BrowseListsActivity}
+    # We only want the text between the final period and the closing bracket.
+    return extract_between(activity_str, '.', '}', -1)
 
-  # The current focus returns a string in the format
-  # mCurrentFocus=Window{35f66c3 u0 com.google.zagat/com.google.android.apps.
-  # zagat.activities.BrowseListsActivity}
-  # We only want the text between the final period and the closing bracket.
-  return extract_between(activity_str, '.', '}', -1)
+  return EXITED_APP
 
 
 def obtain_frag_list(package_name, device):
@@ -99,10 +106,11 @@
   activity_dump = device.shell('dumpsys activity ' + package_name)
   frag_dump = re.findall('Added Fragments:(.*?)FragmentManager', activity_dump,
                          re.DOTALL)
-  if not frag_dump:
-    return 'NoFrag'
-  frag_list = re.findall(': (.*?){', frag_dump[0], re.DOTALL)
-  return frag_list
+  if frag_dump:
+    frag_list = re.findall(': (.*?){', frag_dump[0], re.DOTALL)
+    return frag_list
+
+  return 'NoFrag'
 
 
 def obtain_package_name(device):
@@ -119,7 +127,7 @@
   return pkg_name
 
 
-def save_view_data(package_name, activity, frag_list, vc_dump, device):
+def save_view_data(package_name, activity, frag_list, vc_dump):
   """Stores the view hierarchy and screenshots with unique filenames."""
   # Returns the path to the screenshot and the file number.
 
@@ -159,8 +167,9 @@
 
   screen_name = activity + '-' + first_frag + '-' + str(file_num) + '.png'
   screen_path = os.path.join(directory, screen_name)
-  device.shell('screencap /sdcard/ ' + screen_name)
-  device.shell('pull /sdcard/ ' + screen_name + ' ' + screen_path)
+  # device.shell() does not work for taking/pulling screencaps.
+  subprocess.call([ADB_PATH, 'shell', 'screencap', '/sdcard/' + screen_name])
+  subprocess.call([ADB_PATH, 'pull', '/sdcard/' + screen_name, screen_path])
   # Returns the filename & num so that the screenshot can be accessed
   # programatically.
   return [screen_path, file_num]
@@ -186,23 +195,22 @@
   return -1
 
 
-def create_view(package_name, vc_dump, activity, frag_list, device):
+def create_view(package_name, vc_dump, activity, frag_list):
   """Stores the current view in the View data structure."""
-  screenshot_info = save_view_data(package_name, activity, frag_list, vc_dump,
-                                   device)
+  screenshot_info = save_view_data(package_name, activity, frag_list, vc_dump)
   v = View(activity, frag_list, vc_dump, screenshot_info[0], screenshot_info[1])
 
   for component in v.hierarchy:
     # TODO(afergan): For now, only click on certain components, and allow custom
     # components. Evaluate later if this is worth it or if we should just click
     # on everything attributed as clickable.
-
     if (component.isClickable() and component.getVisibility() == VISIBLE and
         component.getX() >= 0 and component.getX() <= MAX_WIDTH and
         component.getWidth() > 0 and
         component.getY() >= NAVBAR_HEIGHT and component.getY() <= MAX_HEIGHT and
         component.getHeight() > 0):
-      print component.getClass() + '-- will be clicked'
+      print (component.getId() + ' ' + component.getClass()
+             + ' ' + str(component.getXY()) + '-- will be clicked')
       v.clickable.append(component)
 
   return v
@@ -232,55 +240,40 @@
   save_ui_flow_relationships(curr_view, package_name)
 
 
-def obtain_activity_and_view(package_name, vc, view_array, device):
+def obtain_curr_view(activity, package_name, vc_dump, view_array, 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.
 
-  activity = obtain_activity_name(package_name, device)
-  if activity == EXITED_APP:
-    return activity, {}
   frag_list = obtain_frag_list(package_name, device)
-  vc_dump = vc.dump(window='-1')
   view_idx = find_view_idx(activity, frag_list, vc_dump, view_array)
 
   if view_idx >= 0:
     print 'Found duplicate'
-    return activity, view_array[view_idx]
+    return view_array[view_idx]
   else:
     print 'New view'
-    new_view = create_view(package_name, vc_dump, activity, frag_list, device)
+    new_view = create_view(package_name, vc_dump, activity, frag_list)
     view_array.append(new_view)
-    return activity, new_view
+    return new_view
 
 
-def crawl_package(apk_dir, vc, device, debug, package_name=None):
+def crawl_package(vc, device, debug, package_name=None):
   """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)
-  else:
-    # Install the app. device.shell() does not support the install or launch.
-    adb_path = obtainAdbPath()
-    # Install the app.
-    subprocess.call([adb_path, 'install', '-r',
-                     apk_dir + package_name + '.apk'])
-    # Launch the app.
-    subprocess.call([adb_path, 'shell', 'monkey', '-p', package_name, '-c',
-                     'android.intent.category.LAUNCHER', '1'])
 
   # Store the root View
   print 'Storing root'
   vc_dump = vc.dump(window='-1')
   activity = obtain_activity_name(package_name, device)
-  frag_list = obtain_frag_list(package_name, device)
-  view_root = create_view(package_name, vc_dump, activity, frag_list, device)
-  view_array.append(view_root)
+  view_root = obtain_curr_view(activity, package_name, vc_dump, view_array,
+                               device)
   curr_view = view_root
 
   while True:
@@ -291,43 +284,59 @@
     # TODO(afergan): Is this a safe assumption?
     if device.isKeyboardShown():
       perform_press_back(device)
-    else:
-      last_view = curr_view
-      activity, curr_view = obtain_activity_and_view(package_name, vc,
-                                                     view_array, device)
-      if (activity is not EXITED_APP and
-          not last_view.is_duplicate_view(curr_view)):
-        print 'At a diff view!'
-        link_ui_views(last_view, curr_view, last_clicked, package_name,
-                      view_array)
+
+    activity = obtain_activity_name(package_name, device)
+
+    if activity is EXITED_APP:
+      activity = return_to_app_activity(package_name, device)
+      if activity is EXITED_APP:
+        print 'Current view is not app and we cannot return'
+        break
+      else:
+        last_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,
+                                 device)
+    print 'Curr view: ' + curr_view.get_name()
+    if not last_view.is_duplicate_view(curr_view):
+      print 'At a diff view!'
+      link_ui_views(last_view, curr_view, last_clicked, package_name,
+                    view_array)
 
     print 'Num clickable: ' + str(len(curr_view.clickable))
 
     if curr_view.clickable:
       c = curr_view.clickable[-1]
-      print('Clickable: {} {}, ({},{})'.format(c.getUniqueId(), c.getClass(),
-                                               c.getX(), c.getY()))
+      print('Clicking {} {}, ({},{})'.format(c.getUniqueId(), c.getClass(),
+                                             c.getX(), c.getY()))
       c.touch()
-      print str(len(curr_view.clickable)) + ' elements left to click'
       last_clicked = c.getUniqueId()
       del curr_view.clickable[-1]
 
     else:
       print 'Clicking back button'
       perform_press_back(device)
-      activity, curr_view = obtain_activity_and_view(package_name, vc,
-                                                     view_array, device)
-      if activity is not EXITED_APP and last_view.is_duplicate_view(curr_view):
+      last_view = curr_view
+      last_clicked = BACK_BUTTON
+      activity = obtain_activity_name(package_name, device)
+
+      if activity is EXITED_APP:
+        activity = return_to_app_activity(package_name, device)
+        if activity is EXITED_APP:
+          print 'Clicking back took us out of the app'
+          break
+
+      # 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):
         # We have nothing left to click, and the back button doesn't change
         # views.
-        print 'Nothing left to click'
+        print 'Pressing back keeps at the current view'
         break
       else:
-        if activity is not EXITED_APP:
-          link_ui_views(last_view, curr_view, 'back button', package_name,
-                        view_array)
-
-    if activity == EXITED_APP:
-      if not attempt_return_to_app(package_name, device):
-        print 'Left app and could not return'
-        break
\ No newline at end of file
+        link_ui_views(last_view, curr_view, 'back button', package_name,
+                      view_array)
diff --git a/capsule/main.py b/capsule/main.py
index a668ffc..38f20bc 100644
--- a/capsule/main.py
+++ b/capsule/main.py
@@ -5,12 +5,14 @@
 """The main module for the APK Crawler application."""
 
 import os
+import subprocess
 import sys
 
+from com.dtmilano.android.common import obtainAdbPath
 from com.dtmilano.android.viewclient import ViewClient
 import crawlui
 
-
+ADB_PATH = obtainAdbPath()
 # os.environ['ANDROID_ADB_SERVER_PORT'] = '5554'
 APK_DIR = os.path.dirname(os.path.abspath(__file__)) + '/apks/'
 # Whether we should skip the install & load process and just run the program
@@ -32,19 +34,32 @@
 
 
 if __name__ == '__main__':
-
   kwargs1 = {'verbose': True, 'ignoresecuredevice': True}
   kwargs2 = {'startviewserver': True, 'forceviewserveruse': True,
              'autodump': False, 'ignoreuiautomatorkilled': True}
   device, serialno = ViewClient.connectToDeviceOrExit(**kwargs1)
   vc = ViewClient(device, serialno, **kwargs2)
 
-  if not DEBUG:
+  if DEBUG:
+    crawlui.crawl_package(vc, device, DEBUG)
+  else:
     package_list = os.listdir(APK_DIR)
     for package in package_list:
+      # Install and crawl the app. device.shell() does not support the install
+      # or launch.
+      # Install the app.
+      subprocess.call([ADB_PATH, 'install', '-r',
+                       APK_DIR + package])
       if '.apk' in package:
         package_name = os.path.splitext(package)[0]
-        print package_name
-        crawlui.crawl_package(APK_DIR, vc, device, DEBUG, package_name)
-  else:
-    crawlui.crawl_package(APK_DIR, vc, device, DEBUG)
+      else:
+        # If the apk is saved without the extension.
+        package_name = package
+
+      print 'Crawling ' + package_name
+      # Launch the app.
+      subprocess.call([ADB_PATH, 'shell', 'monkey', '-p', package_name, '-c',
+                       'android.intent.category.LAUNCHER', '1'])
+
+      crawlui.crawl_package(vc, device, DEBUG, package_name)
+      subprocess.call([ADB_PATH, 'uninstall', package_name])
diff --git a/capsule/view.py b/capsule/view.py
index 34617a7..1f684e0 100644
--- a/capsule/view.py
+++ b/capsule/view.py
@@ -69,8 +69,8 @@
     """Prints out information about the view."""
     print 'Activity: ' + self.activity
     print 'Fragment: ' + self.frag_list
-    print 'Num: " + str(self.num)'
+    print 'Num: ' + str(self.num)
     print 'Screenshot path:' + self.screenshot
     print 'Hierarchy: '
     for component in self.hierarchy:
-      print component
+      print component.getUniqueId()