| Index: experimental/telemetry_mini/telemetry_mini.py
|
| diff --git a/experimental/telemetry_mini/telemetry_mini.py b/experimental/telemetry_mini/telemetry_mini.py
|
| index 7d244aa3bdffbb61e3cabb893fcd483cb174f8c9..26bc195b851a397f4add5e0db126b8d546be27b0 100644
|
| --- a/experimental/telemetry_mini/telemetry_mini.py
|
| +++ b/experimental/telemetry_mini/telemetry_mini.py
|
| @@ -30,7 +30,9 @@ import websocket # pylint: disable=import-error
|
| from xml.etree import ElementTree as element_tree
|
|
|
|
|
| +KEYCODE_HOME = 3
|
| KEYCODE_BACK = 4
|
| +KEYCODE_APP_SWITCH = 187
|
|
|
| # Parse rectangle bounds given as: '[left,top][right,bottom]'.
|
| RE_BOUNDS = re.compile(
|
| @@ -159,16 +161,24 @@ class AdbMini(object):
|
| self.RunCommand('pull', UI_DUMP_TEMP, f.name)
|
| return element_tree.parse(f.name)
|
|
|
| - @RetryOn(LookupError)
|
| - def FindUiElement(self, attr_values):
|
| - """Find a UI element on screen capture, retrying if not yet visible."""
|
| + def HasUiElement(self, attr_values):
|
| + """Check whether a UI element is visible on the screen."""
|
| root = self.GetUiScreenDump()
|
| for node in root.iter():
|
| if all(node.get(k) == v for k, v in attr_values):
|
| return node
|
| - raise LookupError('Specified UI element not found')
|
| + return None
|
| +
|
| + @RetryOn(LookupError)
|
| + def FindUiElement(self, *args, **kwargs):
|
| + """Find a UI element on the screen, retrying if not yet visible."""
|
| + node = self.HasUiElement(*args, **kwargs)
|
| + if node is None:
|
| + raise LookupError('Specified UI element not found')
|
| + return node
|
|
|
| def TapUiElement(self, *args, **kwargs):
|
| + """Tap on a UI element found on screen."""
|
| node = self.FindUiElement(*args, **kwargs)
|
| m = RE_BOUNDS.match(node.get('bounds'))
|
| left, top, right, bottom = (int(v) for v in m.groups())
|
| @@ -198,6 +208,13 @@ def _UserAction(f):
|
|
|
|
|
| class AndroidActions(object):
|
| + APP_SWITCHER_CLEAR_ALL = [
|
| + ('resource-id', 'com.android.systemui:id/button'),
|
| + ('text', 'CLEAR ALL')]
|
| + APP_SWITCHER_NO_RECENT = [
|
| + ('package', 'com.android.systemui'),
|
| + ('text', 'No recent items')]
|
| +
|
| def __init__(self, device, user_action_delay=1):
|
| self.device = device
|
| self.user_action_delay = user_action_delay
|
| @@ -208,11 +225,21 @@ class AndroidActions(object):
|
| if duration:
|
| time.sleep(duration)
|
|
|
| + @_UserAction
|
| + def GoHome(self, **kwargs):
|
| + del kwargs
|
| + self.device.RunShellCommand('input', 'keyevent', str(KEYCODE_HOME))
|
| +
|
| @_UserAction
|
| def GoBack(self, **kwargs):
|
| del kwargs
|
| self.device.RunShellCommand('input', 'keyevent', str(KEYCODE_BACK))
|
|
|
| + @_UserAction
|
| + def GoAppSwitcher(self, **kwargs):
|
| + del kwargs
|
| + self.device.RunShellCommand('input', 'keyevent', str(KEYCODE_APP_SWITCH))
|
| +
|
| @_UserAction
|
| def StartActivity(
|
| self, data_uri, action='android.intent.action.VIEW', **kwargs):
|
| @@ -224,6 +251,22 @@ class AndroidActions(object):
|
| del kwargs
|
| self.device.TapUiElement(attr_values)
|
|
|
| + def TapHomeScreenShortcut(self, description, **kwargs):
|
| + self.TapUiElement([
|
| + ('package', 'com.android.launcher3'),
|
| + ('class', 'android.widget.TextView'),
|
| + ('content-desc', description)
|
| + ], **kwargs)
|
| +
|
| + def TapAppSwitcherTitle(self, text, **kwargs):
|
| + self.TapUiElement([
|
| + ('resource-id', 'com.android.systemui:id/title'),
|
| + ('text', text)
|
| + ], **kwargs)
|
| +
|
| + def TapAppSwitcherClearAll(self, **kwargs):
|
| + self.TapUiElement(self.APP_SWITCHER_CLEAR_ALL, **kwargs)
|
| +
|
| @_UserAction
|
| def SwipeUp(self, **kwargs):
|
| del kwargs
|
| @@ -233,6 +276,23 @@ class AndroidActions(object):
|
| self.device.RunShellCommand(
|
| 'input', 'swipe', '240', '568', '240', '284', '400')
|
|
|
| + @_UserAction
|
| + def SwipeDown(self, **kwargs):
|
| + del kwargs
|
| + # Hardcoded values for 480x854 screen size; should work reasonably on
|
| + # other screen sizes.
|
| + # Command args: swipe <x1> <y1> <x2> <y2> [duration(ms)]
|
| + self.device.RunShellCommand(
|
| + 'input', 'swipe', '240', '284', '240', '568', '400')
|
| +
|
| + def ClearRecentApps(self):
|
| + self.GoAppSwitcher()
|
| + if self.device.HasUiElement(self.APP_SWITCHER_NO_RECENT):
|
| + self.GoHome()
|
| + else:
|
| + self.SwipeDown()
|
| + self.TapAppSwitcherClearAll()
|
| +
|
|
|
| class DevToolsWebSocket(object):
|
| def __init__(self, url):
|
| @@ -391,7 +451,10 @@ class ChromiumApp(AndroidApp):
|
| To the extent possible, measurements from browsers launched within
|
| different sessions are meant to be independent of each other.
|
| """
|
| - self.RemoveProfile()
|
| + # Removing the profile breaks Chrome Shortcuts on the Home Screen.
|
| + # TODO: Figure out a way to automatically create the shortcuts before
|
| + # running the story.
|
| + # self.RemoveProfile()
|
| with self.CommandLineFlags(flags):
|
| with self.StartupTracing(trace_config):
|
| # Ensure browser is closed after setting command line flags and
|
| @@ -456,19 +519,9 @@ class UserStory(object):
|
| self.browser = browser
|
| self.actions = AndroidActions(self.device)
|
|
|
| - def GetExtraStoryApps(self):
|
| - """Sequence of AndroidApp's, other than the browser, used in the story."""
|
| - return ()
|
| -
|
| - def EnsureExtraStoryAppsClosed(self):
|
| - running_processes = self.device.ProcessStatus()
|
| - for app in self.GetExtraStoryApps():
|
| - if app.PACKAGE_NAME in running_processes:
|
| - app.ForceStop()
|
| -
|
| def Run(self, browser_flags, trace_config, trace_file):
|
| with self.browser.Session(browser_flags, trace_config):
|
| - self.EnsureExtraStoryAppsClosed()
|
| + self.RunPrepareSteps()
|
| try:
|
| self.RunStorySteps()
|
| self.browser.CollectTrace(trace_file)
|
| @@ -478,7 +531,11 @@ class UserStory(object):
|
| logging.error('Aborting story due to %s.', type(exc).__name__)
|
| raise
|
| finally:
|
| - self.EnsureExtraStoryAppsClosed()
|
| + self.RunCleanupSteps()
|
| +
|
| + def RunPrepareSteps(self):
|
| + """Subclasses may override to perform actions before running the story."""
|
| + pass
|
|
|
| def RunStorySteps(self):
|
| """Subclasses should override this method to implement the story.
|
| @@ -489,6 +546,14 @@ class UserStory(object):
|
| """
|
| raise NotImplementedError
|
|
|
| + def RunCleanupSteps(self):
|
| + """Subclasses may override to perform actions after running the story.
|
| +
|
| + Note: This will be called even if an exception was raised during the
|
| + execution of RunStorySteps (but not for errors in RunPrepareSteps).
|
| + """
|
| + pass
|
| +
|
|
|
| def ReadProcessMetrics(trace_file):
|
| """Return a list of {"name": process_name, metric: value} dicts."""
|
|
|