capsule: Add config file and type text in fields.
Introduce a Config class that reads user info from a config.ini file and
uses the field names to help populate text fields.
During our crawl, whenever we see a text field, try to assess what the
field is looking for by its resource id. If we find a match with a key
in the config file, type the value of that field. Else, type the default
text.
Change-Id: Ifb6821eddd2c085aea1066183a11158ea63094bb
diff --git a/capsule/README.md b/capsule/README.md
index 324d7e3..7563c78 100644
--- a/capsule/README.md
+++ b/capsule/README.md
@@ -97,6 +97,25 @@
buttons, so installing/logging into Facebook and logging into a Google account
allows the program to get beyond login screens.
+### Config and Typing in Text Fields
+
+Capsule can automatically fill in text fields according to their Android
+resource ids. Prepopulated fields with logical rules (such as differentiating
+between first and last name and using the zipcode config for views named
+'id/zip' are already located in the [config file](config.ini), but it will also
+check for any fields that the user adds to the config in the extra info section.
+
+If it does not match any of the fields in the config, it will type the text in
+the default field, so delete that field if you do not want text entry.
+
+We recommend personalizing the config, especially since the default email
+address (foo@bar.com) has already been used (or blocked) to create accounts for
+most applications.
+
+To modify the file without Git tracking it, type
+
+``$ git update-index --assume-unchanged <file>``
+
## Contributors
We are happy to accept contributions. However, Vanadium does not accept pull
diff --git a/capsule/capsule.py b/capsule/capsule.py
index ceea28f..b163a6d 100644
--- a/capsule/capsule.py
+++ b/capsule/capsule.py
@@ -97,6 +97,14 @@
print HELP_MSG
sys.exit()
+ # Lock phone orientation to portrait.
+ # Turn off automatic rotation.
+ device.shell('content insert --uri content://settings/system --bind '
+ 'name:s:accelerometer_rotation --bind value:i:0')
+ # Rotate to portrait mode.
+ device.shell('content insert --uri content://settings/system --bind '
+ 'name:s:user_rotation --bind value:i:0')
+
# User only specified emulator name or nothing at all.
if len(sys.argv) <= 2:
print 'No command line arguments, crawling currently launched app.'
diff --git a/capsule/config.ini b/capsule/config.ini
new file mode 100644
index 0000000..6d81280
--- /dev/null
+++ b/capsule/config.ini
@@ -0,0 +1,10 @@
+[basic info]
+first_name = Jane
+last_name = Smith
+email = foo@bar.com
+password = DefaultPw1!
+zipcode = 94109
+default = Hello
+phone_num = 8885551212
+
+[extra]
diff --git a/capsule/config.py b/capsule/config.py
new file mode 100644
index 0000000..057df90
--- /dev/null
+++ b/capsule/config.py
@@ -0,0 +1,27 @@
+# Copyright 2016 The Vanadium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+"""Config class definition."""
+
+from ConfigParser import SafeConfigParser
+
+CONFIG_FILE = 'config.ini'
+
+
+class Config(object):
+ """Class that contains all config info for the crawler.
+
+ Includes information for text fields that should be populated.
+ """
+
+ def __init__(self):
+ """Constructor for Config class."""
+
+ # Loads info from CONFIG_FILE into self.data
+ self.data = {}
+ parser = SafeConfigParser()
+ parser.read(CONFIG_FILE)
+ for section_name in parser.sections():
+ for name, value in parser.items(section_name):
+ self.data[name] = value
diff --git a/capsule/crawlpkg.py b/capsule/crawlpkg.py
index 3f9e336..1fde7f0 100644
--- a/capsule/crawlpkg.py
+++ b/capsule/crawlpkg.py
@@ -12,6 +12,8 @@
import time
from com.dtmilano.android.common import obtainAdbPath
+
+from config import Config
from layout import Layout
# https://material.google.com/layout/structure.html#structure-system-bars
@@ -50,8 +52,8 @@
MAX_FB_AUTH_TAPS = 5
MAX_FB_BUG_RESETS = 5
-NEGATIVE_WORDS = ['no', 'cancel', 'back', 'negative', 'neg' 'deny', 'previous',
- 'prev', 'exit', 'delete', 'end', 'remove', 'clear']
+NEGATIVE_WORDS = ['no', 'cancel', 'back', 'neg' 'deny', 'prev', 'exit',
+ 'delete', 'end', 'remove', 'clear', 'reset', 'undo']
def extract_between(text, sub1, sub2, nth=1):
@@ -120,15 +122,88 @@
try:
(x, y) = view.getXY()
device.touch(x, y)
- print('Clicked top left {} {}, ({},{})'.format(view.getUniqueId(),
- view.getClass(), view.getX(),
- view.getY()))
+ print('Clicked {} {}, ({},{})'.format(view.getUniqueId(),
+ view.getClass(), view.getX(),
+ view.getY()))
except UnicodeEncodeError:
print '***Unicode coordinates'
except TypeError:
print '***String coordinates'
+def use_keyboard(prev_clicked, config_data, device, vc):
+ """Type text when the keyboard is visible."""
+
+ print 'Prev clicked: ' + prev_clicked
+ view = vc.findViewById(prev_clicked)
+
+ if not view:
+ # Sometimes when we get to a new Layout, an EditText is already selected.
+ # This means that prev_clicked will refer to a view from a previous Layout.
+ # The currently selected view will be clicked again during our crawl, so do
+ # not enter text now.
+
+ # TODO(afergan): This is just to check which view is selected, but since the
+ # dump takes time, remove this later.
+ vc_dump = perform_vc_dump(vc)
+ for v in vc_dump:
+ if v.isFocused():
+ print 'Focused: ' + v.getUniqueId()
+ break
+
+ perform_press_back(device)
+ return
+
+ # TODO(afergan): The dump does not include information about hints for the
+ # TextView, which can be very useful in the absence of a descriptive view id.
+ # See if there is a way to access this, or add this to our custom build of
+ # AOSP.
+ # https://developer.android.com/reference/android/widget/TextView.html#attr_android:hint)
+
+ # If there is already text in the field, do not add additional text.
+ if view and view.getText():
+ print 'This text field is already populated.'
+ perform_press_back(device)
+ return
+
+ # Check if the id contains any of the words in the [basic info] section of the
+ # config. If any of these fields have been removed, do not type anything.
+ if 'name' in prev_clicked:
+ if any(x in prev_clicked for x in['last', 'sur']):
+ print 'Typing last name ' + config_data.get('last_name', '')
+ device.type(config_data.get('last_name', ''))
+ else:
+ print 'Typing first name ' + config_data.get('first_name', '')
+ device.type(config_data.get('first_name', ''))
+ elif any(x in prev_clicked for x in['email', 'mail', 'address']):
+ print 'Typing email address ' + config_data.get('email', '')
+ device.type(config_data.get('email', ''))
+ elif any(x in prev_clicked for x in['password', 'pw']):
+ print 'Typing password ' + config_data.get('password', '')
+ device.type(config_data.get('password', ''))
+ elif any(x in prev_clicked for x in['zip']):
+ print 'Typing zip code ' + config_data.get('zipcode', '')
+ device.type(config_data.get('zipcode', ''))
+ elif any(x in prev_clicked for x in['phone']):
+ print 'Typing phone number ' + config_data.get('phone_num', '')
+ device.type(config_data.get('phone_num', ''))
+ else:
+ # If the user has added additional fields in the config, check for those.
+ for c in config_data:
+ if any(x in prev_clicked for x in c):
+ device.type(config_data.get(c, ''))
+ break
+ else:
+ print 'Typing default text ' + config_data.get('default', '')
+ device.type(config_data.get('default', ''))
+
+ # TODO(afergan): The enter key can sometimes advance us to the next field or
+ # Layout, but we would have to track that here. For now, just minimize the
+ # keyboard and let the crawler advance us.
+ perform_press_back(device)
+ return
+
+
def fb_login(package_name, device, curr_layout, click, vc):
"""Log into Facebook by automating the authentication flow."""
@@ -306,9 +381,9 @@
# If the screen is off, turn it on.
if (device.shell("dumpsys power | grep 'Display Power: state=' | grep -oE "
"'(ON|OFF)'") == 'OFF'):
- device.press('26') # KEYCODE_POWER
+ device.press('KEYCODE_POWER')
# Unlock device.
- device.press('82') # KEYCODE_MENU
+ device.press('KEYCODE_MENU')
activity_str = device.shell("dumpsys window windows "
"| grep -E 'mCurrentFocus'")
return activity_str
@@ -695,7 +770,7 @@
def crawl_until_exit(vc, device, package_name, layout_map, layout_graph,
- still_exploring, start_layout, logged_in):
+ still_exploring, start_layout, logged_in, config_data):
"""Main crawler loop. Evaluates layouts, stores new data, and clicks views."""
print 'Logged in: ' + str(logged_in)
@@ -706,10 +781,6 @@
while (len(layout_map) < MAX_LAYOUTS and
consec_back_presses < MAX_CONSEC_BACK_PRESSES):
- # If last click opened the keyboard, assume we're in the same layout and
- # just click on the next element. Since opening the keyboard can leave
- # traces of additional views, don't check if layout is duplicate.
- # TODO(afergan): Is this a safe assumption?
if device.isKeyboardShown():
perform_press_back(device)
@@ -739,10 +810,8 @@
else:
layout_graph[prev_name] = {curr_layout.get_name()}
print 'Adding to set: ' + prev_name + ' ' + curr_layout.get_name()
+ print 'Num of nodes in layout graph: ' + str(len(layout_graph))
- print len(layout_graph)
- for key, val in layout_graph.iteritems():
- print key + ' ' + str(val)
print 'Layout depth: ' + str(curr_layout.depth)
print 'Num clickable: ' + str(len(curr_layout.clickable))
@@ -753,7 +822,6 @@
clickid = click.getUniqueId().lower()
if click.getText():
clicktext = click.getText().lower()
- print 'Click text: ' + clicktext
else:
clicktext = ''
if (click.getClass() == 'com.facebook.widget.LoginButton'
@@ -782,6 +850,8 @@
consec_back_presses = 0
prev_clicked = c.getUniqueId()
curr_layout.clickable.remove(c)
+ if device.isKeyboardShown():
+ use_keyboard(prev_clicked, config_data, device, vc)
else:
print 'Removing ' + curr_layout.get_name() + ' from still_exploring.'
@@ -846,6 +916,8 @@
still_exploring = {}
layout_graph = {}
+ config_data = Config().data
+
# Stores if we have logged in during this crawl/session. If the app has
# previously logged into an app or service (and can skip the authorization
# process), we will be unable to detect that.
@@ -875,7 +947,7 @@
logged_in = crawl_until_exit(vc, device, package_name, layout_map,
layout_graph, still_exploring, first_layout,
- logged_in)
+ logged_in, config_data)
# Recrawl Layouts that aren't completely explored.
while (still_exploring and num_crawls < MAX_CRAWLS and
@@ -938,7 +1010,7 @@
print 'Crawling again'
logged_in = crawl_until_exit(vc, device, package_name, layout_map,
layout_graph, still_exploring,
- curr_layout, logged_in)
+ curr_layout, logged_in, config_data)
print ('Done with the crawl. Still ' + str(len(l.clickable)) +
' views to click for this Layout.')
else: