diff --git a/mobly/controllers/android_device.py b/mobly/controllers/android_device.py
index cb9ab52..c3197ca 100644
--- a/mobly/controllers/android_device.py
+++ b/mobly/controllers/android_device.py
@@ -18,6 +18,7 @@ from past.builtins import basestring
 import contextlib
 import logging
 import os
+import re
 import shutil
 import time
 
@@ -52,8 +53,10 @@ CACHED_SYSTEM_PROPS = [
     'ro.build.version.codename',
     'ro.build.version.sdk',
     'ro.build.product',
+    'ro.build.characteristics',
     'ro.debuggable',
     'ro.product.name',
+    'ro.hardware',
 ]
 
 # Keys for attributes in configs that alternate the controller module behavior.
@@ -78,6 +81,9 @@ Error = errors.Error
 DeviceError = errors.DeviceError
 SnippetError = snippet_management_service.Error
 
+# Regex to heuristically determine if the device is an emulator.
+EMULATOR_SERIAL_REGEX = re.compile(r'emulator-\d+')
+
 
 def create(configs):
     """Creates AndroidDevice controller objects.
@@ -338,7 +344,6 @@ def get_devices(ads, **kwargs):
     Raises:
         Error: No devices are matched.
     """
-
     def _get_device_filter(ad):
         for k, v in kwargs.items():
             if not hasattr(ad, k):
@@ -443,7 +448,6 @@ class AndroidDevice(object):
         services: ServiceManager, the manager of long-running services on the
             device.
     """
-
     def __init__(self, serial=''):
         self._serial = str(serial)
         # logging.log_path only exists when this is used in an Mobly test run.
@@ -763,8 +767,11 @@ class AndroidDevice(object):
             info['build_version_sdk'] = build_info.get('ro.build.version.sdk',
                                                        '')
             info['build_product'] = build_info.get('ro.build.product', '')
+            info['build_characteristics'] = build_info.get(
+                'ro.build.characteristics', '')
             info['debuggable'] = build_info.get('ro.debuggable', '')
             info['product_name'] = build_info.get('ro.product.name', '')
+            info['hardware'] = build_info.get('ro.hardware', '')
             self._build_info = info
             return info
         return self._build_info
@@ -810,6 +817,31 @@ class AndroidDevice(object):
             return model
         return self.build_info['product_name'].lower()
 
+    @property
+    def is_emulator(self):
+        """Whether this device is probably an emulator.
+
+        Returns:
+            True if this is probably an emulator.
+        """
+        if EMULATOR_SERIAL_REGEX.match(self.serial):
+            # If the device's serial follows 'emulator-dddd', then it's almost
+            # certainly an emulator.
+            return True
+        elif self.build_info['build_characteristics'] == 'emulator':
+            # If the device says that it's an emulator, then it's probably an
+            # emulator although some real devices apparently report themselves
+            # as emulators in addition to other things, so only return True on
+            # an exact match.
+            return True
+        elif self.build_info['hardware'] in ['ranchu', 'goldfish']:
+            # Ranchu and Goldfish are the hardware properties that the AOSP
+            # emulators report, so if the device says it's an AOSP emulator, it
+            # probably is one.
+            return True
+        else:
+            return False
+
     def load_config(self, config):
         """Add attributes to the AndroidDevice object based on config.
 
@@ -1049,7 +1081,6 @@ class AndroidDeviceLoggerAdapter(logging.LoggerAdapter):
     Then each log line added by my_log will have a prefix
     '[AndroidDevice|<tag>]'
     """
-
     def process(self, msg, kwargs):
         msg = _DEBUG_PREFIX_TEMPLATE % (self.extra['tag'], msg)
         return (msg, kwargs)

Add is_emulator property to AndroidDevice
diff --git a/tests/lib/mock_android_device.py b/tests/lib/mock_android_device.py
index 959623a..28c90e6 100755
--- a/tests/lib/mock_android_device.py
+++ b/tests/lib/mock_android_device.py
@@ -30,6 +30,8 @@ DEFAULT_MOCK_PROPERTIES = {
     'ro.product.name': 'FakeModel',
     'ro.debuggable': '1',
     'sys.boot_completed': "1",
+    'ro.build.characteristics': 'emulator,phone',
+    'ro.hardware': 'marlin',
 }
 
 
@@ -76,7 +78,6 @@ def list_adb_devices():
 
 class MockAdbProxy(object):
     """Mock class that swaps out calls to adb with mock calls."""
-
     def __init__(self,
                  serial='',
                  fail_br=False,
@@ -115,13 +116,16 @@ class MockAdbProxy(object):
             packages = self.installed_packages + [
                 package for package, _, _ in self.instrumented_packages
             ]
-            return bytes('\n'.join(
-                ['package:%s' % package for package in packages]), 'utf-8')
+            return bytes(
+                '\n'.join(['package:%s' % package for package in packages]),
+                'utf-8')
         elif 'pm list instrumentation' in params:
-            return bytes('\n'.join([
-                'instrumentation:%s/%s (target=%s)' % (package, runner, target)
-                for package, runner, target in self.instrumented_packages
-            ]), 'utf-8')
+            return bytes(
+                '\n'.join([
+                    'instrumentation:%s/%s (target=%s)' %
+                    (package, runner, target)
+                    for package, runner, target in self.instrumented_packages
+                ]), 'utf-8')
         elif 'which' in params:
             return b''
 
@@ -144,7 +148,6 @@ class MockAdbProxy(object):
         """All calls to the none-existent functions in adb proxy would
         simply return the adb command string.
         """
-
         def adb_call(*args, **kwargs):
             arg_str = ' '.join(str(elem) for elem in args)
             return arg_str
@@ -154,7 +157,6 @@ class MockAdbProxy(object):
 
 class MockFastbootProxy(object):
     """Mock class that swaps out calls to adb with mock calls."""
-
     def __init__(self, serial):
         self.serial = serial
 
diff --git a/tests/mobly/controllers/android_device_test.py b/tests/mobly/controllers/android_device_test.py
index 4438490..f3f8e0c 100755
--- a/tests/mobly/controllers/android_device_test.py
+++ b/tests/mobly/controllers/android_device_test.py
@@ -44,7 +44,6 @@ class AndroidDeviceTest(unittest.TestCase):
     """This test class has unit tests for the implementation of everything
     under mobly.controllers.android_device.
     """
-
     def setUp(self):
         # Set log_path to logging since mobly logger setup is not called.
         if not hasattr(logging, 'log_path'):
@@ -280,8 +279,10 @@ class AndroidDeviceTest(unittest.TestCase):
         self.assertEqual(build_info['build_version_codename'], 'Z')
         self.assertEqual(build_info['build_version_sdk'], '28')
         self.assertEqual(build_info['build_product'], 'FakeModel')
+        self.assertEqual(build_info['build_characteristics'], 'emulator,phone')
         self.assertEqual(build_info['product_name'], 'FakeModel')
         self.assertEqual(build_info['debuggable'], '1')
+        self.assertEqual(build_info['hardware'], 'marlin')
         self.assertEqual(len(build_info),
                          len(android_device.CACHED_SYSTEM_PROPS))
 
@@ -303,8 +304,10 @@ class AndroidDeviceTest(unittest.TestCase):
         self.assertEqual(build_info['build_version_codename'], '')
         self.assertEqual(build_info['build_version_sdk'], '')
         self.assertEqual(build_info['build_product'], '')
+        self.assertEqual(build_info['build_characteristics'], '')
         self.assertEqual(build_info['product_name'], '')
         self.assertEqual(build_info['debuggable'], '')
+        self.assertEqual(build_info['hardware'], '')
 
     @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy',
                 return_value=mock_android_device.MockAdbProxy('1'))
@@ -384,6 +387,95 @@ class AndroidDeviceTest(unittest.TestCase):
         self.assertTrue(isinstance(ad.serial, str))
         yaml.safe_dump(ad.serial)
 
+    @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy',
+                return_value=mock_android_device.MockAdbProxy('1'))
+    @mock.patch('mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+                return_value=mock_android_device.MockFastbootProxy('1'))
+    def test_AndroidDevice_is_emulator_when_realish_device(
+            self, MockFastboot, MockAdbProxy):
+        ad = android_device.AndroidDevice(serial='1')
+        self.assertFalse(ad.is_emulator)
+
+    @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy',
+                return_value=mock_android_device.MockAdbProxy('localhost:123'))
+    @mock.patch(
+        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+        return_value=mock_android_device.MockFastbootProxy('localhost:123'))
+    def test_AndroidDevice_is_emulator_when_local_networked_device(
+            self, MockFastboot, MockAdbProxy):
+        # Although these devices are usually emulators, there might be a reason
+        # to do this with a real device.
+        ad = android_device.AndroidDevice(serial='localhost:123')
+        self.assertFalse(ad.is_emulator)
+
+    @mock.patch(
+        'mobly.controllers.android_device_lib.adb.AdbProxy',
+        return_value=mock_android_device.MockAdbProxy('example.com:123'))
+    @mock.patch(
+        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+        return_value=mock_android_device.MockFastbootProxy('example:123'))
+    def test_AndroidDevice_is_emulator_when_remote_networked_device(
+            self, MockFastboot, MockAdbProxy):
+        ad = android_device.AndroidDevice(serial='example.com:123')
+        self.assertFalse(ad.is_emulator)
+
+    @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy',
+                return_value=mock_android_device.MockAdbProxy(
+                    'localhost:5554',
+                    mock_properties={
+                        'ro.hardware': 'ranchu',
+                        'ro.build.id': 'AB42',
+                        'ro.build.type': 'userdebug',
+                    }))
+    @mock.patch(
+        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+        return_value=mock_android_device.MockFastbootProxy('localhost:5554'))
+    def test_AndroidDevice_is_emulator_when_ranchu_device(
+            self, MockFastboot, MockAdbProxy):
+        ad = android_device.AndroidDevice(serial='localhost:5554')
+        self.assertTrue(ad.is_emulator)
+
+    @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy',
+                return_value=mock_android_device.MockAdbProxy(
+                    '1',
+                    mock_properties={
+                        'ro.build.id': 'AB42',
+                        'ro.build.type': 'userdebug',
+                        'ro.hardware': 'goldfish',
+                    }))
+    @mock.patch('mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+                return_value=mock_android_device.MockFastbootProxy('1'))
+    def test_AndroidDevice_is_emulator_when_goldfish_device(
+            self, MockFastboot, MockAdbProxy):
+        ad = android_device.AndroidDevice(serial='1')
+        self.assertTrue(ad.is_emulator)
+
+    @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy',
+                return_value=mock_android_device.MockAdbProxy(
+                    'example.com:123',
+                    mock_properties={
+                        'ro.build.id': 'AB42',
+                        'ro.build.type': 'userdebug',
+                        'ro.build.characteristics': 'emulator',
+                    }))
+    @mock.patch(
+        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+        return_value=mock_android_device.MockFastbootProxy('example.com:123'))
+    def test_AndroidDevice_is_emulator_when_emulator_characteristic(
+            self, MockFastboot, MockAdbProxy):
+        ad = android_device.AndroidDevice(serial='example.com:123')
+        self.assertTrue(ad.is_emulator)
+
+    @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy',
+                return_value=mock_android_device.MockAdbProxy('emulator-5554'))
+    @mock.patch(
+        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+        return_value=mock_android_device.MockFastbootProxy('emulator-5554'))
+    def test_AndroidDevice_is_emulator_when_emulator_serial(
+            self, MockFastboot, MockAdbProxy):
+        ad = android_device.AndroidDevice(serial='emulator-5554')
+        self.assertTrue(ad.is_emulator)
+
     @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy',
                 return_value=mock_android_device.MockAdbProxy(1))
     @mock.patch('mobly.controllers.android_device_lib.fastboot.FastbootProxy',

pip install -e .
parse_log_pytest
requirements.txt
pytest mock pytz
apt-get update apt-get install -y gcc
3.8
docs/requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
babel==2.17.0
certifi==2025.1.31
charset-normalizer==3.4.1
commonmark==0.9.1
docutils==0.20.1
exceptiongroup==1.2.2
future==1.0.0
idna==3.10
imagesize==1.4.1
importlib_metadata==8.5.0
iniconfig==2.1.0
Jinja2==3.1.6
MarkupSafe==2.1.5
-e git+https://github.com/google/mobly.git@4541f3a65fe6631fe3e9621b620472ce0ce66602#egg=mobly
mock==5.2.0
packaging==24.2
pluggy==1.5.0
portpicker==1.6.0
psutil==7.0.0
Pygments==2.19.1
pyserial==3.5
pytest==8.3.5
pytz==2025.2
PyYAML==6.0.2
recommonmark==0.7.1
requests==2.32.3
snowballstemmer==2.2.0
Sphinx==7.1.2
sphinxcontrib-applehelp==1.0.4
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
timeout-decorator==0.5.0
tomli==2.2.1
urllib3==2.2.3
zipp==3.20.2

name: mobly
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=24.2=py38h06a4308_0
  - python=3.8.20=he870216_0
  - readline=8.2=h5eee18b_0
  - setuptools=75.1.0=py38h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.44.0=py38h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - babel==2.17.0
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - commonmark==0.9.1
      - docutils==0.20.1
      - exceptiongroup==1.2.2
      - future==1.0.0
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.5.0
      - iniconfig==2.1.0
      - jinja2==3.1.6
      - markupsafe==2.1.5
      - mock==5.2.0
      - packaging==24.2
      - pluggy==1.5.0
      - portpicker==1.6.0
      - psutil==7.0.0
      - pygments==2.19.1
      - pyserial==3.5
      - pytest==8.3.5
      - pytz==2025.2
      - pyyaml==6.0.2
      - recommonmark==0.7.1
      - requests==2.32.3
      - snowballstemmer==2.2.0
      - sphinx==7.1.2
      - sphinxcontrib-applehelp==1.0.4
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - timeout-decorator==0.5.0
      - tomli==2.2.1
      - urllib3==2.2.3
      - zipp==3.20.2
prefix: /opt/conda/envs/mobly

tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_build_info tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_build_info_with_minimal_properties tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_is_emulator_when_emulator_characteristic tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_is_emulator_when_emulator_serial tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_is_emulator_when_goldfish_device tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_is_emulator_when_local_networked_device tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_is_emulator_when_ranchu_device tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_is_emulator_when_realish_device tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_is_emulator_when_remote_networked_device
tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_build_info_cached tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_change_log_path tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_change_log_path_no_log_exists tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_change_log_path_with_existing_file tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_change_log_path_with_service tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_debug_tag tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_device_info tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_getattr tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_handle_reboot tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_handle_reboot_changes_build_info tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_handle_reboot_changes_build_info_with_caching tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_handle_usb_disconnect tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_instantiation tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_is_rootable_when_user_device tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_is_rootable_when_userdebug_device tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_load_snippet tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_load_snippet_dup_attribute_name tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_load_snippet_dup_package tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_load_snippet_dup_snippet_name tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_load_snippet_start_app_fails tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_serial_is_valid tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_snippet_cleanup tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_take_bug_report tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_take_bug_report_fail tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_take_bug_report_fallback tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_take_bug_report_with_destination tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_take_bug_report_with_only_begin_time tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_take_bug_report_with_only_test_name tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_take_bug_report_with_positional_args tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_take_bug_report_without_args tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_unload_snippet tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_update_serial tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_update_serial_with_service_running tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_wait_for_completion_completed tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_wait_for_completion_never_boot tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_AndroidDevice_with_reserved_character_in_serial_log_path tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_create_with_dict_list tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_create_with_empty_config tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_create_with_no_valid_config tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_create_with_not_list_config tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_create_with_pickup_all tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_create_with_string_list tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_create_with_usb_id tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_get_device_no_match tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_get_device_success_with_serial tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_get_device_success_with_serial_and_extra_field tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_get_device_too_many_matches tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_get_devices_no_match tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_get_devices_success_with_extra_field tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_start_services_on_ads tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_start_services_on_ads_skip_logcat tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_take_bug_reports tests/mobly/controllers/android_device_test.py::AndroidDeviceTest::test_take_bug_reports_with_none_values
mobly/controllers/android_device.py
mobly/controllers/android_device.py