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
5"""Finds android browsers that can be controlled by telemetry."""
6
7import logging
8import os
9import subprocess
10import sys
11
12from py_utils import dependency_util
13from devil import base_error
14from devil.android import apk_helper
15
16from telemetry.core import exceptions
17from telemetry.core import platform
18from telemetry.core import util
19from telemetry import decorators
20from telemetry.internal.backends import android_browser_backend_settings
21from telemetry.internal.backends.chrome import android_browser_backend
22from telemetry.internal.browser import browser
23from telemetry.internal.browser import possible_browser
24from telemetry.internal.platform import android_device
25from telemetry.internal.util import binary_manager
26
27
28CHROME_PACKAGE_NAMES = {
29  'android-content-shell':
30      ['org.chromium.content_shell_apk',
31       android_browser_backend_settings.ContentShellBackendSettings,
32       'ContentShell.apk'],
33  'android-webview':
34      ['org.chromium.webview_shell',
35       android_browser_backend_settings.WebviewBackendSettings,
36       None],
37  'android-webview-shell':
38      ['org.chromium.android_webview.shell',
39       android_browser_backend_settings.WebviewShellBackendSettings,
40       'AndroidWebView.apk'],
41  'android-chromium':
42      ['org.chromium.chrome',
43       android_browser_backend_settings.ChromeBackendSettings,
44       'ChromePublic.apk'],
45  'android-chrome':
46      ['com.google.android.apps.chrome',
47       android_browser_backend_settings.ChromeBackendSettings,
48       'Chrome.apk'],
49  'android-chrome-work':
50      ['com.chrome.work',
51       android_browser_backend_settings.ChromeBackendSettings,
52       None],
53  'android-chrome-beta':
54      ['com.chrome.beta',
55       android_browser_backend_settings.ChromeBackendSettings,
56       None],
57  'android-chrome-dev':
58      ['com.chrome.dev',
59       android_browser_backend_settings.ChromeBackendSettings,
60       None],
61  'android-chrome-canary':
62      ['com.chrome.canary',
63       android_browser_backend_settings.ChromeBackendSettings,
64       None],
65  'android-system-chrome':
66      ['com.android.chrome',
67       android_browser_backend_settings.ChromeBackendSettings,
68       None],
69}
70
71
72class PossibleAndroidBrowser(possible_browser.PossibleBrowser):
73  """A launchable android browser instance."""
74  def __init__(self, browser_type, finder_options, android_platform,
75               backend_settings, apk_name):
76    super(PossibleAndroidBrowser, self).__init__(
77        browser_type, 'android', backend_settings.supports_tab_control)
78    assert browser_type in FindAllBrowserTypes(finder_options), (
79        'Please add %s to android_browser_finder.FindAllBrowserTypes' %
80         browser_type)
81    self._platform = android_platform
82    self._platform_backend = (
83        android_platform._platform_backend)  # pylint: disable=protected-access
84    self._backend_settings = backend_settings
85    self._local_apk = None
86
87    if browser_type == 'exact':
88      if not os.path.exists(apk_name):
89        raise exceptions.PathMissingError(
90            'Unable to find exact apk %s specified by --browser-executable' %
91            apk_name)
92      self._local_apk = apk_name
93    elif browser_type == 'reference':
94      if not os.path.exists(apk_name):
95        raise exceptions.PathMissingError(
96            'Unable to find reference apk at expected location %s.' % apk_name)
97      self._local_apk = apk_name
98    elif apk_name:
99      assert finder_options.chrome_root, (
100          'Must specify Chromium source to use apk_name')
101      chrome_root = finder_options.chrome_root
102      candidate_apks = []
103      for build_path in util.GetBuildDirectories(chrome_root):
104        apk_full_name = os.path.join(build_path, 'apks', apk_name)
105        if os.path.exists(apk_full_name):
106          last_changed = os.path.getmtime(apk_full_name)
107          candidate_apks.append((last_changed, apk_full_name))
108
109      if candidate_apks:
110        # Find the candidate .apk with the latest modification time.
111        newest_apk_path = sorted(candidate_apks)[-1][1]
112        self._local_apk = newest_apk_path
113
114  def __repr__(self):
115    return 'PossibleAndroidBrowser(browser_type=%s)' % self.browser_type
116
117  def _InitPlatformIfNeeded(self):
118    pass
119
120  def Create(self, finder_options):
121    self._InitPlatformIfNeeded()
122    browser_backend = android_browser_backend.AndroidBrowserBackend(
123        self._platform_backend,
124        finder_options.browser_options, self._backend_settings)
125    try:
126      return browser.Browser(
127          browser_backend, self._platform_backend, self._credentials_path)
128    except Exception:
129      logging.exception('Failure while creating Android browser.')
130      original_exception = sys.exc_info()
131      try:
132        browser_backend.Close()
133      except Exception:
134        logging.exception('Secondary failure while closing browser backend.')
135
136      raise original_exception[0], original_exception[1], original_exception[2]
137
138  def SupportsOptions(self, browser_options):
139    if len(browser_options.extensions_to_load) != 0:
140      return False
141    return True
142
143  def HaveLocalAPK(self):
144    return self._local_apk and os.path.exists(self._local_apk)
145
146  @decorators.Cache
147  def UpdateExecutableIfNeeded(self):
148    if self.HaveLocalAPK():
149      logging.warn('Installing %s on device if needed.' % self._local_apk)
150      self.platform.InstallApplication(self._local_apk)
151
152  def last_modification_time(self):
153    if self.HaveLocalAPK():
154      return os.path.getmtime(self._local_apk)
155    return -1
156
157
158def SelectDefaultBrowser(possible_browsers):
159  """Return the newest possible browser."""
160  if not possible_browsers:
161    return None
162  return max(possible_browsers, key=lambda b: b.last_modification_time())
163
164
165def CanFindAvailableBrowsers():
166  return android_device.CanDiscoverDevices()
167
168
169def CanPossiblyHandlePath(target_path):
170  return os.path.splitext(target_path.lower())[1] == '.apk'
171
172
173def FindAllBrowserTypes(options):
174  del options  # unused
175  return CHROME_PACKAGE_NAMES.keys() + ['exact', 'reference']
176
177
178def _FindAllPossibleBrowsers(finder_options, android_platform):
179  """Testable version of FindAllAvailableBrowsers."""
180  if not android_platform:
181    return []
182  possible_browsers = []
183
184  # Add the exact APK if given.
185  if (finder_options.browser_executable and
186      CanPossiblyHandlePath(finder_options.browser_executable)):
187    apk_name = os.path.basename(finder_options.browser_executable)
188    normalized_path = os.path.expanduser(finder_options.browser_executable)
189    exact_package = apk_helper.GetPackageName(normalized_path)
190    package_info = next(
191        (info for info in CHROME_PACKAGE_NAMES.itervalues()
192         if info[0] == exact_package or info[2] == apk_name), None)
193
194    # It is okay if the APK name or package doesn't match any of known chrome
195    # browser APKs, since it may be of a different browser.
196    if package_info:
197      if not exact_package:
198        raise exceptions.PackageDetectionError(
199            'Unable to find package for %s specified by --browser-executable' %
200            normalized_path)
201
202      [package, backend_settings, _] = package_info
203      if package == exact_package:
204        possible_browsers.append(PossibleAndroidBrowser(
205            'exact',
206            finder_options,
207            android_platform,
208            backend_settings(package),
209            normalized_path))
210      else:
211        raise exceptions.UnknownPackageError(
212            '%s specified by --browser-executable has an unknown package: %s' %
213            (normalized_path, exact_package))
214
215  # Add the reference build if found.
216  os_version = dependency_util.GetChromeApkOsVersion(
217      android_platform.GetOSVersionName())
218  arch = android_platform.GetArchName()
219  try:
220    reference_build = binary_manager.FetchPath(
221        'chrome_stable', arch, 'android', os_version)
222  except (binary_manager.NoPathFoundError,
223          binary_manager.CloudStorageError):
224    reference_build = None
225
226  if reference_build and os.path.exists(reference_build):
227    # TODO(aiolos): how do we stably map the android chrome_stable apk to the
228    # correct package name?
229    package, backend_settings, _ = CHROME_PACKAGE_NAMES['android-chrome']
230    possible_browsers.append(PossibleAndroidBrowser(
231        'reference',
232        finder_options,
233        android_platform,
234        backend_settings(package),
235        reference_build))
236
237  # Add any known local versions.
238  for name, package_info in CHROME_PACKAGE_NAMES.iteritems():
239    package, backend_settings, apk_name = package_info
240    if apk_name and not finder_options.chrome_root:
241      continue
242    b = PossibleAndroidBrowser(name,
243                               finder_options,
244                               android_platform,
245                               backend_settings(package),
246                               apk_name)
247    if b.platform.CanLaunchApplication(package) or b.HaveLocalAPK():
248      possible_browsers.append(b)
249  return possible_browsers
250
251
252def FindAllAvailableBrowsers(finder_options, device):
253  """Finds all the possible browsers on one device.
254
255  The device is either the only device on the host platform,
256  or |finder_options| specifies a particular device.
257  """
258  if not isinstance(device, android_device.AndroidDevice):
259    return []
260
261  try:
262    android_platform = platform.GetPlatformForDevice(device, finder_options)
263    return _FindAllPossibleBrowsers(finder_options, android_platform)
264  except base_error.BaseError as e:
265    logging.error('Unable to find browsers on %s: %s', device.device_id, str(e))
266    ps_output = subprocess.check_output(['ps', '-ef'])
267    logging.error('Ongoing processes:\n%s', ps_output)
268  return []
269