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 os
7import posixpath
8import re
9import subprocess
10import threading
11import time
12
13from telemetry.core import android_platform
14from telemetry.core import exceptions
15from telemetry.core import util
16from telemetry import compat_mode_options
17from telemetry import decorators
18from telemetry.internal.forwarders import android_forwarder
19from telemetry.internal.platform import android_device
20from telemetry.internal.platform import linux_based_platform_backend
21from telemetry.internal.util import binary_manager
22from telemetry.internal.util import external_modules
23from telemetry.testing import test_utils
24
25from devil.android import app_ui
26from devil.android import battery_utils
27from devil.android import cpu_temperature
28from devil.android import device_errors
29from devil.android import device_utils
30from devil.android.perf import cache_control
31from devil.android.perf import perf_control
32from devil.android.perf import thermal_throttle
33from devil.android.sdk import shared_prefs
34from devil.android.tools import provision_devices
35from devil.android.tools import system_app
36from devil.android.tools import video_recorder
37
38try:
39  # devil.android.forwarder uses fcntl, which doesn't exist on Windows.
40  from devil.android import forwarder
41except ImportError:
42  forwarder = None
43
44try:
45  from devil.android.perf import surface_stats_collector
46except Exception: # pylint: disable=broad-except
47  surface_stats_collector = None
48
49psutil = external_modules.ImportOptionalModule('psutil')
50
51_ARCH_TO_STACK_TOOL_ARCH = {
52    'armeabi-v7a': 'arm',
53    'arm64-v8a': 'arm64',
54}
55_MAP_TO_USER_FRIENDLY_OS_NAMES = {
56    'l': 'lollipop',
57    'm': 'marshmallow',
58    'n': 'nougat',
59    'o': 'oreo',
60    'p': 'pie',
61    'q': '10',
62    'k': 'kitkat'
63}
64_MAP_TO_USER_FRIENDLY_DEVICE_NAMES = {
65    'gobo': 'go',
66    'W6210': 'one',
67    'AOSP on Shamu': 'nexus 6',
68    'AOSP on BullHead': 'nexus 5x'
69}
70_DEVICE_COPY_SCRIPT_FILE = os.path.abspath(os.path.join(
71    os.path.dirname(__file__), 'efficient_android_directory_copy.sh'))
72_DEVICE_COPY_SCRIPT_LOCATION = (
73    '/data/local/tmp/efficient_android_directory_copy.sh')
74_DEVICE_MEMTRACK_HELPER_LOCATION = '/data/local/tmp/profilers/memtrack_helper'
75_DEVICE_CLEAR_SYSTEM_CACHE_TOOL_LOCATION = '/data/local/tmp/clear_system_cache'
76
77
78class _VideoRecorder(object):
79  def __init__(self):
80    self._stop_recording_signal = threading.Event()
81    self._recording_path = None
82    self._runner = None
83
84  def WaitForSignal(self):
85    self._stop_recording_signal.wait()
86
87  @property
88  def recording_path(self):
89    return self._recording_path
90
91  def Start(self, device):
92    def record_video(device, state):
93      recorder = video_recorder.VideoRecorder(device)
94      with recorder:
95        state.WaitForSignal()
96      if state.recording_path:
97        f = recorder.Pull(state.recording_path)
98        logging.info('Video written to %s' % os.path.abspath(f))
99
100    # Start recording the video in parallel to running the story, so that the
101    # video recording here does not block running the story (which involve
102    # executing additional commands in parallel on the device).
103    parallel_devices = device_utils.DeviceUtils.parallel([device], async=True)
104    self._runner = parallel_devices.pMap(record_video, self)
105
106  def Stop(self, video_path):
107    self._recording_path = video_path
108    self._stop_recording_signal.set()
109    # Recording the video may take a few seconds in the extreme cases. So allow
110    # a few seconds when shutting down the recording.
111    self._runner.pGet(timeout=10)
112
113
114class AndroidPlatformBackend(
115    linux_based_platform_backend.LinuxBasedPlatformBackend):
116  def __init__(self, device, require_root):
117    assert device, (
118        'AndroidPlatformBackend can only be initialized from remote device')
119    super(AndroidPlatformBackend, self).__init__(device)
120    self._device = device_utils.DeviceUtils(device.device_id)
121    self._can_elevate_privilege = False
122    self._require_root = require_root
123    if self._require_root:
124      # Trying to root the device, if possible.
125      if not self._device.HasRoot():
126        try:
127          self._device.EnableRoot()
128        except device_errors.CommandFailedError:
129          logging.warning('Unable to root %s', str(self._device))
130      self._can_elevate_privilege = (
131          self._device.HasRoot() or self._device.NeedsSU())
132      assert self._can_elevate_privilege, (
133          'Android device must have root access to run Telemetry')
134    self._battery = battery_utils.BatteryUtils(self._device)
135    self._surface_stats_collector = None
136    self._perf_tests_setup = perf_control.PerfControl(self._device)
137    self._thermal_throttle = thermal_throttle.ThermalThrottle(self._device)
138    self._raw_display_frame_rate_measurements = []
139    self._device_copy_script = None
140    self._system_ui = None
141    self._device_host_clock_offset = None
142    self._video_recorder = None
143
144    # TODO(https://crbug.com/1026296): Remove this once --chromium-output-dir
145    # has a default value we can use.
146    self._build_dir = util.GetUsedBuildDirectory()
147
148    _FixPossibleAdbInstability()
149
150  @property
151  def log_file_path(self):
152    return None
153
154  @classmethod
155  def SupportsDevice(cls, device):
156    return isinstance(device, android_device.AndroidDevice)
157
158  @classmethod
159  def CreatePlatformForDevice(cls, device, finder_options):
160    assert cls.SupportsDevice(device)
161    require_root = (compat_mode_options.DONT_REQUIRE_ROOTED_DEVICE not in
162                    finder_options.browser_options.compatibility_mode)
163    platform_backend = AndroidPlatformBackend(device, require_root)
164    return android_platform.AndroidPlatform(platform_backend)
165
166  def _CreateForwarderFactory(self):
167    return android_forwarder.AndroidForwarderFactory(self._device)
168
169  @property
170  def device(self):
171    return self._device
172
173  def Initialize(self):
174    self.EnsureBackgroundApkInstalled()
175
176  def GetSystemUi(self):
177    if self._system_ui is None:
178      self._system_ui = app_ui.AppUi(self.device, 'com.android.systemui')
179    return self._system_ui
180
181  def GetSharedPrefs(self, package, filename, use_encrypted_path=False):
182    """Creates a Devil SharedPrefs instance.
183
184    See devil.android.sdk.shared_prefs for the documentation of the returned
185    object.
186
187    Args:
188      package: A string containing the package of the app that the SharedPrefs
189          instance will be for.
190      filename: A string containing the specific settings file of the app that
191          the SharedPrefs instance will be for.
192      use_encrypted_path: Whether to use the newer device-encrypted path
193          (/data/user_de/) instead of the older unencrypted path (/data/data/).
194
195    Returns:
196      A reference to a SharedPrefs object for the given package and filename
197      on whatever device the platform backend has a reference to.
198    """
199    return shared_prefs.SharedPrefs(
200        self._device, package, filename, use_encrypted_path=use_encrypted_path)
201
202  def IsSvelte(self):
203    # TODO (crbug.com/973936) This function is used to find out if expectations
204    # with the Android_Svelte tag should apply to a certain build in the old
205    # expectations format. Instead of eliminating the Android_Svelte tag in the
206    # old format, we should wait until we change the expectations into the new
207    # format. And then we can get rid of this function.
208    description = self._device.GetProp('ro.build.description', cache=True)
209    return description and 'svelte' in description
210
211  def IsLowEnd(self):
212    return self.IsSvelte() or self.GetDeviceTypeName() in ('gobo', 'W6210')
213
214  def IsAosp(self):
215    description = self._device.GetProp('ro.build.description', cache=True)
216    if description is not None:
217      return 'aosp' in description
218    return False
219
220
221  def GetRemotePort(self, port):
222    return forwarder.Forwarder.DevicePortForHostPort(port) or 0
223
224  def IsRemoteDevice(self):
225    # Android device is connected via adb which is on remote.
226    return True
227
228  def IsDisplayTracingSupported(self):
229    return bool(self.GetOSVersionName() >= 'J')
230
231  def StartDisplayTracing(self):
232    assert not self._surface_stats_collector
233    # Clear any leftover data from previous timed out tests
234    self._raw_display_frame_rate_measurements = []
235    self._surface_stats_collector = \
236        surface_stats_collector.SurfaceStatsCollector(self._device)
237    self._surface_stats_collector.Start()
238
239  def StopDisplayTracing(self):
240    if not self._surface_stats_collector:
241      return
242
243    try:
244      refresh_period, timestamps = self._surface_stats_collector.Stop()
245      pid = self._surface_stats_collector.GetSurfaceFlingerPid()
246    finally:
247      self._surface_stats_collector = None
248    # TODO(sullivan): should this code be inline, or live elsewhere?
249    events = [_BuildEvent(
250        '__metadata', 'process_name', 'M', pid, 0, {'name': 'SurfaceFlinger'})]
251    for ts in timestamps:
252      events.append(_BuildEvent('SurfaceFlinger', 'vsync_before', 'I', pid, ts,
253                                {'data': {'frame_count': 1}}))
254
255    return {
256        'traceEvents': events,
257        'metadata': {
258            'clock-domain': 'LINUX_CLOCK_MONOTONIC',
259            'surface_flinger': {
260                'refresh_period': refresh_period,
261            },
262        }
263    }
264
265  def CanTakeScreenshot(self):
266    return True
267
268  def TakeScreenshot(self, file_path):
269    return bool(self._device.TakeScreenshot(host_path=file_path))
270
271  def CanRecordVideo(self):
272    return True
273
274  def StartVideoRecording(self):
275    assert self._video_recorder is None
276    self._video_recorder = _VideoRecorder()
277    self._video_recorder.Start(self.device)
278
279  def StopVideoRecording(self, video_path):
280    assert self._video_recorder
281    self._video_recorder.Stop(video_path)
282    self._video_recorder = None
283
284  def CooperativelyShutdown(self, proc, app_name):
285    # Suppress the 'abstract-method' lint warning.
286    return False
287
288  def SetPerformanceMode(self, performance_mode):
289    if not self._can_elevate_privilege:
290      logging.warning('No root privileges, so ignoring performance mode.')
291      return
292    if performance_mode == android_device.KEEP_PERFORMANCE_MODE:
293      logging.info('Keeping device performance settings intact.')
294      return
295    elif performance_mode == android_device.HIGH_PERFORMANCE_MODE:
296      logging.info('Setting high performance mode.')
297      self._perf_tests_setup.SetHighPerfMode()
298    elif performance_mode == android_device.NORMAL_PERFORMANCE_MODE:
299      logging.info('Setting normal performance mode.')
300      self._perf_tests_setup.SetDefaultPerfMode()
301    elif performance_mode == android_device.LITTLE_ONLY_PERFORMANCE_MODE:
302      logging.info('Setting little-only performance mode.')
303      self._perf_tests_setup.SetLittleOnlyMode()
304    else:
305      raise ValueError('Unknown performance mode: %s' % performance_mode)
306
307
308  def CanMonitorThermalThrottling(self):
309    return True
310
311  def IsThermallyThrottled(self):
312    return self._thermal_throttle.IsThrottled()
313
314  def HasBeenThermallyThrottled(self):
315    return self._thermal_throttle.HasBeenThrottled()
316
317  def SetGraphicsMemoryTrackingEnabled(self, enabled):
318    if not enabled:
319      self.KillApplication('memtrack_helper')
320      return
321
322    binary_manager.ReinstallAndroidHelperIfNeeded(
323        'memtrack_helper', _DEVICE_MEMTRACK_HELPER_LOCATION,
324        self._device)
325    self._device.RunShellCommand(
326        [_DEVICE_MEMTRACK_HELPER_LOCATION, '-d'], as_root=True,
327        check_return=True)
328
329  def EnsureBackgroundApkInstalled(self):
330    app = 'push_apps_to_background_apk'
331    arch_name = self._device.GetABI()
332    host_path = binary_manager.FetchPath(app, 'android', arch_name)
333    if not host_path:
334      raise Exception('Error installing PushAppsToBackground.apk.')
335    self.InstallApplication(host_path)
336
337  @decorators.Cache
338  def GetArchName(self):
339    return self._device.GetABI()
340
341  def GetOSName(self):
342    return 'android'
343
344  def GetDeviceId(self):
345    return self._device.serial
346
347  def GetDeviceTypeName(self):
348    return self._device.product_model
349
350  def GetTypExpectationsTags(self):
351    # telemetry benchmark's expectations need to know the model name
352    # and if it is a low end device
353    os_version = self.GetOSVersionName().lower()
354    os_version = _MAP_TO_USER_FRIENDLY_OS_NAMES.get(os_version, os_version)
355    tags = test_utils.sanitizeTypExpectationsTags(
356        [self.GetOSName(), 'android-' + os_version])
357    device_type_name = self.GetDeviceTypeName()
358    tags += test_utils.sanitizeTypExpectationsTags(
359        ['android-' + _MAP_TO_USER_FRIENDLY_DEVICE_NAMES.get(
360            device_type_name, device_type_name)])
361    if self.IsLowEnd():
362      tags.append('android-low-end')
363    tags.append('mobile')
364    return tags
365
366  @decorators.Cache
367  def GetOSVersionName(self):
368    return self._device.GetProp('ro.build.id')[0]
369
370  def GetOSVersionDetailString(self):
371    return ''  # TODO(kbr): Implement this.
372
373  def GetDeviceHostClockOffset(self):
374    """Returns the difference between the device and host clocks."""
375    if self._device_host_clock_offset is None:
376      # Get the current time in seconds since the epoch.
377      device_time = self.device.RunShellCommand(
378          ['date', '+%s'], single_line=True)
379      host_time = time.time()
380      self._device_host_clock_offset = int(int(device_time.strip()) - host_time)
381    return self._device_host_clock_offset
382
383  def CanFlushIndividualFilesFromSystemCache(self):
384    return True
385
386  def SupportFlushEntireSystemCache(self):
387    return self._can_elevate_privilege
388
389  def FlushEntireSystemCache(self):
390    cache = cache_control.CacheControl(self._device)
391    cache.DropRamCaches()
392
393  def FlushSystemCacheForDirectory(self, directory):
394    binary_manager.ReinstallAndroidHelperIfNeeded(
395        'clear_system_cache', _DEVICE_CLEAR_SYSTEM_CACHE_TOOL_LOCATION,
396        self._device)
397    self._device.RunShellCommand(
398        [_DEVICE_CLEAR_SYSTEM_CACHE_TOOL_LOCATION, '--recurse', directory],
399        as_root=True, check_return=True)
400
401  def FlushDnsCache(self):
402    self._device.RunShellCommand(
403        ['ndc', 'resolver', 'flushdefaultif'], as_root=True, check_return=True)
404
405  def StopApplication(self, application):
406    """Stop the given |application|.
407
408    Args:
409       application: The full package name string of the application to stop.
410    """
411    self._device.ForceStop(application)
412
413  def KillApplication(self, application):
414    """Kill the given |application|.
415
416    Might be used instead of ForceStop for efficiency reasons.
417
418    Args:
419      application: The full package name string of the application to kill.
420    """
421    assert isinstance(application, basestring)
422    self._device.KillAll(application, blocking=True, quiet=True, as_root=True)
423
424  def LaunchApplication(
425      self, application, parameters=None, elevate_privilege=False):
426    """Launches the given |application| with a list of |parameters| on the OS.
427
428    Args:
429      application: The full package name string of the application to launch.
430      parameters: A list of parameters to be passed to the ActivityManager.
431      elevate_privilege: Currently unimplemented on Android.
432    """
433    if elevate_privilege:
434      raise NotImplementedError("elevate_privilege isn't supported on android.")
435    # TODO(catapult:#3215): Migrate to StartActivity.
436    cmd = ['am', 'start']
437    if parameters:
438      cmd.extend(parameters)
439    cmd.append(application)
440    result_lines = self._device.RunShellCommand(cmd, check_return=True)
441    for line in result_lines:
442      if line.startswith('Error: '):
443        raise ValueError('Failed to start "%s" with error\n  %s' %
444                         (application, line))
445
446  def StartActivity(self, intent, blocking):
447    """Starts an activity for the given intent on the device."""
448    self._device.StartActivity(intent, blocking=blocking)
449
450  def CanLaunchApplication(self, application):
451    return bool(self._device.GetApplicationPaths(application))
452
453  # pylint: disable=arguments-differ
454  def InstallApplication(self, application, modules=None):
455    self._device.Install(application, modules=modules)
456
457  def RemoveSystemPackages(self, packages):
458    system_app.RemoveSystemApps(self._device, packages)
459
460  def PathExists(self, device_path, **kwargs):
461    """ Return whether the given path exists on the device.
462    This method is the same as
463    devil.android.device_utils.DeviceUtils.PathExists.
464    """
465    return self._device.PathExists(device_path, **kwargs)
466
467  def GetFileContents(self, fname):
468    if not self._can_elevate_privilege:
469      logging.warning('%s cannot be retrieved on non-rooted device.', fname)
470      return ''
471    return self._device.ReadFile(fname, as_root=True)
472
473  def RunCommand(self, command):
474    return '\n'.join(self._device.RunShellCommand(
475        command, shell=isinstance(command, basestring), check_return=True))
476
477  def SetRelaxSslCheck(self, value):
478    old_flag = self._device.GetProp('socket.relaxsslcheck')
479    self._device.SetProp('socket.relaxsslcheck', value)
480    return old_flag
481
482  def DismissCrashDialogIfNeeded(self):
483    """Dismiss any error dialogs.
484
485    Limit the number in case we have an error loop or we are failing to dismiss.
486    """
487    for _ in xrange(10):
488      if not self._device.DismissCrashDialogIfNeeded():
489        break
490
491  def PushProfile(self, package, new_profile_dir):
492    """Replace application profile with files found on host machine.
493
494    Pushing the profile is slow, so we don't want to do it every time.
495    Avoid this by pushing to a safe location using PushChangedFiles, and
496    then copying into the correct location on each test run.
497
498    Args:
499      package: The full package name string of the application for which the
500        profile is to be updated.
501      new_profile_dir: Location where profile to be pushed is stored on the
502        host machine.
503    """
504    (profile_parent, profile_base) = os.path.split(new_profile_dir)
505    # If the path ends with a '/' python split will return an empty string for
506    # the base name; so we now need to get the base name from the directory.
507    if not profile_base:
508      profile_base = os.path.basename(profile_parent)
509
510    provision_devices.CheckExternalStorage(self._device)
511
512    saved_profile_location = posixpath.join(
513        self._device.GetExternalStoragePath(),
514        'profile', profile_base)
515    self._device.PushChangedFiles([(new_profile_dir, saved_profile_location)],
516                                  delete_device_stale=True)
517
518    profile_dir = self.GetProfileDir(package)
519    self._EfficientDeviceDirectoryCopy(
520        saved_profile_location, profile_dir)
521    dumpsys = self._device.RunShellCommand(
522        ['dumpsys', 'package', package], check_return=True)
523    id_line = next(line for line in dumpsys if 'userId=' in line)
524    uid = re.search(r'\d+', id_line).group()
525
526    # Generate all of the paths copied to the device, via walking through
527    # |new_profile_dir| and doing path manipulations. This could be replaced
528    # with recursive commands (e.g. chown -R) below, but those are not well
529    # supported by older Android versions.
530    device_paths = []
531    for root, dirs, files in os.walk(new_profile_dir):
532      rel_root = os.path.relpath(root, new_profile_dir)
533      posix_rel_root = rel_root.replace(os.sep, posixpath.sep)
534
535      device_root = posixpath.normpath(posixpath.join(profile_dir,
536                                                      posix_rel_root))
537
538      if rel_root == '.' and 'lib' in files:
539        files.remove('lib')
540      device_paths.extend(posixpath.join(device_root, n) for n in files + dirs)
541
542    owner_group = '%s.%s' % (uid, uid)
543    self._device.ChangeOwner(owner_group, device_paths)
544
545    # Not having the correct SELinux security context can prevent Chrome from
546    # loading files even though the mode/group/owner combination should allow
547    # it.
548    security_context = self._device.GetSecurityContextForPackage(package)
549    self._device.ChangeSecurityContext(security_context, device_paths)
550
551  def _EfficientDeviceDirectoryCopy(self, source, dest):
552    if not self._device_copy_script:
553      self._device.adb.Push(
554          _DEVICE_COPY_SCRIPT_FILE,
555          _DEVICE_COPY_SCRIPT_LOCATION)
556      self._device_copy_script = _DEVICE_COPY_SCRIPT_LOCATION
557    self._device.RunShellCommand(
558        ['sh', self._device_copy_script, source, dest], check_return=True)
559
560  def RemoveProfile(self, package, ignore_list):
561    """Delete application profile on device.
562
563    Args:
564      package: The full package name string of the application for which the
565        profile is to be deleted.
566      ignore_list: List of files to keep.
567    """
568    profile_dir = self.GetProfileDir(package)
569    if not self._device.PathExists(profile_dir):
570      return
571    files = [
572        posixpath.join(profile_dir, f)
573        for f in self._device.ListDirectory(profile_dir, as_root=True)
574        if f not in ignore_list]
575    if not files:
576      return
577    self._device.RemovePath(files, force=True, recursive=True, as_root=True)
578
579  def GetProfileDir(self, package):
580    """Returns the on-device location where the application profile is stored
581    based on Android convention.
582
583    Args:
584      package: The full package name string of the application.
585    """
586    if self._require_root:
587      return '/data/data/%s/' % package
588    else:
589      return '/data/local/tmp/%s/' % package
590
591  def GetDumpLocation(self, package):
592    """Returns the location where crash dumps should be written to.
593
594    Args:
595      package: A string containing the package name of the application that the
596          dump location is for.
597    """
598    # On Android Q+, apps by default only have access to certain directories,
599    # so use a subdirectory of the package's profile directory to guarantee that
600    # it has access to it.
601    return self.GetProfileDir(package) + 'dumps'
602
603  def SetDebugApp(self, package):
604    """Set application to debugging.
605
606    Args:
607      package: The full package name string of the application.
608    """
609    if self._device.IsUserBuild():
610      logging.debug('User build device, setting debug app')
611      self._device.RunShellCommand(
612          ['am', 'set-debug-app', '--persistent', package],
613          check_return=True)
614
615  def GetLogCat(self, number_of_lines=1500):
616    """Returns most recent lines of logcat dump.
617
618    Args:
619      number_of_lines: Number of lines of log to return.
620    """
621    def decode_line(line):
622      try:
623        uline = unicode(line, encoding='utf-8')
624        return uline.encode('ascii', 'backslashreplace')
625      except Exception: # pylint: disable=broad-except
626        logging.error('Error encoding UTF-8 logcat line as ASCII.')
627        return '<MISSING LOGCAT LINE: FAILED TO ENCODE>'
628
629    logcat_output = self._device.RunShellCommand(
630        ['logcat', '-d', '-t', str(number_of_lines)],
631        check_return=True, large_output=True)
632    return '\n'.join(decode_line(l) for l in logcat_output)
633
634  def SymbolizeLogCat(self, logcat):
635    """Attempts to symbolize any crash stacks in the given logcat data.
636
637    Args:
638      logcat: A string containing the logcat data to be symbolized.
639
640    Returns:
641      A string containing the symbolized logcat data, or None if the symbolize
642      script was not found.
643    """
644    stack = os.path.join(util.GetChromiumSrcDir(), 'third_party',
645                         'android_platform', 'development', 'scripts', 'stack')
646    if _ExecutableExists(stack):
647      cmd = [stack]
648      arch = self.GetArchName()
649      arch = _ARCH_TO_STACK_TOOL_ARCH.get(arch, arch)
650      cmd.append('--arch=%s' % arch)
651      cmd.append('--output-directory=%s' % self._build_dir)
652      p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
653      return p.communicate(input=logcat)[0]
654
655    return None
656
657  def GetTombstones(self):
658    """Attempts to get any tombstones currently on the device.
659
660    Returns:
661      A string containing any tombstones found on the device, or None if the
662      tombstones script was not found or failed.
663    """
664    tombstones = os.path.join(util.GetChromiumSrcDir(), 'build', 'android',
665                              'tombstones.py')
666    if _ExecutableExists(tombstones):
667      tombstones_cmd = [
668          tombstones, '-w',
669          '--device', self._device.adb.GetDeviceSerial(),
670          '--adb-path', self._device.adb.GetAdbPath(),
671          '--output-directory=%s' % self._build_dir,
672      ]
673      try:
674        return subprocess.check_output(tombstones_cmd)
675      except subprocess.CalledProcessError:
676        return None
677
678    return None
679
680  def GetStandardOutput(self):
681    return 'Cannot get standard output on Android'
682
683  def IsScreenOn(self):
684    """Determines if device screen is on."""
685    return self._device.IsScreenOn()
686
687  @staticmethod
688  def _ExtractLastNativeCrashPackageFromLogcat(
689      logcat, default_package_name='com.google.android.apps.chrome'):
690    # pylint: disable=line-too-long
691    # Match against lines like:
692    # <unimportant prefix> : Fatal signal 5 (SIGTRAP), code -6 in tid NNNNN (oid.apps.chrome)
693    # <a few more lines>
694    # <unimportant prefix>: Build fingerprint: 'google/bullhead/bullhead:7.1.2/N2G47F/3769476:userdebug/dev-keys'
695    # <a few more lines>
696    # <unimportant prefix> : pid: NNNNN, tid: NNNNN, name: oid.apps.chrome  >>> com.google.android.apps.chrome <<<
697    # pylint: enable=line-too-long
698    fatal_signal_re = re.compile(r'.*: Fatal signal [0-9]')
699    build_fingerprint_re = re.compile(r'.*: Build fingerprint: ')
700    package_re = re.compile(r'.*: pid: [0-9]+, tid: [0-9]+, name: .*'
701                            r'>>> (?P<package_name>[^ ]+) <<<')
702    last_package = default_package_name
703    build_fingerprint_found = False
704    lookahead_lines_remaining = 0
705    for line in logcat.splitlines():
706      if fatal_signal_re.match(line):
707        lookahead_lines_remaining = 10
708        continue
709      if not lookahead_lines_remaining:
710        build_fingerprint_found = False
711      else:
712        lookahead_lines_remaining -= 1
713        if build_fingerprint_re.match(line):
714          build_fingerprint_found = True
715          continue
716        if build_fingerprint_found:
717          m = package_re.match(line)
718          if m:
719            last_package = m.group('package_name')
720            # The package name may have a trailing process name in it,
721            # for example: "org.chromium.chrome:privileged_process0".
722            last_package = last_package.split(':')[0]
723    return last_package
724
725  @staticmethod
726  def _IsScreenLocked(input_methods):
727    """Parser method for IsScreenLocked()
728
729    Args:
730      input_methods: Output from dumpsys input_methods
731
732    Returns:
733      boolean: True if screen is locked, false if screen is not locked.
734
735    Raises:
736      ValueError: An unknown value is found for the screen lock state.
737      AndroidDeviceParsingError: Error in detecting screen state.
738
739    """
740    for line in input_methods:
741      if 'mHasBeenInactive' in line:
742        for pair in line.strip().split(' '):
743          key, value = pair.split('=', 1)
744          if key == 'mHasBeenInactive':
745            if value == 'true':
746              return True
747            elif value == 'false':
748              return False
749            else:
750              raise ValueError('Unknown value for %s: %s' % (key, value))
751    raise exceptions.AndroidDeviceParsingError(str(input_methods))
752
753  def IsScreenLocked(self):
754    """Determines if device screen is locked."""
755    input_methods = self._device.RunShellCommand(['dumpsys', 'input_method'],
756                                                 check_return=True)
757    return self._IsScreenLocked(input_methods)
758
759  def Log(self, message):
760    """Prints line to logcat."""
761    TELEMETRY_LOGCAT_TAG = 'Telemetry'
762    self._device.RunShellCommand(
763        ['log', '-p', 'i', '-t', TELEMETRY_LOGCAT_TAG, message],
764        check_return=True)
765
766  def WaitForBatteryTemperature(self, temp):
767    # Temperature is in tenths of a degree C, so we convert to that scale.
768    self._battery.LetBatteryCoolToTemperature(temp * 10)
769
770  def WaitForCpuTemperature(self, temp):
771    controller = cpu_temperature.CpuTemperature(self._device)
772    # Check if the device temperature zones are known
773    if controller.IsSupported():
774      controller.LetCpuCoolToTemperature(temp)
775    else:
776      logging.warn('CPU temperature cooling delay - '
777                   'CPU temperature cannot be read: Either the current '
778                   'device is not supported or the specified temperature '
779                   'zones do not exist.')
780
781
782def _FixPossibleAdbInstability():
783  """Host side workaround for crbug.com/268450 (adb instability).
784
785  The adb server has a race which is mitigated by binding to a single core.
786  """
787  if not psutil:
788    return
789  for process in psutil.process_iter():
790    try:
791      if psutil.version_info >= (2, 0):
792        if 'adb' in process.name():
793          process.cpu_affinity([0])
794      else:
795        if 'adb' in process.name:
796          process.set_cpu_affinity([0])
797    except (psutil.NoSuchProcess, psutil.AccessDenied):
798      logging.warn('Failed to set adb process CPU affinity')
799
800
801def _BuildEvent(cat, name, ph, pid, ts, args):
802  event = {
803      'cat': cat,
804      'name': name,
805      'ph': ph,
806      'pid': pid,
807      'tid': pid,
808      'ts': ts * 1000,
809      'args': args
810  }
811  # Instant events need to specify the scope, too.
812  if ph == 'I':
813    event['s'] = 't'
814  return event
815
816
817def _ExecutableExists(file_name):
818  return os.access(file_name, os.X_OK)
819