apkcrawler: Store view hierarchy and fix clicking.
Store the view hierarchy and fragment list in a JSON file. Also, improve
clicking by using the right height/width parameters and converting them
from unicode to int.
Change-Id: If08574181faaa711f1b921414e1c6c75fc07ade6
diff --git a/crawlui.py b/apkcrawler/crawlui.py
similarity index 62%
rename from crawlui.py
rename to apkcrawler/crawlui.py
index 4d9b5b1..4d94719 100644
--- a/crawlui.py
+++ b/apkcrawler/crawlui.py
@@ -1,5 +1,7 @@
"""A module for installing and crawling the UI of Android application."""
+import copy
+import json
import os
import re
import subprocess
@@ -7,15 +9,16 @@
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
+# Nexus 6 dimensions.
+MAX_WIDTH = 1440
# TODO(afergan): For layouts longer than the width of the screen, scroll down
# and click on them. For now, we ignore them.
-MAX_HEIGHT = 1920
-NAVBAR_HEIGHT = 63
+MAX_HEIGHT = 2560
+NAVBAR_HEIGHT = 84
# Visibility
VISIBLE = 0x0
@@ -26,9 +29,6 @@
# activities cannot have spaces, we ensure that no activity will be named this.
EXITED_APP = 'exited app'
-view_root = []
-view_array = []
-
def perform_press_back():
subprocess.call([ADB_PATH, 'shell', 'input', 'keyevent', '4'])
@@ -79,70 +79,74 @@
return frag_list
-def save_screenshot(package_name, activity, frag_list):
+def save_view_data(package_name, activity, frag_list, vc_dump):
"""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 + '-' + frag_list[0] + '-' +
- str(screenshot_num) + '.png'):
- screenshot_num += 1
- screen_name = activity + '-' + frag_list[0] + '-' + str(screenshot_num) + '.png'
- print screen_name
- screen_path = directory + '/' + screen_name
+ file_num = 0
+ dump_file = os.path.join(directory, activity + '-' + frag_list[0] + '-'
+ + str(file_num) + '.json')
+ while os.path.exists(dump_file):
+ file_num += 1
+ dump_file = os.path.join(directory, activity + '-' + frag_list[0] + '-'
+ + str(file_num) + '.json')
+
+ view_info = {}
+ view_info['hierarchy'] = {}
+ view_info['fragmentList'] = frag_list
+
+ for component in vc_dump:
+ # Because the children and parent are each instances, they are not JSON
+ # serializable. We replace them with just the ids of the instances (and
+ # discard the device info).
+ dict_copy = copy.copy(component.__dict__)
+ del dict_copy['device']
+ if dict_copy['parent']:
+ dict_copy['parent'] = dict_copy['parent']['uniqueId']
+ dict_copy['children'] = []
+ for child in component.__dict__['children']:
+ dict_copy['children'].append(child['uniqueId'])
+ view_info['hierarchy'][component['uniqueId']] = dict_copy
+
+ with open(dump_file, 'w') as out_file:
+ json.dump(view_info, out_file, indent=2)
+
+ screen_name = (activity + '-' + frag_list[0] + '-' + str(file_num) + '.png')
+ screen_path = os.path.join(directory, screen_name)
subprocess.call([ADB_PATH, 'shell', 'screencap', '/sdcard/' + screen_name])
subprocess.call([ADB_PATH, 'pull', '/sdcard/' + screen_name, screen_path])
# Return the filename & num so that the screenshot can be accessed
# programatically.
- return [screen_path, screenshot_num]
+ return [screen_path, file_num]
-def find_view_idx(vc_dump, activity, frag_list):
+def find_view_idx(vc_dump, activity, frag_list, view_array):
"""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,
- frag_list, vc_dump):
+ if view_array[i].is_duplicate(activity, frag_list, vc_dump):
return i
return -1
-def create_view(package_name, vc_dump, activity, frag_list, debug):
+def create_view(package_name, vc_dump, activity, frag_list):
"""Store the current view in the View data structure."""
- screenshot_info = save_screenshot(package_name, activity, frag_list)
- v = View(activity, frag_list, vc_dump,screenshot_info[0], screenshot_info[1])
+ 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.
- # 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() >= 0 and component.getY() <= MAX_WIDTH and
- component['layout:layout_width'] > 0 and
+ component.getX() >= 0 and component.getX() <= MAX_WIDTH and
+ int(component['layout:getWidth()']) > 0 and
component.getY() >= (NAVBAR_HEIGHT) and
component.getY() <= MAX_HEIGHT and
- component['layout:layout_height'] > 0 and
- ('Button' in component['class'] or
- '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']) 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))):
+ int(component['layout:getHeight()']) > 0):
print component['class'] + '-- will be clicked'
v.clickable.append(component)
@@ -152,6 +156,8 @@
def crawl_package(apk_dir, package_name, vc, device, debug):
"""Main crawler loop. Evaluate views, store new views, and click on items."""
+ view_root = []
+ view_array = []
if not debug:
# Install the app.
subprocess.call([ADB_PATH, 'install', '-r', apk_dir + package_name
@@ -167,37 +173,39 @@
if activity == EXITED_APP:
return
frag_list = get_frag_list(package_name)
- global view_root
- view_root = create_view(package_name, vc_dump, activity,
- frag_list, debug)
+ view_root = create_view(package_name, vc_dump, activity, frag_list)
view_array.append(view_root)
+ curr_view = view_root
while True:
+ # If last click opened the keyboard, assume we're in the same view and just
+ # click on the next element. Since opening the keyboard can leave traces of
+ # additional components, don't check if view is duplicate.
+ # TODO(afergan): Is this a safe assumption?
if device.isKeyboardShown():
perform_press_back()
-
- # Determine if this is a View that has already been seen.
- 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, frag_list, debug)
- view_array.append(curr_view)
+ # Determine if this is a View that has already been seen.
+ view_idx = find_view_idx(vc_dump, activity, frag_list, view_array)
+ 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, frag_list)
+ 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()))
+ c = curr_view.clickable[-1]
+ print ('Clickable: {} {}, ({},{})'.format(c['uniqueId'], c['class'],
+ c.getX(), 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]
+ del curr_view.clickable[-1]
else:
print '!!! Clicking back button'
@@ -210,4 +218,3 @@
if activity == EXITED_APP:
return
frag_list = get_frag_list(package_name)
-
diff --git a/main.py b/apkcrawler/main.py
similarity index 100%
rename from main.py
rename to apkcrawler/main.py
diff --git a/view.py b/apkcrawler/view.py
similarity index 95%
rename from view.py
rename to apkcrawler/view.py
index 6863b63..965e0e2 100644
--- a/view.py
+++ b/apkcrawler/view.py
@@ -30,8 +30,8 @@
def is_duplicate(self, cv_activity, cv_frag_list, cv_hierarchy):
"""Determine if the passed-in current view is identical to this View."""
- # Since the fragment names are hashable, this is the most efficient method to
- # compare two unordered lists according to
+ # 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