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: