1# Copyright 2013 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 logging
6import subprocess
7
8from telemetry.core import exceptions
9from telemetry.internal.platform import android_platform_backend as \
10  android_platform_backend_module
11from telemetry.core import util
12from telemetry.internal.backends import android_command_line_backend
13from telemetry.internal.backends import browser_backend
14from telemetry.internal.backends.chrome import chrome_browser_backend
15from telemetry.internal.browser import user_agent
16
17from devil.android.sdk import intent
18
19
20class AndroidBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
21  """The backend for controlling a browser instance running on Android."""
22  def __init__(self, android_platform_backend, browser_options,
23               backend_settings, output_profile_path, extensions_to_load):
24    assert isinstance(android_platform_backend,
25                      android_platform_backend_module.AndroidPlatformBackend)
26    super(AndroidBrowserBackend, self).__init__(
27        android_platform_backend,
28        supports_tab_control=backend_settings.supports_tab_control,
29        supports_extensions=False, browser_options=browser_options,
30        output_profile_path=output_profile_path,
31        extensions_to_load=extensions_to_load)
32
33    self._port_keeper = util.PortKeeper()
34    # Use the port hold by _port_keeper by default.
35    self._port = self._port_keeper.port
36
37
38    if len(extensions_to_load) > 0:
39      raise browser_backend.ExtensionsNotSupportedException(
40          'Android browser does not support extensions.')
41
42    # Initialize fields so that an explosion during init doesn't break in Close.
43    self._backend_settings = backend_settings
44    self._saved_sslflag = ''
45
46    # Kill old browser.
47    self._KillBrowser()
48
49    if self.device.HasRoot() or self.device.NeedsSU():
50      if self.browser_options.profile_dir:
51        self.platform_backend.PushProfile(
52            self._backend_settings.package,
53            self.browser_options.profile_dir)
54      elif not self.browser_options.dont_override_profile:
55        self.platform_backend.RemoveProfile(
56            self._backend_settings.package,
57            self._backend_settings.profile_ignore_list)
58
59    # Set the debug app if needed.
60    self.platform_backend.SetDebugApp(self._backend_settings.package)
61
62  @property
63  def log_file_path(self):
64    return None
65
66  @property
67  def device(self):
68    return self.platform_backend.device
69
70  def _KillBrowser(self):
71    if self.device.IsUserBuild():
72      self.platform_backend.StopApplication(self._backend_settings.package)
73    else:
74      self.platform_backend.KillApplication(self._backend_settings.package)
75
76  def Start(self):
77    self.device.RunShellCommand('logcat -c')
78    if self.browser_options.startup_url:
79      url = self.browser_options.startup_url
80    elif self.browser_options.profile_dir:
81      url = None
82    else:
83      # If we have no existing tabs start with a blank page since default
84      # startup with the NTP can lead to race conditions with Telemetry
85      url = 'about:blank'
86
87    self.platform_backend.DismissCrashDialogIfNeeded()
88
89    user_agent_dict = user_agent.GetChromeUserAgentDictFromType(
90        self.browser_options.browser_user_agent_type)
91
92    browser_startup_args = self.GetBrowserStartupArgs()
93    with android_command_line_backend.SetUpCommandLineFlags(
94        self.device, self._backend_settings, browser_startup_args):
95      self.device.StartActivity(
96          intent.Intent(package=self._backend_settings.package,
97                        activity=self._backend_settings.activity,
98                        action=None, data=url, category=None,
99                        extras=user_agent_dict),
100          blocking=True)
101
102      # TODO(crbug.com/404771): Move port forwarding to network_controller.
103      remote_devtools_port = self._backend_settings.GetDevtoolsRemotePort(
104          self.device)
105      try:
106        # Release reserved port right before forwarding host to device.
107        self._port_keeper.Release()
108        assert self._port == self._port_keeper.port, (
109          'Android browser backend must use reserved port by _port_keeper')
110        self.platform_backend.ForwardHostToDevice(
111            self._port, remote_devtools_port)
112      except Exception:
113        logging.exception('Failed to forward %s to %s.',
114            str(self._port), str(remote_devtools_port))
115        logging.warning('Currently forwarding:')
116        try:
117          for line in self.device.adb.ForwardList().splitlines():
118            logging.warning('  %s', line)
119        except Exception:
120          logging.warning('Exception raised while listing forwarded '
121                          'connections.')
122
123        logging.warning('Host tcp ports in use:')
124        try:
125          for line in subprocess.check_output(['netstat', '-t']).splitlines():
126            logging.warning('  %s', line)
127        except Exception:
128          logging.warning('Exception raised while listing tcp ports.')
129
130        logging.warning('Device unix domain sockets in use:')
131        try:
132          for line in self.device.ReadFile('/proc/net/unix', as_root=True,
133                                           force_pull=True).splitlines():
134            logging.warning('  %s', line)
135        except Exception:
136          logging.warning('Exception raised while listing unix domain sockets.')
137
138        raise
139
140      try:
141        self._WaitForBrowserToComeUp()
142        self._InitDevtoolsClientBackend(remote_devtools_port)
143      except exceptions.BrowserGoneException:
144        logging.critical('Failed to connect to browser.')
145        if not (self.device.HasRoot() or self.device.NeedsSU()):
146          logging.critical(
147            'Resolve this by either: '
148            '(1) Flashing to a userdebug build OR '
149            '(2) Manually enabling web debugging in Chrome at '
150            'Settings > Developer tools > Enable USB Web debugging.')
151        self.Close()
152        raise
153      except:
154        self.Close()
155        raise
156
157  def GetBrowserStartupArgs(self):
158    args = super(AndroidBrowserBackend, self).GetBrowserStartupArgs()
159    args.append('--enable-remote-debugging')
160    args.append('--disable-fre')
161    args.append('--disable-external-intent-requests')
162    return args
163
164  @property
165  def pid(self):
166    pids = self.device.GetPids(self._backend_settings.package)
167    if not pids or self._backend_settings.package not in pids:
168      raise exceptions.BrowserGoneException(self.browser)
169    if len(pids[self._backend_settings.package]) > 1:
170      raise Exception(
171          'At most one instance of process %s expected but found pids: '
172          '%s' % (self._backend_settings.package, pids))
173    return int(pids[self._backend_settings.package][0])
174
175  @property
176  def browser_directory(self):
177    return None
178
179  @property
180  def profile_directory(self):
181    return self._backend_settings.profile_dir
182
183  @property
184  def package(self):
185    return self._backend_settings.package
186
187  @property
188  def activity(self):
189    return self._backend_settings.activity
190
191  def __del__(self):
192    self.Close()
193
194  def Close(self):
195    super(AndroidBrowserBackend, self).Close()
196
197    self._KillBrowser()
198
199    self.platform_backend.StopForwardingHost(self._port)
200
201    if self._output_profile_path:
202      self.platform_backend.PullProfile(
203          self._backend_settings.package, self._output_profile_path)
204
205  def IsBrowserRunning(self):
206    return self.platform_backend.IsAppRunning(self._backend_settings.package)
207
208  def GetStandardOutput(self):
209    return self.platform_backend.GetStandardOutput()
210
211  def GetStackTrace(self):
212    return self.platform_backend.GetStackTrace()
213