| OLD | NEW |
| 1 # Copyright 2014 The Chromium Authors. All rights reserved. | 1 # Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """Provides a variety of device interactions based on adb. | 5 """Provides a variety of device interactions based on adb. |
| 6 | 6 |
| 7 Eventually, this will be based on adb_wrapper. | 7 Eventually, this will be based on adb_wrapper. |
| 8 """ | 8 """ |
| 9 # pylint: disable=unused-argument | 9 # pylint: disable=unused-argument |
| 10 | 10 |
| (...skipping 175 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 186 else: | 186 else: |
| 187 raise ValueError('Unsupported device value: %r' % device) | 187 raise ValueError('Unsupported device value: %r' % device) |
| 188 self._commands_installed = None | 188 self._commands_installed = None |
| 189 self._default_timeout = default_timeout | 189 self._default_timeout = default_timeout |
| 190 self._default_retries = default_retries | 190 self._default_retries = default_retries |
| 191 self._cache = {} | 191 self._cache = {} |
| 192 self._client_caches = {} | 192 self._client_caches = {} |
| 193 assert hasattr(self, decorators.DEFAULT_TIMEOUT_ATTR) | 193 assert hasattr(self, decorators.DEFAULT_TIMEOUT_ATTR) |
| 194 assert hasattr(self, decorators.DEFAULT_RETRIES_ATTR) | 194 assert hasattr(self, decorators.DEFAULT_RETRIES_ATTR) |
| 195 | 195 |
| 196 # We need to flush the cache on uninstall, so wrap self.adb.Uninstall |
| 197 # in order to ensure we detect calls to it. |
| 198 old_adb_uninstall = self.adb.Uninstall |
| 199 def WrappedAdbUninstall(package_name, *args, **kwargs): |
| 200 try: |
| 201 old_adb_uninstall(package_name, *args, **kwargs) |
| 202 except: |
| 203 # Clear cache since we can't be sure of the state. |
| 204 self._cache['package_apk_paths'].pop(package_name, 0) |
| 205 self._cache['package_apk_checksums'].pop(package_name, 0) |
| 206 raise |
| 207 self._cache['package_apk_paths'][package_name] = [] |
| 208 self._cache['package_apk_checksums'][package_name] = set() |
| 209 self.adb.Uninstall = WrappedAdbUninstall |
| 210 |
| 211 self._ClearCache() |
| 212 |
| 196 def __eq__(self, other): | 213 def __eq__(self, other): |
| 197 """Checks whether |other| refers to the same device as |self|. | 214 """Checks whether |other| refers to the same device as |self|. |
| 198 | 215 |
| 199 Args: | 216 Args: |
| 200 other: The object to compare to. This can be a basestring, an instance | 217 other: The object to compare to. This can be a basestring, an instance |
| 201 of adb_wrapper.AdbWrapper, or an instance of DeviceUtils. | 218 of adb_wrapper.AdbWrapper, or an instance of DeviceUtils. |
| 202 Returns: | 219 Returns: |
| 203 Whether |other| refers to the same device as |self|. | 220 Whether |other| refers to the same device as |self|. |
| 204 """ | 221 """ |
| 205 return self.adb.GetDeviceSerial() == str(other) | 222 return self.adb.GetDeviceSerial() == str(other) |
| (...skipping 153 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 359 @decorators.WithTimeoutAndRetriesFromInstance() | 376 @decorators.WithTimeoutAndRetriesFromInstance() |
| 360 def GetApplicationPaths(self, package, timeout=None, retries=None): | 377 def GetApplicationPaths(self, package, timeout=None, retries=None): |
| 361 """Get the paths of the installed apks on the device for the given package. | 378 """Get the paths of the installed apks on the device for the given package. |
| 362 | 379 |
| 363 Args: | 380 Args: |
| 364 package: Name of the package. | 381 package: Name of the package. |
| 365 | 382 |
| 366 Returns: | 383 Returns: |
| 367 List of paths to the apks on the device for the given package. | 384 List of paths to the apks on the device for the given package. |
| 368 """ | 385 """ |
| 386 return self._GetApplicationPathsInternal(package) |
| 387 |
| 388 def _GetApplicationPathsInternal(self, package, skip_cache=False): |
| 389 cached_result = self._cache['package_apk_paths'].get(package) |
| 390 if cached_result is not None and not skip_cache: |
| 391 return list(cached_result) |
| 369 # 'pm path' is liable to incorrectly exit with a nonzero number starting | 392 # 'pm path' is liable to incorrectly exit with a nonzero number starting |
| 370 # in Lollipop. | 393 # in Lollipop. |
| 371 # TODO(jbudorick): Check if this is fixed as new Android versions are | 394 # TODO(jbudorick): Check if this is fixed as new Android versions are |
| 372 # released to put an upper bound on this. | 395 # released to put an upper bound on this. |
| 373 should_check_return = (self.build_version_sdk < | 396 should_check_return = (self.build_version_sdk < |
| 374 constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP) | 397 constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP) |
| 375 output = self.RunShellCommand( | 398 output = self.RunShellCommand( |
| 376 ['pm', 'path', package], check_return=should_check_return) | 399 ['pm', 'path', package], check_return=should_check_return) |
| 377 apks = [] | 400 apks = [] |
| 378 for line in output: | 401 for line in output: |
| 379 if not line.startswith('package:'): | 402 if not line.startswith('package:'): |
| 380 raise device_errors.CommandFailedError( | 403 raise device_errors.CommandFailedError( |
| 381 'pm path returned: %r' % '\n'.join(output), str(self)) | 404 'pm path returned: %r' % '\n'.join(output), str(self)) |
| 382 apks.append(line[len('package:'):]) | 405 apks.append(line[len('package:'):]) |
| 406 self._cache['package_apk_paths'][package] = list(apks) |
| 383 return apks | 407 return apks |
| 384 | 408 |
| 385 @decorators.WithTimeoutAndRetriesFromInstance() | 409 @decorators.WithTimeoutAndRetriesFromInstance() |
| 386 def GetApplicationDataDirectory(self, package, timeout=None, retries=None): | 410 def GetApplicationDataDirectory(self, package, timeout=None, retries=None): |
| 387 """Get the data directory on the device for the given package. | 411 """Get the data directory on the device for the given package. |
| 388 | 412 |
| 389 Args: | 413 Args: |
| 390 package: Name of the package. | 414 package: Name of the package. |
| 391 | 415 |
| 392 Returns: | 416 Returns: |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 425 def sd_card_ready(): | 449 def sd_card_ready(): |
| 426 try: | 450 try: |
| 427 self.RunShellCommand(['test', '-d', self.GetExternalStoragePath()], | 451 self.RunShellCommand(['test', '-d', self.GetExternalStoragePath()], |
| 428 check_return=True) | 452 check_return=True) |
| 429 return True | 453 return True |
| 430 except device_errors.AdbCommandFailedError: | 454 except device_errors.AdbCommandFailedError: |
| 431 return False | 455 return False |
| 432 | 456 |
| 433 def pm_ready(): | 457 def pm_ready(): |
| 434 try: | 458 try: |
| 435 return self.GetApplicationPaths('android') | 459 return self._GetApplicationPathsInternal('android', skip_cache=True) |
| 436 except device_errors.CommandFailedError: | 460 except device_errors.CommandFailedError: |
| 437 return False | 461 return False |
| 438 | 462 |
| 439 def boot_completed(): | 463 def boot_completed(): |
| 440 return self.GetProp('sys.boot_completed') == '1' | 464 return self.GetProp('sys.boot_completed') == '1' |
| 441 | 465 |
| 442 def wifi_enabled(): | 466 def wifi_enabled(): |
| 443 return 'Wi-Fi is enabled' in self.RunShellCommand(['dumpsys', 'wifi'], | 467 return 'Wi-Fi is enabled' in self.RunShellCommand(['dumpsys', 'wifi'], |
| 444 check_return=False) | 468 check_return=False) |
| 445 | 469 |
| (...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 495 reinstall: A boolean indicating if we should keep any existing app data. | 519 reinstall: A boolean indicating if we should keep any existing app data. |
| 496 timeout: timeout in seconds | 520 timeout: timeout in seconds |
| 497 retries: number of retries | 521 retries: number of retries |
| 498 | 522 |
| 499 Raises: | 523 Raises: |
| 500 CommandFailedError if the installation fails. | 524 CommandFailedError if the installation fails. |
| 501 CommandTimeoutError if the installation times out. | 525 CommandTimeoutError if the installation times out. |
| 502 DeviceUnreachableError on missing device. | 526 DeviceUnreachableError on missing device. |
| 503 """ | 527 """ |
| 504 package_name = apk_helper.GetPackageName(apk_path) | 528 package_name = apk_helper.GetPackageName(apk_path) |
| 505 device_paths = self.GetApplicationPaths(package_name) | 529 device_paths = self._GetApplicationPathsInternal(package_name) |
| 506 if device_paths: | 530 if device_paths: |
| 507 if len(device_paths) > 1: | 531 if len(device_paths) > 1: |
| 508 logging.warning( | 532 logging.warning( |
| 509 'Installing single APK (%s) when split APKs (%s) are currently ' | 533 'Installing single APK (%s) when split APKs (%s) are currently ' |
| 510 'installed.', apk_path, ' '.join(device_paths)) | 534 'installed.', apk_path, ' '.join(device_paths)) |
| 511 (files_to_push, _) = self._GetChangedAndStaleFiles( | 535 apks_to_install, host_checksums = ( |
| 512 apk_path, device_paths[0]) | 536 self._ComputeStaleApks(package_name, [apk_path])) |
| 513 should_install = bool(files_to_push) | 537 should_install = bool(apks_to_install) |
| 514 if should_install and not reinstall: | 538 if should_install and not reinstall: |
| 515 self.adb.Uninstall(package_name) | 539 self.adb.Uninstall(package_name) |
| 516 else: | 540 else: |
| 517 should_install = True | 541 should_install = True |
| 542 host_checksums = None |
| 518 if should_install: | 543 if should_install: |
| 544 # We won't know the resulting device apk names. |
| 545 self._cache['package_apk_paths'].pop(package_name, 0) |
| 519 self.adb.Install(apk_path, reinstall=reinstall) | 546 self.adb.Install(apk_path, reinstall=reinstall) |
| 547 self._cache['package_apk_checksums'][package_name] = host_checksums |
| 520 | 548 |
| 521 @decorators.WithTimeoutAndRetriesDefaults( | 549 @decorators.WithTimeoutAndRetriesDefaults( |
| 522 INSTALL_DEFAULT_TIMEOUT, | 550 INSTALL_DEFAULT_TIMEOUT, |
| 523 INSTALL_DEFAULT_RETRIES) | 551 INSTALL_DEFAULT_RETRIES) |
| 524 def InstallSplitApk(self, base_apk, split_apks, reinstall=False, | 552 def InstallSplitApk(self, base_apk, split_apks, reinstall=False, |
| 525 timeout=None, retries=None): | 553 timeout=None, retries=None): |
| 526 """Install a split APK. | 554 """Install a split APK. |
| 527 | 555 |
| 528 Noop if all of the APK splits are already installed. | 556 Noop if all of the APK splits are already installed. |
| 529 | 557 |
| 530 Args: | 558 Args: |
| 531 base_apk: A string of the path to the base APK. | 559 base_apk: A string of the path to the base APK. |
| 532 split_apks: A list of strings of paths of all of the APK splits. | 560 split_apks: A list of strings of paths of all of the APK splits. |
| 533 reinstall: A boolean indicating if we should keep any existing app data. | 561 reinstall: A boolean indicating if we should keep any existing app data. |
| 534 timeout: timeout in seconds | 562 timeout: timeout in seconds |
| 535 retries: number of retries | 563 retries: number of retries |
| 536 | 564 |
| 537 Raises: | 565 Raises: |
| 538 CommandFailedError if the installation fails. | 566 CommandFailedError if the installation fails. |
| 539 CommandTimeoutError if the installation times out. | 567 CommandTimeoutError if the installation times out. |
| 540 DeviceUnreachableError on missing device. | 568 DeviceUnreachableError on missing device. |
| 541 DeviceVersionError if device SDK is less than Android L. | 569 DeviceVersionError if device SDK is less than Android L. |
| 542 """ | 570 """ |
| 543 self._CheckSdkLevel(constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP) | 571 self._CheckSdkLevel(constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP) |
| 544 | 572 |
| 545 all_apks = [base_apk] + split_select.SelectSplits( | 573 all_apks = [base_apk] + split_select.SelectSplits( |
| 546 self, base_apk, split_apks) | 574 self, base_apk, split_apks) |
| 547 package_name = apk_helper.GetPackageName(base_apk) | 575 package_name = apk_helper.GetPackageName(base_apk) |
| 548 device_apk_paths = self.GetApplicationPaths(package_name) | 576 device_apk_paths = self._GetApplicationPathsInternal(package_name) |
| 549 | 577 |
| 550 if device_apk_paths: | 578 if device_apk_paths: |
| 551 partial_install_package = package_name | 579 partial_install_package = package_name |
| 552 device_checksums = md5sum.CalculateDeviceMd5Sums(device_apk_paths, self) | 580 apks_to_install, host_checksums = ( |
| 553 host_checksums = md5sum.CalculateHostMd5Sums(all_apks) | 581 self._ComputeStaleApks(package_name, all_apks)) |
| 554 apks_to_install = [k for (k, v) in host_checksums.iteritems() | |
| 555 if v not in device_checksums.values()] | |
| 556 if apks_to_install and not reinstall: | 582 if apks_to_install and not reinstall: |
| 557 self.adb.Uninstall(package_name) | 583 self.adb.Uninstall(package_name) |
| 558 partial_install_package = None | 584 partial_install_package = None |
| 559 apks_to_install = all_apks | 585 apks_to_install = all_apks |
| 560 else: | 586 else: |
| 561 partial_install_package = None | 587 partial_install_package = None |
| 562 apks_to_install = all_apks | 588 apks_to_install = all_apks |
| 589 host_checksums = None |
| 563 if apks_to_install: | 590 if apks_to_install: |
| 591 # We won't know the resulting device apk names. |
| 592 self._cache['package_apk_paths'].pop(package_name, 0) |
| 564 self.adb.InstallMultiple( | 593 self.adb.InstallMultiple( |
| 565 apks_to_install, partial=partial_install_package, reinstall=reinstall) | 594 apks_to_install, partial=partial_install_package, |
| 595 reinstall=reinstall) |
| 596 self._cache['package_apk_checksums'][package_name] = host_checksums |
| 566 | 597 |
| 567 def _CheckSdkLevel(self, required_sdk_level): | 598 def _CheckSdkLevel(self, required_sdk_level): |
| 568 """Raises an exception if the device does not have the required SDK level. | 599 """Raises an exception if the device does not have the required SDK level. |
| 569 """ | 600 """ |
| 570 if self.build_version_sdk < required_sdk_level: | 601 if self.build_version_sdk < required_sdk_level: |
| 571 raise device_errors.DeviceVersionError( | 602 raise device_errors.DeviceVersionError( |
| 572 ('Requires SDK level %s, device is SDK level %s' % | 603 ('Requires SDK level %s, device is SDK level %s' % |
| 573 (required_sdk_level, self.build_version_sdk)), | 604 (required_sdk_level, self.build_version_sdk)), |
| 574 device_serial=self.adb.GetDeviceSerial()) | 605 device_serial=self.adb.GetDeviceSerial()) |
| 575 | 606 |
| (...skipping 331 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 907 | 938 |
| 908 Raises: | 939 Raises: |
| 909 CommandTimeoutError on timeout. | 940 CommandTimeoutError on timeout. |
| 910 DeviceUnreachableError on missing device. | 941 DeviceUnreachableError on missing device. |
| 911 """ | 942 """ |
| 912 # Check that the package exists before clearing it for android builds below | 943 # Check that the package exists before clearing it for android builds below |
| 913 # JB MR2. Necessary because calling pm clear on a package that doesn't exist | 944 # JB MR2. Necessary because calling pm clear on a package that doesn't exist |
| 914 # may never return. | 945 # may never return. |
| 915 if ((self.build_version_sdk >= | 946 if ((self.build_version_sdk >= |
| 916 constants.ANDROID_SDK_VERSION_CODES.JELLY_BEAN_MR2) | 947 constants.ANDROID_SDK_VERSION_CODES.JELLY_BEAN_MR2) |
| 917 or self.GetApplicationPaths(package)): | 948 or self._GetApplicationPathsInternal(package)): |
| 918 self.RunShellCommand(['pm', 'clear', package], check_return=True) | 949 self.RunShellCommand(['pm', 'clear', package], check_return=True) |
| 919 | 950 |
| 920 @decorators.WithTimeoutAndRetriesFromInstance() | 951 @decorators.WithTimeoutAndRetriesFromInstance() |
| 921 def SendKeyEvent(self, keycode, timeout=None, retries=None): | 952 def SendKeyEvent(self, keycode, timeout=None, retries=None): |
| 922 """Sends a keycode to the device. | 953 """Sends a keycode to the device. |
| 923 | 954 |
| 924 See the pylib.constants.keyevent module for suitable keycode values. | 955 See the pylib.constants.keyevent module for suitable keycode values. |
| 925 | 956 |
| 926 Args: | 957 Args: |
| 927 keycode: A integer keycode to send to the device. | 958 keycode: A integer keycode to send to the device. |
| (...skipping 103 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1031 to_push = [] | 1062 to_push = [] |
| 1032 for host_abs_path, host_checksum in host_checksums.iteritems(): | 1063 for host_abs_path, host_checksum in host_checksums.iteritems(): |
| 1033 device_abs_path = '%s/%s' % ( | 1064 device_abs_path = '%s/%s' % ( |
| 1034 real_device_path, os.path.relpath(host_abs_path, real_host_path)) | 1065 real_device_path, os.path.relpath(host_abs_path, real_host_path)) |
| 1035 device_checksum = device_checksums.pop(device_abs_path, None) | 1066 device_checksum = device_checksums.pop(device_abs_path, None) |
| 1036 if device_checksum != host_checksum: | 1067 if device_checksum != host_checksum: |
| 1037 to_push.append((host_abs_path, device_abs_path)) | 1068 to_push.append((host_abs_path, device_abs_path)) |
| 1038 to_delete = device_checksums.keys() | 1069 to_delete = device_checksums.keys() |
| 1039 return (to_push, to_delete) | 1070 return (to_push, to_delete) |
| 1040 | 1071 |
| 1072 def _ComputeDeviceChecksumsForApks(self, package_name): |
| 1073 ret = self._cache['package_apk_checksums'].get(package_name) |
| 1074 if ret is None: |
| 1075 device_paths = self._GetApplicationPathsInternal(package_name) |
| 1076 file_to_checksums = md5sum.CalculateDeviceMd5Sums(device_paths, self) |
| 1077 ret = set(file_to_checksums.values()) |
| 1078 self._cache['package_apk_checksums'][package_name] = ret |
| 1079 return ret |
| 1080 |
| 1081 def _ComputeStaleApks(self, package_name, host_apk_paths): |
| 1082 host_checksums = md5sum.CalculateHostMd5Sums(host_apk_paths) |
| 1083 device_checksums = self._ComputeDeviceChecksumsForApks(package_name) |
| 1084 stale_apks = [k for (k, v) in host_checksums.iteritems() |
| 1085 if v not in device_checksums] |
| 1086 return stale_apks, set(host_checksums.values()) |
| 1087 |
| 1041 def _PushFilesImpl(self, host_device_tuples, files): | 1088 def _PushFilesImpl(self, host_device_tuples, files): |
| 1042 size = sum(host_utils.GetRecursiveDiskUsage(h) for h, _ in files) | 1089 size = sum(host_utils.GetRecursiveDiskUsage(h) for h, _ in files) |
| 1043 file_count = len(files) | 1090 file_count = len(files) |
| 1044 dir_size = sum(host_utils.GetRecursiveDiskUsage(h) | 1091 dir_size = sum(host_utils.GetRecursiveDiskUsage(h) |
| 1045 for h, _ in host_device_tuples) | 1092 for h, _ in host_device_tuples) |
| 1046 dir_file_count = 0 | 1093 dir_file_count = 0 |
| 1047 for h, _ in host_device_tuples: | 1094 for h, _ in host_device_tuples: |
| 1048 if os.path.isdir(h): | 1095 if os.path.isdir(h): |
| 1049 dir_file_count += sum(len(f) for _r, _d, f in os.walk(h)) | 1096 dir_file_count += sum(len(f) for _r, _d, f in os.walk(h)) |
| 1050 else: | 1097 else: |
| (...skipping 715 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1766 def GetClientCache(self, client_name): | 1813 def GetClientCache(self, client_name): |
| 1767 """Returns client cache.""" | 1814 """Returns client cache.""" |
| 1768 if client_name not in self._client_caches: | 1815 if client_name not in self._client_caches: |
| 1769 self._client_caches[client_name] = {} | 1816 self._client_caches[client_name] = {} |
| 1770 return self._client_caches[client_name] | 1817 return self._client_caches[client_name] |
| 1771 | 1818 |
| 1772 def _ClearCache(self): | 1819 def _ClearCache(self): |
| 1773 """Clears all caches.""" | 1820 """Clears all caches.""" |
| 1774 for client in self._client_caches: | 1821 for client in self._client_caches: |
| 1775 self._client_caches[client].clear() | 1822 self._client_caches[client].clear() |
| 1776 self._cache.clear() | 1823 self._cache = { |
| 1824 # Map of packageId -> list of on-device .apk paths |
| 1825 'package_apk_paths': {}, |
| 1826 # Map of packageId -> set of on-device .apk checksums |
| 1827 'package_apk_checksums': {}, |
| 1828 } |
| 1777 | 1829 |
| 1778 @classmethod | 1830 @classmethod |
| 1779 def parallel(cls, devices=None, async=False): | 1831 def parallel(cls, devices=None, async=False): |
| 1780 """Creates a Parallelizer to operate over the provided list of devices. | 1832 """Creates a Parallelizer to operate over the provided list of devices. |
| 1781 | 1833 |
| 1782 If |devices| is either |None| or an empty list, the Parallelizer will | 1834 If |devices| is either |None| or an empty list, the Parallelizer will |
| 1783 operate over all attached devices that have not been blacklisted. | 1835 operate over all attached devices that have not been blacklisted. |
| 1784 | 1836 |
| 1785 Args: | 1837 Args: |
| 1786 devices: A list of either DeviceUtils instances or objects from | 1838 devices: A list of either DeviceUtils instances or objects from |
| (...skipping 28 matching lines...) Expand all Loading... |
| 1815 return [cls(adb) for adb in adb_wrapper.AdbWrapper.Devices() | 1867 return [cls(adb) for adb in adb_wrapper.AdbWrapper.Devices() |
| 1816 if not blacklisted(adb)] | 1868 if not blacklisted(adb)] |
| 1817 | 1869 |
| 1818 @decorators.WithTimeoutAndRetriesFromInstance() | 1870 @decorators.WithTimeoutAndRetriesFromInstance() |
| 1819 def RestartAdbd(self, timeout=None, retries=None): | 1871 def RestartAdbd(self, timeout=None, retries=None): |
| 1820 logging.info('Restarting adbd on device.') | 1872 logging.info('Restarting adbd on device.') |
| 1821 with device_temp_file.DeviceTempFile(self.adb, suffix='.sh') as script: | 1873 with device_temp_file.DeviceTempFile(self.adb, suffix='.sh') as script: |
| 1822 self.WriteFile(script.name, _RESTART_ADBD_SCRIPT) | 1874 self.WriteFile(script.name, _RESTART_ADBD_SCRIPT) |
| 1823 self.RunShellCommand(['source', script.name], as_root=True) | 1875 self.RunShellCommand(['source', script.name], as_root=True) |
| 1824 self.adb.WaitForDevice() | 1876 self.adb.WaitForDevice() |
| OLD | NEW |