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