apkcrawler: Store fragment list.

Use the developer-provided fragments instead of the system fragment
name. Since each fragment may be composed of multiple fragments, we
store them all in a list, but use the first one for file naming
purposes.

Change-Id: I584309de7c12b6c1c89d688e5bd878026fae0d53
diff --git a/crawlui.py b/crawlui.py
index add0233..4d9b5b1 100644
--- a/crawlui.py
+++ b/crawlui.py
@@ -7,9 +7,9 @@
 from view import View
 
 # Linux ADB path
-ADB_PATH = os.path.expanduser('~') + '/Android/Sdk/platform-tools/adb'
+# 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,6 +22,10 @@
 INVISIBLE = 0x4
 GONE = 0x8
 
+# 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'
+
 view_root = []
 view_array = []
 
@@ -30,10 +34,10 @@
   subprocess.call([ADB_PATH, 'shell', 'input', 'keyevent', '4'])
 
 
-def get_activity_name():
+def get_activity_name(package_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.
+  # TODO(afergan): See if we can consolidate this with get_fragment_list, but
+  # still make sure that the current app has focus.
 
   proc = subprocess.Popen([ADB_PATH, 'shell', 'dumpsys window windows '
                                               '| grep -E \'mCurrentFocus\''],
@@ -46,6 +50,14 @@
     popup_str = activity_str[activity_str.find('PopupWindow'):].split('}')[0]
     return popup_str.replace(':', '')
 
+  # We are no longer in the app.
+  if package_name not in activity_str:
+    print 'Exited app'
+    # If app opened a different app, try to get back to it.
+    perform_press_back()
+    if package_name not in activity_str:
+      return EXITED_APP
+
   # The current focus returns a string in the format
   # mCurrentFocus=Window{35f66c3 u0 com.google.zagat/com.google.android.apps.
   # zagat.activities.BrowseListsActivity}
@@ -53,32 +65,31 @@
   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, _ = proc.communicate()
-
-  fragment_name = re.search('Local FragmentActivity (.*?) State:', fragment_str)
-  if fragment_name is None:
+def get_frag_list(package_name):
+  """Gets the list of fragments in the current view."""
+  proc = subprocess.Popen([ADB_PATH, 'shell', 'dumpsys activity', package_name],
+                          stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+  adb_dump, _ = proc.communicate()
+  frag_dump = re.findall('Added Fragments:(.*?)FragmentManager', adb_dump,
+                         re.DOTALL)
+  if not frag_dump:
     return 'NoFrag'
-  return re.search('Local FragmentActivity (.*?) State:', fragment_str).group(1)
+  frag_list = re.findall(': (.*?){', frag_dump[0], re.DOTALL)
+  print frag_list
+  return frag_list
 
 
-def save_screenshot(package_name, activity, fragment):
+def save_screenshot(package_name, activity, frag_list):
   """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
-  while os.path.exists(directory + '/' + activity + '-' + fragment + '-' +
+  while os.path.exists(directory + '/' + activity + '-' + frag_list[0] + '-' +
                        str(screenshot_num) + '.png'):
     screenshot_num += 1
-  screen_name = activity + '-' + fragment + '-' + str(screenshot_num) + '.png'
+  screen_name = activity + '-' + frag_list[0] + '-' + str(screenshot_num) + '.png'
   print screen_name
   screen_path = directory + '/' + screen_name
   subprocess.call([ADB_PATH, 'shell', 'screencap', '/sdcard/' + screen_name])
@@ -89,18 +100,19 @@
   return [screen_path, screenshot_num]
 
 
-def find_view_idx(vc_dump, activity, fragment):
+def find_view_idx(vc_dump, activity, frag_list):
+  """Find 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,
-                                  fragment, vc_dump):
+                                  frag_list, vc_dump):
       return i
   return -1
 
 
-def create_view(package_name, vc_dump, activity, fragment):
+def create_view(package_name, vc_dump, activity, frag_list, debug):
   """Store the current view in the View data structure."""
-  v = View(activity, fragment)
-  v.hierarchy = vc_dump
+  screenshot_info = save_screenshot(package_name, activity, frag_list)
+  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
@@ -126,14 +138,14 @@
           'com.android.settings.dashboard.DashboardTileView')  or
          component['class'] == 'android.widget.ImageView' or
          component.parent == 'android.widget.ListView'
-         'android' not in component['class'])):
+         'android' not in component['class']) and
+         # TODO(afergan): Remove this.
+         # For debugging purposes, get rid of the phantom clickable components
+         # that Zagat creates.
+         not (debug and component.getXY() == (0, 273))):
       print component['class'] + '-- will be clicked'
       v.clickable.append(component)
 
-  screenshot_info = save_screenshot(package_name, activity, fragment)
-  v.screenshot = screenshot_info[0]
-  v.num = screenshot_info[1]
-
   return v
 
 
@@ -151,11 +163,13 @@
   # Store the root View
   print 'Storing root'
   vc_dump = vc.dump(window='-1')
-  activity = get_activity_name()
-  fragment = get_fragment_name(package_name)
+  activity = get_activity_name(package_name)
+  if activity == EXITED_APP:
+    return
+  frag_list = get_frag_list(package_name)
   global view_root
   view_root = create_view(package_name, vc_dump, activity,
-                          fragment)
+                          frag_list, debug)
   view_array.append(view_root)
 
   while True:
@@ -164,13 +178,13 @@
       perform_press_back()
 
     # Determine if this is a View that has already been seen.
-    view_idx = find_view_idx(vc_dump, activity, fragment)
+    view_idx = find_view_idx(vc_dump, activity, frag_list)
     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)
+      curr_view = create_view(package_name, vc_dump, activity, frag_list, debug)
       view_array.append(curr_view)
 
     print 'Num clickable: ' + str(len(curr_view.clickable))
@@ -192,5 +206,8 @@
         return
 
     vc_dump = vc.dump(window='-1')
-    activity = get_activity_name()
-    fragment = get_fragment_name(package_name)
+    activity = get_activity_name(package_name)
+    if activity == EXITED_APP:
+      return
+    frag_list = get_frag_list(package_name)
+
diff --git a/view.py b/view.py
index 98e7474..6863b63 100644
--- a/view.py
+++ b/view.py
@@ -10,26 +10,32 @@
   components and their resulting views.
   """
 
-  def __init__(self, activity, fragment):
+  def __init__(self, activity, frag_list, hierarchy, screenshot, num):
     self.activity = activity
-    self.fragment = fragment
-    self.num = 0
-    self.screenshot = ''
-    self.hierarchy = []
+    self.frag_list = frag_list
+    self.hierarchy = hierarchy
+    self.screenshot = screenshot
+    self.num = num
     self.clickable = []
     self.preceding = []
 
   def get_name(self):
-    # Return the identifying name of the View (activity, fragment, and number).
-    return [self.activity, self.fragment, self.num]
+    # Return the identifying name of the View (activity, fragment list, and
+    # number).
+    return [self.activity, self.frag_list, self.num]
 
   def num_components(self):
     return len(self.hierarchy)
 
-  def is_duplicate(self, cv_activity, cv_fragment, cv_hierarchy):
+  def is_duplicate(self, cv_activity, cv_frag_list, cv_hierarchy):
     """Determine if the passed-in current view is identical to this View."""
 
-    if self.activity != cv_activity or self.fragment != cv_fragment:
+    # 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)):
       return False
 
     if len(cv_hierarchy) != self.num_components():
@@ -38,15 +44,12 @@
     hierarchy_ids = [h['uniqueId'] for h in self.hierarchy]
     curr_view_ids = [cv['uniqueId'] for cv in cv_hierarchy]
 
-    # Since the unique ids 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
     return Counter(hierarchy_ids) == Counter(curr_view_ids)
 
   def print_info(self):
 
     print 'Activity: ' + self.activity
-    print 'Fragment: ' + self.fragment
+    print 'Fragment: ' + self.frag_list
     print 'Num: " + str(self.num)'
     print 'Screenshot path:' + self.screenshot
     print 'Hierarchy: '