1#!/usr/bin/env python
2
3# Copyright 2015 Google Inc.
4#
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
8
9from __future__ import with_statement
10
11# Imports the monkeyrunner modules used by this program
12from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice
13
14import ast
15import os
16import subprocess
17import time
18
19
20# Time to wait between performing UI actions and capturing the SKP.
21WAIT_FOR_SKP_CAPTURE = 1
22
23
24class DragAction:
25  """Action describing a touch drag."""
26  def __init__(self, start, end, duration, points):
27    self.start = start
28    self.end = end
29    self.duration = duration
30    self.points = points
31
32  def run(self, device):
33    """Perform the action."""
34    return device.drag(self.start, self.end, self.duration, self.points)
35
36
37class PressAction:
38  """Action describing a button press."""
39  def __init__(self, button, press_type):
40    self.button = button
41    self.press_type = press_type
42
43  def run(self, device):
44    """Perform the action."""
45    return device.press(self.button, self.press_type)
46
47
48def parse_action(action_dict):
49  """Parse a dict describing an action and return an Action object."""
50  if action_dict['type'] == 'drag':
51    return DragAction(tuple(action_dict['start']),
52                      tuple(action_dict['end']),
53                      action_dict['duration'],
54                      action_dict['points'])
55  elif action_dict['type'] == 'press':
56    return PressAction(action_dict['button'], action_dict['press_type'])
57  else:
58    raise TypeError('Unsupported action type: %s' % action_dict['type'])
59
60
61class App:
62  """Class which describes an app to launch and actions to run."""
63  def __init__(self, name, package, activity, app_launch_delay, actions):
64    self.name = name
65    self.package = package
66    self.activity = activity
67    self.app_launch_delay = app_launch_delay
68    self.run_component = '%s/%s' % (self.package, self.activity)
69    self.actions = [parse_action(a) for a in actions]
70
71  def launch(self, device):
72    """Launch the app on the device."""
73    device.startActivity(component=self.run_component)
74    time.sleep(self.app_launch_delay)
75
76  def kill(self):
77    """Kill the app."""
78    adb_shell('am force-stop %s' % self.package)
79
80
81def check_output(cmd):
82  """Convenience implementation of subprocess.check_output."""
83  proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
84  if proc.wait() != 0:
85    raise Exception('Command failed: %s' % ' '.join(cmd))
86  return proc.communicate()[0]
87
88
89def adb_shell(cmd):
90  """Run the given ADB shell command and emulate the exit code."""
91  output = check_output(['adb', 'shell', cmd + '; echo $?']).strip()
92  lines = output.splitlines()
93  if lines[-1] != '0':
94    raise Exception('ADB command failed: %s\n\nOutput:\n%s' % (cmd, output))
95  return '\n'.join(lines[:-1])
96
97
98def remote_file_exists(filename):
99  """Return True if the given file exists on the device and False otherwise."""
100  try:
101    adb_shell('test -f %s' % filename)
102    return True
103  except Exception:
104    return False
105
106
107def capture_skp(skp_file, package, device):
108  """Capture an SKP."""
109  remote_path = '/data/data/%s/cache/%s' % (package, os.path.basename(skp_file))
110  try:
111    adb_shell('rm %s' % remote_path)
112  except Exception:
113    if remote_file_exists(remote_path):
114      raise
115
116  adb_shell('setprop debug.hwui.capture_frame_as_skp %s' % remote_path)
117  try:
118    # Spin, wait for the SKP to be written.
119    timeout = 10  # Seconds
120    start = time.time()
121    device.drag((300, 300), (300, 350), 1, 10)  # Dummy action to force a draw.
122    while not remote_file_exists(remote_path):
123      if time.time() - start > timeout:
124        raise Exception('Timed out waiting for SKP capture.')
125      time.sleep(1)
126
127    # Pull the SKP from the device.
128    cmd = ['adb', 'pull', remote_path, skp_file]
129    check_output(cmd)
130
131  finally:
132    adb_shell('setprop debug.hwui.capture_frame_as_skp ""')
133
134
135def load_app(filename):
136  """Load the JSON file describing an app and return an App instance."""
137  with open(filename) as f:
138    app_dict = ast.literal_eval(f.read())
139  return App(app_dict['name'],
140             app_dict['package'],
141             app_dict['activity'],
142             app_dict['app_launch_delay'],
143             app_dict['actions'])
144
145
146def main():
147  """Capture SKPs for all apps."""
148  device = MonkeyRunner.waitForConnection()
149
150  # TODO(borenet): Kill all apps.
151  device.wake()
152  device.drag((600, 600), (10, 10), 0.2, 10)
153
154  apps_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'apps')
155  app_files = [os.path.join(apps_dir, app) for app in os.listdir(apps_dir)]
156
157  for app_file in app_files:
158    app = load_app(app_file)
159    print app.name
160    print '  Package %s' % app.package
161    app.launch(device)
162    print '  Launched activity %s' % app.activity
163
164    for action in app.actions:
165      print '  %s' % action.__class__.__name__
166      action.run(device)
167
168    time.sleep(WAIT_FOR_SKP_CAPTURE)
169    print '  Capturing SKP.'
170    skp_file = '%s.skp' % app.name
171    capture_skp(skp_file, app.package, device)
172    print '  Wrote SKP to %s' % skp_file
173    print
174    app.kill()
175
176
177if __name__ == '__main__':
178  main()
179