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 sys
10
11from catapult_base import dependency_util
12from devil.android import apk_helper
13
14from telemetry.core import exceptions
15from telemetry.core import platform
16from telemetry.core import util
17from telemetry import decorators
18from telemetry.internal.backends import android_browser_backend_settings
19from telemetry.internal.backends.chrome import android_browser_backend
20from telemetry.internal.browser import browser
21from telemetry.internal.browser import possible_browser
22from telemetry.internal.platform import android_device
23from telemetry.internal.util import binary_manager
24
25
26CHROME_PACKAGE_NAMES = {
27  'android-content-shell':
28      ['org.chromium.content_shell_apk',
29       android_browser_backend_settings.ContentShellBackendSettings,
30       'ContentShell.apk'],
31  'android-webview':
32      ['org.chromium.webview_shell',
33       android_browser_backend_settings.WebviewBackendSettings,
34       None],
35  'android-webview-shell':
36      ['org.chromium.android_webview.shell',
37       android_browser_backend_settings.WebviewShellBackendSettings,
38       'AndroidWebView.apk'],
39  'android-chromium':
40      ['org.chromium.chrome',
41       android_browser_backend_settings.ChromeBackendSettings,
42       'ChromePublic.apk'],
43  'android-chrome':
44      ['com.google.android.apps.chrome',
45       android_browser_backend_settings.ChromeBackendSettings,
46       'Chrome.apk'],
47  'android-chrome-work':
48      ['com.chrome.work',
49       android_browser_backend_settings.ChromeBackendSettings,
50       None],
51  'android-chrome-beta':
52      ['com.chrome.beta',
53       android_browser_backend_settings.ChromeBackendSettings,
54       None],
55  'android-chrome-dev':
56      ['com.chrome.dev',
57       android_browser_backend_settings.ChromeBackendSettings,
58       None],
59  'android-chrome-canary':
60      ['com.chrome.canary',
61       android_browser_backend_settings.ChromeBackendSettings,
62       None],
63  'android-system-chrome':
64      ['com.android.chrome',
65       android_browser_backend_settings.ChromeBackendSettings,
66       None],
67}
68
69
70class PossibleAndroidBrowser(possible_browser.PossibleBrowser):
71  """A launchable android browser instance."""
72  def __init__(self, browser_type, finder_options, android_platform,
73               backend_settings, apk_name):
74    super(PossibleAndroidBrowser, self).__init__(
75        browser_type, 'android', backend_settings.supports_tab_control)
76    assert browser_type in FindAllBrowserTypes(finder_options), (
77        'Please add %s to android_browser_finder.FindAllBrowserTypes' %
78         browser_type)
79    self._platform = android_platform
80    self._platform_backend = (
81        android_platform._platform_backend)  # pylint: disable=protected-access
82    self._backend_settings = backend_settings
83    self._local_apk = None
84
85    if browser_type == 'exact':
86      if not os.path.exists(apk_name):
87        raise exceptions.PathMissingError(
88            'Unable to find exact apk %s specified by --browser-executable' %
89            apk_name)
90      self._local_apk = apk_name
91    elif browser_type == 'reference':
92      if not os.path.exists(apk_name):
93        raise exceptions.PathMissingError(
94            'Unable to find reference apk at expected location %s.' % apk_name)
95      self._local_apk = apk_name
96    elif apk_name:
97      assert finder_options.chrome_root, (
98          'Must specify Chromium source to use apk_name')
99      chrome_root = finder_options.chrome_root
100      candidate_apks = []
101      for build_path in util.GetBuildDirectories(chrome_root):
102        apk_full_name = os.path.join(build_path, 'apks', apk_name)
103        if os.path.exists(apk_full_name):
104          last_changed = os.path.getmtime(apk_full_name)
105          candidate_apks.append((last_changed, apk_full_name))
106
107      if candidate_apks:
108        # Find the candidate .apk with the latest modification time.
109        newest_apk_path = sorted(candidate_apks)[-1][1]
110        self._local_apk = newest_apk_path
111
112  def __repr__(self):
113    return 'PossibleAndroidBrowser(browser_type=%s)' % self.browser_type
114
115  def _InitPlatformIfNeeded(self):
116    pass
117
118  def Create(self, finder_options):
119    self._InitPlatformIfNeeded()
120    browser_backend = android_browser_backend.AndroidBrowserBackend(
121        self._platform_backend,
122        finder_options.browser_options, self._backend_settings,
123        output_profile_path=finder_options.output_profile_path,
124        extensions_to_load=finder_options.extensions_to_load)
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, finder_options):
139    if len(finder_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    package_info = next((info for info in CHROME_PACKAGE_NAMES.itervalues()
189                         if info[2] == apk_name), None)
190
191    # It is okay if the APK name doesn't match any of known chrome browser APKs,
192    # since it may be of a different browser.
193    if package_info:
194      normalized_path = os.path.expanduser(finder_options.browser_executable)
195      exact_package = apk_helper.GetPackageName(normalized_path)
196      if not exact_package:
197        raise exceptions.PackageDetectionError(
198            'Unable to find package for %s specified by --browser-executable' %
199            normalized_path)
200
201      [package, backend_settings, _] = package_info
202      if package == exact_package:
203        possible_browsers.append(PossibleAndroidBrowser(
204            'exact',
205            finder_options,
206            android_platform,
207            backend_settings(package),
208            normalized_path))
209      else:
210        raise exceptions.UnknownPackageError(
211            '%s specified by --browser-executable has an unknown package: %s' %
212            (normalized_path, exact_package))
213
214  # Add the reference build if found.
215  os_version = dependency_util.GetChromeApkOsVersion(
216      android_platform.GetOSVersionName())
217  arch = android_platform.GetArchName()
218  try:
219    reference_build = binary_manager.FetchPath(
220        'chrome_stable', arch, 'android', os_version)
221  except (binary_manager.NoPathFoundError,
222          binary_manager.CloudStorageError):
223    reference_build = None
224
225  if reference_build and os.path.exists(reference_build):
226    # TODO(aiolos): how do we stably map the android chrome_stable apk to the
227    # correct package name?
228    package, backend_settings, _ = CHROME_PACKAGE_NAMES['android-chrome']
229    possible_browsers.append(PossibleAndroidBrowser(
230        'reference',
231        finder_options,
232        android_platform,
233        backend_settings(package),
234        reference_build))
235
236  # Add any known local versions.
237  for name, package_info in CHROME_PACKAGE_NAMES.iteritems():
238    package, backend_settings, local_apk = package_info
239    b = PossibleAndroidBrowser(name,
240                               finder_options,
241                               android_platform,
242                               backend_settings(package),
243                               local_apk)
244    if b.platform.CanLaunchApplication(package) or b.HaveLocalAPK():
245      possible_browsers.append(b)
246  return possible_browsers
247
248
249def FindAllAvailableBrowsers(finder_options, device):
250  """Finds all the possible browsers on one device.
251
252  The device is either the only device on the host platform,
253  or |finder_options| specifies a particular device.
254  """
255  if not isinstance(device, android_device.AndroidDevice):
256    return []
257  android_platform = platform.GetPlatformForDevice(device, finder_options)
258  return _FindAllPossibleBrowsers(finder_options, android_platform)
259