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.
11 from telemetry.core import exceptions
12 from telemetry.core import util
13 from telemetry.core.backends import adb_commands
14 from telemetry.core.backends import android_rndis
15 from telemetry.core.backends import browser_backend
16 from telemetry.core.backends.chrome import chrome_browser_backend
19 class AndroidBrowserBackendSettings(object):
20 def __init__(self, adb, activity, cmdline_file, package, pseudo_exec_name,
21 supports_tab_control):
23 self.activity = activity
24 self.cmdline_file = cmdline_file
25 self.package = package
26 self.pseudo_exec_name = pseudo_exec_name
27 self.supports_tab_control = supports_tab_control
29 def GetDevtoolsRemotePort(self):
30 raise NotImplementedError()
32 def RemoveProfile(self):
33 self.adb.RunShellCommand(
34 'su -c rm -r "%s"' % self.profile_dir)
36 def PushProfile(self, _):
37 logging.critical('Profiles cannot be overriden with current configuration')
41 def is_content_shell(self):
45 def profile_dir(self):
46 raise NotImplementedError()
49 class ChromeBackendSettings(AndroidBrowserBackendSettings):
50 # Stores a default Preferences file, re-used to speed up "--page-repeat".
51 _default_preferences_file = None
53 def __init__(self, adb, package):
54 super(ChromeBackendSettings, self).__init__(
56 activity='com.google.android.apps.chrome.Main',
57 cmdline_file='/data/local/chrome-command-line',
59 pseudo_exec_name='chrome',
60 supports_tab_control=True)
62 def GetDevtoolsRemotePort(self):
63 return 'localabstract:chrome_devtools_remote'
65 def PushProfile(self, new_profile_dir):
66 self.adb.Push(new_profile_dir, self.profile_dir)
69 def profile_dir(self):
70 return '/data/data/%s/app_chrome/' % self.package
73 class ContentShellBackendSettings(AndroidBrowserBackendSettings):
74 def __init__(self, adb, package):
75 super(ContentShellBackendSettings, self).__init__(
77 activity='org.chromium.content_shell_apk.ContentShellActivity',
78 cmdline_file='/data/local/tmp/content-shell-command-line',
80 pseudo_exec_name='content_shell',
81 supports_tab_control=False)
83 def GetDevtoolsRemotePort(self):
84 return 'localabstract:content_shell_devtools_remote'
87 def is_content_shell(self):
91 def profile_dir(self):
92 return '/data/data/%s/app_content_shell/' % self.package
95 class ChromiumTestShellBackendSettings(AndroidBrowserBackendSettings):
96 def __init__(self, adb, package):
97 super(ChromiumTestShellBackendSettings, self).__init__(
99 activity='org.chromium.chrome.testshell.ChromiumTestShellActivity',
100 cmdline_file='/data/local/tmp/chromium-testshell-command-line',
102 pseudo_exec_name='chromium_testshell',
103 supports_tab_control=False)
105 def GetDevtoolsRemotePort(self):
106 return 'localabstract:chromium_testshell_devtools_remote'
109 def is_content_shell(self):
113 def profile_dir(self):
114 return '/data/data/%s/app_chromiumtestshell/' % self.package
117 class WebviewBackendSettings(AndroidBrowserBackendSettings):
118 def __init__(self, adb, package):
119 super(WebviewBackendSettings, self).__init__(
121 activity='com.android.webview.chromium.shell.TelemetryActivity',
122 cmdline_file='/data/local/tmp/webview-command-line',
124 pseudo_exec_name='webview',
125 supports_tab_control=False)
127 def GetDevtoolsRemotePort(self):
128 # The DevTools socket name for WebView depends on the activity PID's.
133 pids = self.adb.ExtractPid(self.package)
141 logging.critical('android_browser_backend: Timeout while waiting for '
142 'activity %s:%s to come up',
145 raise exceptions.BrowserGoneException('Timeout waiting for PID.')
146 return 'localabstract:webview_devtools_remote_%s' % str(pid)
149 def profile_dir(self):
150 return '/data/data/%s/app_webview/' % self.package
153 class AndroidBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
154 """The backend for controlling a browser instance running on Android.
156 def __init__(self, browser_options, backend_settings, rndis,
157 output_profile_path, extensions_to_load):
158 super(AndroidBrowserBackend, self).__init__(
159 is_content_shell=backend_settings.is_content_shell,
160 supports_extensions=False, browser_options=browser_options,
161 output_profile_path=output_profile_path,
162 extensions_to_load=extensions_to_load)
163 if len(extensions_to_load) > 0:
164 raise browser_backend.ExtensionsNotSupportedException(
165 'Android browser does not support extensions.')
167 # Initialize fields so that an explosion during init doesn't break in Close.
168 self._adb = backend_settings.adb
169 self._backend_settings = backend_settings
170 self._saved_cmdline = None
171 if not self.browser_options.keep_test_server_ports:
172 adb_commands.ResetTestServerPortAllocation()
173 self._port = adb_commands.AllocateTestServerPort()
176 self._adb.CloseApplication(self._backend_settings.package)
178 if self._adb.Adb().CanAccessProtectedFileContents():
179 if not self.browser_options.dont_override_profile:
180 self._backend_settings.RemoveProfile()
181 if self.browser_options.profile_dir:
182 self._backend_settings.PushProfile(self.browser_options.profile_dir)
184 # Pre-configure RNDIS forwarding.
185 self._rndis_forwarder = None
187 self._rndis_forwarder = android_rndis.RndisForwarderWithRoot(self._adb)
188 self.WEBPAGEREPLAY_HOST = self._rndis_forwarder.host_ip
189 # TODO(szym): only override DNS if WPR has privileges to proxy on port 25.
190 self._override_dns = False
192 self._SetUpCommandLine()
194 def _SetUpCommandLine(self):
195 def QuoteIfNeeded(arg):
196 # Escape 'key=valueA valueB' to 'key="valueA valueB"'
197 # Already quoted values, or values without space are left untouched.
198 # This is required so CommandLine.java can parse valueB correctly rather
199 # than as a separate switch.
200 params = arg.split('=')
204 if ' ' not in values:
206 if values[0] in '"\'' and values[-1] == values[0]:
208 return '%s="%s"' % (key, values)
210 args = [self._backend_settings.pseudo_exec_name]
211 args.extend(self.GetBrowserStartupArgs())
212 args = ' '.join(map(QuoteIfNeeded, args))
214 self._SetCommandLineFile(args)
216 def _SetCommandLineFile(self, file_contents):
217 def IsProtectedFile(name):
218 if self._adb.Adb().FileExistsOnDevice(name):
219 return not self._adb.Adb().IsFileWritableOnDevice(name)
221 parent_name = os.path.dirname(name)
222 if parent_name != '':
223 return IsProtectedFile(parent_name)
227 if IsProtectedFile(self._backend_settings.cmdline_file):
228 if not self._adb.Adb().CanAccessProtectedFileContents():
229 logging.critical('Cannot set Chrome command line. '
230 'Fix this by flashing to a userdebug build.')
232 self._saved_cmdline = ''.join(self._adb.Adb().GetProtectedFileContents(
233 self._backend_settings.cmdline_file) or [])
234 self._adb.Adb().SetProtectedFileContents(
235 self._backend_settings.cmdline_file, file_contents)
237 self._saved_cmdline = ''.join(self._adb.Adb().GetFileContents(
238 self._backend_settings.cmdline_file) or [])
239 self._adb.Adb().SetFileContents(self._backend_settings.cmdline_file,
243 self._adb.RunShellCommand('logcat -c')
244 if self.browser_options.startup_url:
245 url = self.browser_options.startup_url
248 self._adb.StartActivity(self._backend_settings.package,
249 self._backend_settings.activity,
255 self._adb.Forward('tcp:%d' % self._port,
256 self._backend_settings.GetDevtoolsRemotePort())
259 self._WaitForBrowserToComeUp()
260 self._PostBrowserStartupInitialization()
261 except exceptions.BrowserGoneException:
262 logging.critical('Failed to connect to browser.')
263 if not self._adb.Adb().CanAccessProtectedFileContents():
265 'Resolve this by either: '
266 '(1) Flashing to a userdebug build OR '
267 '(2) Manually enabling web debugging in Chrome at '
268 'Settings > Developer tools > Enable USB Web debugging.')
272 traceback.print_exc()
276 self._SetCommandLineFile(self._saved_cmdline or '')
278 def GetBrowserStartupArgs(self):
279 args = super(AndroidBrowserBackend, self).GetBrowserStartupArgs()
280 if self._override_dns:
281 args = [arg for arg in args
282 if not arg.startswith('--host-resolver-rules')]
283 args.append('--enable-remote-debugging')
284 args.append('--no-restore-state')
285 args.append('--disable-fre')
294 pids = self._adb.ExtractPid(self._backend_settings.package)
296 raise exceptions.BrowserGoneException(self.GetStackTrace())
300 def browser_directory(self):
304 def profile_directory(self):
305 return self._backend_settings.profile_dir
309 return self._backend_settings.package
313 return self._backend_settings.activity
316 def supports_tab_control(self):
317 return self._backend_settings.supports_tab_control
323 super(AndroidBrowserBackend, self).Close()
324 self._adb.CloseApplication(self._backend_settings.package)
326 if self._output_profile_path:
327 logging.info("Pulling profile directory from device: '%s'->'%s'.",
328 self._backend_settings.profile_dir,
329 self._output_profile_path)
330 # To minimize bandwidth it might be good to look at whether all the data
331 # pulled down is really needed e.g. .pak files.
332 self._adb.Pull(self._backend_settings.profile_dir,
333 self._output_profile_path)
335 def IsBrowserRunning(self):
336 pids = self._adb.ExtractPid(self._backend_settings.package)
337 return len(pids) != 0
339 def GetRemotePort(self, local_port):
342 def GetStandardOutput(self):
343 return '\n'.join(self._adb.RunShellCommand('logcat -d -t 500'))
345 def GetStackTrace(self):
346 def Decorate(title, content):
347 return title + '\n' + content + '\n' + '*' * 80 + '\n'
348 # Get the last lines of logcat (large enough to contain stacktrace)
349 logcat = self.GetStandardOutput()
350 ret = Decorate('Logcat', logcat)
351 stack = os.path.join(util.GetChromiumSrcDir(), 'third_party',
352 'android_platform', 'development', 'scripts', 'stack')
353 # Try to symbolize logcat.
354 if os.path.exists(stack):
355 p = subprocess.Popen([stack], stdin=subprocess.PIPE,
356 stdout=subprocess.PIPE)
357 ret += Decorate('Stack from Logcat', p.communicate(input=logcat)[0])
359 # Try to get tombstones.
360 tombstones = os.path.join(util.GetChromiumSrcDir(), 'build', 'android',
362 if os.path.exists(tombstones):
363 ret += Decorate('Tombstones',
364 subprocess.Popen([tombstones, '-w', '--device',
366 stdout=subprocess.PIPE).communicate()[0])
369 def AddReplayServerOptions(self, extra_wpr_args):
370 """Override. Only add --no-dns_forwarding if not using RNDIS."""
371 if not self._override_dns:
372 extra_wpr_args.append('--no-dns_forwarding')
374 def CreateForwarder(self, *port_pairs):
375 if self._rndis_forwarder:
376 forwarder = self._rndis_forwarder
377 forwarder.SetPorts(*port_pairs)
378 assert self.WEBPAGEREPLAY_HOST == forwarder.host_ip, (
379 'Host IP address on the RNDIS interface changed. Must restart browser!')
380 if self._override_dns:
381 forwarder.OverrideDns()
383 assert not self._override_dns, ('The user-space forwarder does not support '
385 logging.warning('Using the user-space forwarder.\n')
386 return adb_commands.Forwarder(self._adb, *port_pairs)