blob: b5ff21cdc9f16103095b2ea72ac6c6a199113543 [file] [log] [blame]
"""A module for installing and crawling the UI of Android application."""
import copy
import json
import os
import re
import subprocess
from view import View
ADB_PATH = None
# 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 = 2560
NAVBAR_HEIGHT = 84
# Visibility
VISIBLE = 0x0
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'
def set_adb_path():
"""Define the ADB path based on operating system."""
try:
global ADB_PATH
# For machines with multiple installations of adb, use the last listed
# version of adb. If this doesn't work for your setup, modify to taste.
ADB_PATH = subprocess.check_output(['which -a adb'], shell=True).split(
'\n')[-2]
except subprocess.CalledProcessError:
print 'Could not find adb. Please check your PATH.'
def perform_press_back():
subprocess.call([ADB_PATH, 'shell', 'input', 'keyevent', '4'])
def get_activity_name(package_name):
"""Gets the current running activity of the package."""
# TODO(afergan): See if we can consolidate this with get_fragment_list, but
# still make sure that the current app has focus.
# TODO(afergan): Check for Windows compatibility.
proc = subprocess.Popen([ADB_PATH, 'shell', 'dumpsys window windows '
'| 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(':', '')
# 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}
# We only want the text between the final period and the closing bracket.
return activity_str.split('.')[-1].split('}')[0]
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'
frag_list = re.findall(': (.*?){', frag_dump[0], re.DOTALL)
print frag_list
return frag_list
def get_package_name():
"""Get the package name of the current focused window."""
proc = subprocess.Popen([ADB_PATH, 'shell', 'dumpsys window windows '
'| grep -E \'mCurrentFocus\''],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
activity_str, _ = proc.communicate()
# The current focus returns a string in the format
# mCurrentFocus=Window{35f66c3 u0 com.google.zagat/com.google.android.apps.
# zagat.activities.BrowseListsActivity}
# We want the text before the /
pkg_name = activity_str.split('/')[0].split(' ')[-1]
print 'Package name is ' + pkg_name
return pkg_name
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)
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, file_num]
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):
return i
return -1
def create_view(package_name, vc_dump, activity, frag_list):
"""Store the current view in the View data structure."""
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
int(component['layout:getWidth()']) > 0 and
component.getY() >= (NAVBAR_HEIGHT) and
component.getY() <= MAX_HEIGHT and
int(component['layout:getHeight()']) > 0):
print component['class'] + '-- will be clicked'
v.clickable.append(component)
return v
def crawl_package(apk_dir, vc, device, debug, package_name=None):
"""Main crawler loop. Evaluate views, store new views, and click on items."""
set_adb_path()
view_root = []
view_array = []
if debug or not package_name: # These should be equal
package_name = get_package_name()
else:
# 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 = get_activity_name(package_name)
if activity == EXITED_APP:
return
frag_list = get_frag_list(package_name)
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()
else:
# 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[-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[-1]
else:
print '!!! Clicking back button'
perform_press_back()
if curr_view == view_root:
return
vc_dump = vc.dump(window='-1')
activity = get_activity_name(package_name)
if activity == EXITED_APP:
return
frag_list = get_frag_list(package_name)