1# Copyright 2012 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import re
6
7from telemetry.internal.app import android_process
8from telemetry.internal.backends import android_browser_backend_settings
9from telemetry.internal.backends import app_backend
10
11from devil.android import app_ui
12from devil.android import flag_changer
13from devil.android.sdk import intent
14
15import py_utils
16
17
18class AndroidAppBackend(app_backend.AppBackend):
19
20  def __init__(self, android_platform_backend, start_intent,
21               is_app_ready_predicate=None, app_has_webviews=True):
22    super(AndroidAppBackend, self).__init__(
23        start_intent.package, android_platform_backend)
24    self._default_process_name = start_intent.package
25    self._start_intent = start_intent
26    self._is_app_ready_predicate = is_app_ready_predicate
27    self._is_running = False
28    self._app_has_webviews = app_has_webviews
29    self._existing_processes_by_pid = {}
30    self._app_ui = None
31
32  @property
33  def device(self):
34    return self.platform_backend.device
35
36  def GetAppUi(self):
37    if self._app_ui is None:
38      self._app_ui = app_ui.AppUi(self.device, self._start_intent.package)
39    return self._app_ui
40
41  def _LaunchAndWaitForApplication(self):
42    """Launch the app and wait for it to be ready."""
43    def is_app_ready():
44      return self._is_app_ready_predicate(self.app)
45
46    # When "is_app_ready_predicate" is provided, we use it to wait for the
47    # app to become ready, otherwise "blocking=True" is used as a fall back.
48    # TODO(slamm): check if the wait for "ps" check is really needed, or
49    # whether the "blocking=True" fall back is sufficient.
50    has_ready_predicate = self._is_app_ready_predicate is not None
51    self.device.StartActivity(
52        self._start_intent,
53        blocking=not has_ready_predicate,
54        force_stop=True,  # Ensure app was not running.
55    )
56    if has_ready_predicate:
57      py_utils.WaitFor(is_app_ready, timeout=60)
58
59  def Start(self):
60    """Start an Android app and wait for it to finish launching.
61
62    If the app has webviews, the app is launched with the suitable
63    command line arguments.
64
65    AppStory derivations can customize the wait-for-ready-state to wait
66    for a more specific event if needed.
67    """
68    if self._app_has_webviews:
69      webview_startup_args = self.GetWebviewStartupArgs()
70      command_line_name = (
71          android_browser_backend_settings.WebviewBackendSettings(
72              'android-webview')).command_line_name
73      with flag_changer.CustomCommandLineFlags(
74          self.device, command_line_name, webview_startup_args):
75        self._LaunchAndWaitForApplication()
76    else:
77      self._LaunchAndWaitForApplication()
78    self._is_running = True
79
80  def Foreground(self):
81    self.device.StartActivity(
82        intent.Intent(package=self._start_intent.package,
83                      activity=self._start_intent.activity,
84                      action=None,
85                      flags=[intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED]),
86        blocking=True)
87
88  def Background(self):
89    package = 'org.chromium.push_apps_to_background'
90    activity = package + '.PushAppsToBackgroundActivity'
91    self.device.StartActivity(
92        intent.Intent(
93            package=package,
94            activity=activity,
95            action=None,
96            flags=[intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED]),
97        blocking=True)
98
99  def Close(self):
100    self._is_running = False
101    self.platform_backend.KillApplication(self._start_intent.package)
102
103  def IsAppRunning(self):
104    return self._is_running
105
106  def GetStandardOutput(self):
107    raise NotImplementedError
108
109  def GetStackTrace(self):
110    raise NotImplementedError
111
112  def GetProcesses(self, process_filter=None):
113    if process_filter is None:
114      # Match process names of the form: 'process_name[:subprocess]'.
115      process_filter = re.compile(
116          '^%s(:|$)' % re.escape(self._default_process_name)).match
117
118    processes = set()
119    ps_output = self.platform_backend.GetPsOutput(['pid', 'name'])
120    for pid, name in ps_output:
121      if not process_filter(name):
122        continue
123
124      if pid not in self._existing_processes_by_pid:
125        self._existing_processes_by_pid[pid] = android_process.AndroidProcess(
126            self, pid, name)
127      processes.add(self._existing_processes_by_pid[pid])
128    return processes
129
130  def GetProcess(self, subprocess_name):
131    assert subprocess_name.startswith(':')
132    process_name = self._default_process_name + subprocess_name
133    return self.GetProcesses(lambda n: n == process_name).pop()
134
135  def GetWebViews(self):
136    assert self._app_has_webviews
137    webviews = set()
138    for process in self.GetProcesses():
139      webviews.update(process.GetWebViews())
140    return webviews
141
142  def GetWebviewStartupArgs(self):
143    assert self._app_has_webviews
144    args = []
145
146    # Turn on GPU benchmarking extension for all runs. The only side effect of
147    # the extension being on is that render stats are tracked. This is believed
148    # to be effectively free. And, by doing so here, it avoids us having to
149    # programmatically inspect a pageset's actions in order to determine if it
150    # might eventually scroll.
151    args.append('--enable-gpu-benchmarking')
152
153    return args
154