blob: add0233517b104427513934b582d692b2d15ef77 [file] [log] [blame]
"""A module for installing and crawling the UI of Android application."""
import os
import re
import subprocess
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'
MAX_WIDTH = 1080
# 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
# Visibility
VISIBLE = 0x0
INVISIBLE = 0x4
GONE = 0x8
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, _ = 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(':', '')
# 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_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:
return 'NoFrag'
return re.search('Local FragmentActivity (.*?) State:', fragment_str).group(1)
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
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
screen_path = 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]
def find_view_idx(vc_dump, activity, fragment):
for i in range(len(view_array)):
if view_array[i].is_duplicate(activity,
fragment, vc_dump):
return i
return -1
def create_view(package_name, vc_dump, activity, fragment):
"""Store the current view in the View data structure."""
v = View(activity, fragment)
v.hierarchy = vc_dump
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.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'])):
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
def crawl_package(apk_dir, package_name, vc, device, 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.
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()
fragment = get_fragment_name(package_name)
global view_root
view_root = create_view(package_name, vc_dump, activity,
fragment)
view_array.append(view_root)
while True:
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, fragment)
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')
activity = get_activity_name()
fragment = get_fragment_name(package_name)