Introduce proper service management for AndroidDevice. (#503)

This is the initial check in of AndroidDevice service mechanism.
More changes and refactorings are coming once the first service (logcat) is in.

* A proper way to manage the life cycles of the AndroidDevice's services.
* Add the service manager into `AndroidDevice`.
* Implement the service for logcat.
diff --git a/mobly/controllers/android_device.py b/mobly/controllers/android_device.py
index 72f0848..e5cde1c 100644
--- a/mobly/controllers/android_device.py
+++ b/mobly/controllers/android_device.py
@@ -16,7 +16,6 @@
 from past.builtins import basestring
 
 import contextlib
-import io
 import logging
 import os
 import shutil
@@ -27,8 +26,10 @@
 from mobly import utils
 from mobly.controllers.android_device_lib import adb
 from mobly.controllers.android_device_lib import fastboot
+from mobly.controllers.android_device_lib import service_manager
 from mobly.controllers.android_device_lib import sl4a_client
 from mobly.controllers.android_device_lib import snippet_client
+from mobly.controllers.android_device_lib.services import logcat
 
 # Convenience constant for the package of Mobly Bundled Snippets
 # (http://github.com/google/mobly-bundled-snippets).
@@ -429,19 +430,6 @@
             via fastboot.
     """
 
-    @property
-    def _normalized_serial(self):
-        """Normalized serial name for usage in log filename.
-
-        Some Android emulators use ip:port as their serial names, while on 
-        Windows `:` is not valid in filename, it should be sanitized first.
-        """
-        if self._serial is None:
-            return None
-        normalized_serial = self._serial.replace(' ', '_')
-        normalized_serial = normalized_serial.replace(':', '-')
-        return normalized_serial
-
     def __init__(self, serial=''):
         self._serial = str(serial)
         # logging.log_path only exists when this is used in an Mobly test run.
@@ -454,12 +442,11 @@
         })
         self.sl4a = None
         self.ed = None
-        self._adb_logcat_process = None
-        self.adb_logcat_file_path = None
         self.adb = adb.AdbProxy(serial)
         self.fastboot = fastboot.FastbootProxy(serial)
         if not self.is_bootloader and self.is_rootable:
             self.root_adb()
+        self.services = service_manager.ServiceManager(self)
         # A dict for tracking snippet clients. Keys are clients' attribute
         # names, values are the clients: {<attr name string>: <client object>}.
         self._snippet_clients = {}
@@ -470,6 +457,26 @@
         return '<AndroidDevice|%s>' % self.debug_tag
 
     @property
+    def adb_logcat_file_path(self):
+        try:
+            return self.services.logcat.adb_logcat_file_path
+        except AttributeError:
+            return None
+
+    @property
+    def _normalized_serial(self):
+        """Normalized serial name for usage in log filename.
+
+        Some Android emulators use ip:port as their serial names, while on 
+        Windows `:` is not valid in filename, it should be sanitized first.
+        """
+        if self._serial is None:
+            return None
+        normalized_serial = self._serial.replace(' ', '_')
+        normalized_serial = normalized_serial.replace(':', '-')
+        return normalized_serial
+
+    @property
     def device_info(self):
         """Information to be pulled into controller info.
 
@@ -532,7 +539,7 @@
         A service can be a snippet or logcat collection.
         """
         return any(
-            [self._snippet_clients, self._adb_logcat_process, self.sl4a])
+            [self._snippet_clients, self.services.is_any_alive, self.sl4a])
 
     @property
     def log_path(self):
@@ -608,11 +615,23 @@
         """Starts long running services on the android device, like adb logcat
         capture.
         """
-        try:
-            self.start_adb_logcat(clear_log)
-        except:
-            self.log.exception('Failed to start adb logcat!')
-            raise
+        configs = logcat.Config(clear_log=clear_log)
+        self.services.register('logcat', logcat.Logcat, configs)
+
+    def start_adb_logcat(self, clear_log=True):
+        """.. deprecated:: 1.8
+
+        Use `self.services.logcat.start` instead.
+        """
+        configs = logcat.Config(clear_log=clear_log)
+        self.services.logcat.start(configs)
+
+    def stop_adb_logcat(self):
+        """.. deprecated:: 1.8
+
+        Use `self.services.logcat.stop` instead.
+        """
+        self.services.logcat.stop()
 
     def stop_services(self):
         """Stops long running services on the Android device.
@@ -633,18 +652,9 @@
             client.stop_app()
             delattr(self, attr_name)
         self._snippet_clients = {}
-        self._stop_logcat_process()
+        self.services.unregister_all()
         return service_info
 
-    def _stop_logcat_process(self):
-        """Stops logcat process."""
-        if self._adb_logcat_process:
-            try:
-                self.stop_adb_logcat()
-            except:
-                self.log.exception('Failed to stop adb logcat.')
-            self._adb_logcat_process = None
-
     @contextlib.contextmanager
     def handle_reboot(self):
         """Properly manage the service life cycle when the device needs to
@@ -709,16 +719,11 @@
                   # context
                   ad.adb.wait_for_device(timeout=SOME_TIMEOUT)
         """
-        self._stop_logcat_process()
+        self.services.pause_all()
         # Only need to stop dispatcher because it continuously polling device
         # It's not necessary to stop snippet and sl4a.
         if self.sl4a:
             self.sl4a.stop_event_dispatcher()
-        # Clears cached adb content, so that the next time start_adb_logcat()
-        # won't produce duplicated logs to log file.
-        # This helps disconnection that caused by, e.g., USB off; at the
-        # cost of losing logs at disconnection caused by reboot.
-        self._clear_adb_log()
         try:
             yield
         finally:
@@ -746,9 +751,7 @@
 
     def _reconnect_to_services(self):
         """Reconnects to services after USB reconnected."""
-        # Do not clear device log at this time. Otherwise the log during USB
-        # disconnection will be lost.
-        self.start_services(clear_log=False)
+        self.services.resume_all()
         # Restore snippets.
         for attr_name, client in self._snippet_clients.items():
             client.restore_app_connection()
@@ -817,8 +820,7 @@
         model = self.adb.getprop('ro.build.product').lower()
         if model == 'sprout':
             return model
-        else:
-            return self.adb.getprop('ro.product.name').lower()
+        return self.adb.getprop('ro.product.name').lower()
 
     def load_config(self, config):
         """Add attributes to the AndroidDevice object based on config.
@@ -938,141 +940,6 @@
         # Unpack the 'ed' attribute for compatibility.
         self.ed = self.sl4a.ed
 
-    def _is_timestamp_in_range(self, target, begin_time, end_time):
-        low = mobly_logger.logline_timestamp_comparator(begin_time,
-                                                        target) <= 0
-        high = mobly_logger.logline_timestamp_comparator(end_time, target) >= 0
-        return low and high
-
-    def cat_adb_log(self, tag, begin_time):
-        """Takes an excerpt of the adb logcat log from a certain time point to
-        current time.
-
-        Args:
-            tag: An identifier of the time period, usualy the name of a test.
-            begin_time: Logline format timestamp of the beginning of the time
-                period.
-        """
-        if not self.adb_logcat_file_path:
-            raise DeviceError(
-                self,
-                'Attempting to cat adb log when none has been collected.')
-        end_time = mobly_logger.get_log_line_timestamp()
-        self.log.debug('Extracting adb log from logcat.')
-        adb_excerpt_path = os.path.join(self.log_path, 'AdbLogExcerpts')
-        utils.create_dir(adb_excerpt_path)
-        f_name = os.path.basename(self.adb_logcat_file_path)
-        out_name = f_name.replace('adblog,', '').replace('.txt', '')
-        out_name = ',%s,%s.txt' % (begin_time, out_name)
-        out_name = out_name.replace(':', '-')
-        tag_len = utils.MAX_FILENAME_LEN - len(out_name)
-        tag = tag[:tag_len]
-        out_name = tag + out_name
-        full_adblog_path = os.path.join(adb_excerpt_path, out_name)
-        with io.open(full_adblog_path, 'w', encoding='utf-8') as out:
-            in_file = self.adb_logcat_file_path
-            with io.open(
-                    in_file, 'r', encoding='utf-8', errors='replace') as f:
-                in_range = False
-                while True:
-                    line = None
-                    try:
-                        line = f.readline()
-                        if not line:
-                            break
-                    except:
-                        continue
-                    line_time = line[:mobly_logger.log_line_timestamp_len]
-                    if not mobly_logger.is_valid_logline_timestamp(line_time):
-                        continue
-                    if self._is_timestamp_in_range(line_time, begin_time,
-                                                   end_time):
-                        in_range = True
-                        if not line.endswith('\n'):
-                            line += '\n'
-                        out.write(line)
-                    else:
-                        if in_range:
-                            break
-
-    def _enable_logpersist(self):
-        """Attempts to enable logpersist daemon to persist logs."""
-        # Logpersist is only allowed on rootable devices because of excessive
-        # reads/writes for persisting logs.
-        if not self.is_rootable:
-            return
-
-        logpersist_warning = ('%s encountered an error enabling persistent'
-                              ' logs, logs may not get saved.')
-        # Android L and older versions do not have logpersist installed,
-        # so check that the logpersist scripts exists before trying to use
-        # them.
-        if not self.adb.has_shell_command('logpersist.start'):
-            logging.warning(logpersist_warning, self)
-            return
-
-        try:
-            # Disable adb log spam filter for rootable devices. Have to stop
-            # and clear settings first because 'start' doesn't support --clear
-            # option before Android N.
-            self.adb.shell('logpersist.stop --clear')
-            self.adb.shell('logpersist.start')
-        except adb.AdbError:
-            logging.warning(logpersist_warning, self)
-
-    def start_adb_logcat(self, clear_log=True):
-        """Starts a standing adb logcat collection in separate subprocesses and
-        save the logcat in a file.
-
-        This clears the previous cached logcat content on device.
-
-        Args:
-            clear: If True, clear device log before starting logcat.
-        """
-        if self._adb_logcat_process:
-            raise DeviceError(
-                self,
-                'Logcat thread is already running, cannot start another one.')
-        if clear_log:
-            try:
-                self._clear_adb_log()
-            except adb.AdbError as e:
-                # On Android O, the clear command fails due to a known bug.
-                # Catching this so we don't crash from this Android issue.
-                if "failed to clear" in e.stderr:
-                    self.log.warning(
-                        'Encountered known Android error to clear logcat.')
-                else:
-                    raise
-
-        self._enable_logpersist()
-
-        f_name = 'adblog,%s,%s.txt' % (self.model, self._normalized_serial)
-        utils.create_dir(self.log_path)
-        logcat_file_path = os.path.join(self.log_path, f_name)
-        try:
-            extra_params = self.adb_logcat_param
-        except AttributeError:
-            extra_params = ''
-        cmd = '"%s" -s %s logcat -v threadtime %s >> "%s"' % (adb.ADB,
-                                                              self.serial,
-                                                              extra_params,
-                                                              logcat_file_path)
-        process = utils.start_standing_subprocess(cmd, shell=True)
-        self._adb_logcat_process = process
-        self.adb_logcat_file_path = logcat_file_path
-
-    def stop_adb_logcat(self):
-        """Stops the adb logcat collection subprocess.
-
-        Raises:
-            DeviceError: raised if there's no adb logcat collection going on.
-        """
-        if not self._adb_logcat_process:
-            raise DeviceError(self, 'No ongoing adb logcat collection found.')
-        utils.stop_standing_subprocess(self._adb_logcat_process)
-        self._adb_logcat_process = None
-
     def take_bug_report(self,
                         test_name,
                         begin_time,
@@ -1125,10 +992,6 @@
         self.log.info('Bugreport for %s taken at %s.', test_name,
                       full_out_path)
 
-    def _clear_adb_log(self):
-        # Clears cached adb content.
-        self.adb.logcat('-c')
-
     def _terminate_sl4a(self):
         """Terminate the current sl4a session.
 
diff --git a/mobly/controllers/android_device_lib/errors.py b/mobly/controllers/android_device_lib/errors.py
index c5d889a..077841a 100644
--- a/mobly/controllers/android_device_lib/errors.py
+++ b/mobly/controllers/android_device_lib/errors.py
@@ -16,6 +16,8 @@
 
 from mobly import signals
 
+HIERARCHY_TOKEN = '::'
+
 
 class Error(signals.ControllerError):
     pass
@@ -23,6 +25,26 @@
 
 class DeviceError(Error):
     """Raised for errors specific to an AndroidDevice object."""
+
     def __init__(self, ad, msg):
-        new_msg = '%s %s' % (repr(ad), msg)
+        template = '%s %s'
+        # If the message starts with the hierarchy token, don't add the extra
+        # space.
+        if isinstance(msg, str) and msg.startswith(HIERARCHY_TOKEN):
+            template = '%s%s'
+        new_msg = template % (repr(ad), msg)
         super(DeviceError, self).__init__(new_msg)
+
+
+class ServiceError(DeviceError):
+    """Raised for errors specific to an AndroidDevice service.
+
+    A service is inherently associated with a device instance, so the service
+    error type is a subtype of `DeviceError`.
+    """
+    SERVICE_TYPE = None
+
+    def __init__(self, device, msg):
+        new_msg = '%sService<%s> %s' % (HIERARCHY_TOKEN, self.SERVICE_TYPE,
+                                        msg)
+        super(ServiceError, self).__init__(device, new_msg)
diff --git a/mobly/controllers/android_device_lib/service_manager.py b/mobly/controllers/android_device_lib/service_manager.py
new file mode 100644
index 0000000..f0a96c4
--- /dev/null
+++ b/mobly/controllers/android_device_lib/service_manager.py
@@ -0,0 +1,112 @@
+# Copyright 2018 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Module for the manager of services."""
+# TODO(xpconanfan: move the device errors to a more generic location so
+# other device controllers like iOS can share it.
+from mobly import expects
+from mobly.controllers.android_device_lib import errors
+
+
+class Error(errors.DeviceError):
+    """Root error type for this module."""
+
+
+class ServiceManager(object):
+    """Manager for services of AndroidDevice.
+
+    A service is a long running process that involves an Android device, like
+    adb logcat or Snippet.
+    """
+
+    def __init__(self, device):
+        self._service_objects = {}
+        self._device = device
+
+    @property
+    def is_any_alive(self):
+        """True if any service is alive; False otherwise."""
+        for service in self._service_objects.values():
+            if service.is_alive:
+                return True
+        return False
+
+    def register(self, alias, service_class, configs=None):
+        """Registers a service.
+
+        This will create a service instance, starts the service, and adds the
+        instance to the mananger.
+
+        Args:
+            alias: string, the alias for this instance.
+            service_class: class, the service class to instantiate.
+            configs: (optional) config object to pass to the service class's
+                constructor.
+        """
+        if alias in self._service_objects:
+            raise Error(
+                self._device,
+                'A service is already registered with alias "%s".' % alias)
+        service_obj = service_class(self._device, configs)
+        service_obj.start()
+        self._service_objects[alias] = service_obj
+
+    def unregister(self, alias):
+        """Unregisters a service instance.
+
+        Stops a service and removes it from the manager.
+
+        Args:
+            alias: string, the alias of the service instance to unregister.
+        """
+        if alias not in self._service_objects:
+            raise Error(self._device,
+                        'No service is registered with alias "%s".' % alias)
+        service_obj = self._service_objects.pop(alias)
+        if service_obj.is_alive:
+            with expects.expect_no_raises(
+                    'Failed to stop service instance "%s".' % alias):
+                service_obj.stop()
+
+    def unregister_all(self):
+        """Safely unregisters all active instances.
+
+        Errors occurred here will be recorded but not raised.
+        """
+        aliases = list(self._service_objects.keys())
+        for alias in aliases:
+            self.unregister(alias)
+
+    def pause_all(self):
+        """Pauses all active service instances."""
+        for alias, obj in self._service_objects.items():
+            if obj.is_alive:
+                with expects.expect_no_raises(
+                        'Failed to pause service "%s".' % alias):
+                    obj.pause()
+
+    def resume_all(self):
+        """Resumes all paused service instances."""
+        for alias, obj in self._service_objects.items():
+            if not obj.is_alive:
+                with expects.expect_no_raises(
+                        'Failed to pause service "%s".' % alias):
+                    obj.resume()
+
+    def __getattr__(self, name):
+        """Syntactic sugar to enable direct access of service objects by alias.
+
+        Args:
+            name: string, the alias a service object was registered under.
+        """
+        return self._service_objects[name]
diff --git a/mobly/controllers/android_device_lib/services/__init__.py b/mobly/controllers/android_device_lib/services/__init__.py
new file mode 100644
index 0000000..87da62d
--- /dev/null
+++ b/mobly/controllers/android_device_lib/services/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2018 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/mobly/controllers/android_device_lib/services/base_service.py b/mobly/controllers/android_device_lib/services/base_service.py
new file mode 100644
index 0000000..1806ae2
--- /dev/null
+++ b/mobly/controllers/android_device_lib/services/base_service.py
@@ -0,0 +1,87 @@
+# Copyright 2018 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Module for the BaseService."""
+
+
+class BaseService(object):
+    """Base class of a Mobly AndroidDevice service.
+
+    This class defines the interface for Mobly's AndroidDevice service.
+    """
+
+    def __init__(self, device, configs=None):
+        """Constructor of the class.
+
+        Args:
+          device: the device object this service is associated with.
+          config: optional configuration defined by the author of the service
+              class.
+        """
+        self._device = device
+        self._configs = configs
+
+    @property
+    def is_alive(self):
+        """True if the service is active; False otherwise."""
+        raise NotImplementedError('"is_alive" is a required service property.')
+
+    def start(self, configs=None):
+        """Starts the service.
+
+        Args:
+            configs: optional configs to be passed for startup.
+        """
+        raise NotImplementedError('"start" is a required service method.')
+
+    def stop(self):
+        """Stops the service and cleans up all resources.
+
+        This method should handle any error and not throw.
+        """
+        raise NotImplementedError('"stop" is a required service method.')
+
+    def pause(self):
+        """Pauses a service temporarily.
+
+        For when the Python service object needs to temporarily lose connection
+        to the device without shutting down the service running on the actual
+        device.
+
+        This is relevant when a service needs to maintain a constant connection
+        to the device and the connection is lost if USB connection to the
+        device is disrupted.
+
+        E.g. a services that utilizes a socket connection over adb port
+        forwarding would need to implement this for the situation where the USB
+        connection to the device will be temporarily cut, but the device is not
+        rebooted.
+
+        For more context, see:
+        `mobly.controllers.android_device.AndroidDevice.handle_usb_disconnect`
+
+        If not implemented, we assume the service is not sensitive to device
+        disconnect, and `stop` will be called by default.
+        """
+        self.stop()
+
+    def resume(self):
+        """Resumes a paused service.
+
+        Same context as the `pause` method. This should resume the service
+        after the connection to the device has been re-established.
+
+        If not implemented, we assume the service is not sensitive to device
+        disconnect, and `start` will be called by default.
+        """
+        self.start(configs=self._configs)
diff --git a/mobly/controllers/android_device_lib/services/logcat.py b/mobly/controllers/android_device_lib/services/logcat.py
new file mode 100644
index 0000000..5db5a6c
--- /dev/null
+++ b/mobly/controllers/android_device_lib/services/logcat.py
@@ -0,0 +1,201 @@
+# Copyright 2018 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import copy
+import io
+import logging
+import os
+
+from mobly import logger as mobly_logger
+from mobly import utils
+from mobly.controllers.android_device_lib import adb
+from mobly.controllers.android_device_lib import errors
+from mobly.controllers.android_device_lib.services import base_service
+
+
+class Error(errors.ServiceError):
+    """Root error type for logcat service."""
+    SERVICE_TYPE = 'Logcat'
+
+
+class Config(object):
+    """Config object for logcat service.
+
+    Attributes:
+        clear_log: bool, clears the logcat before collection if True.
+        logcat_params: string, extra params to be added to logcat command.
+    """
+
+    def __init__(self, params=None, clear_log=True):
+        self.clear_log = clear_log
+        self.logcat_params = params if params else ''
+
+
+class Logcat(base_service.BaseService):
+    """Android logcat service for Mobly's AndroidDevice controller."""
+
+    def __init__(self, android_device, configs=None):
+        super(Logcat, self).__init__(android_device, configs)
+        self._ad = android_device
+        self._adb_logcat_process = None
+        self.adb_logcat_file_path = None
+        self._configs = configs if configs else Config()
+
+    def _enable_logpersist(self):
+        """Attempts to enable logpersist daemon to persist logs."""
+        # Logpersist is only allowed on rootable devices because of excessive
+        # reads/writes for persisting logs.
+        if not self._ad.is_rootable:
+            return
+
+        logpersist_warning = ('%s encountered an error enabling persistent'
+                              ' logs, logs may not get saved.')
+        # Android L and older versions do not have logpersist installed,
+        # so check that the logpersist scripts exists before trying to use
+        # them.
+        if not self._ad.adb.has_shell_command('logpersist.start'):
+            logging.warning(logpersist_warning, self)
+            return
+
+        try:
+            # Disable adb log spam filter for rootable devices. Have to stop
+            # and clear settings first because 'start' doesn't support --clear
+            # option before Android N.
+            self._ad.adb.shell('logpersist.stop --clear')
+            self._ad.adb.shell('logpersist.start')
+        except adb.AdbError:
+            logging.warning(logpersist_warning, self)
+
+    def _is_timestamp_in_range(self, target, begin_time, end_time):
+        low = mobly_logger.logline_timestamp_comparator(begin_time,
+                                                        target) <= 0
+        high = mobly_logger.logline_timestamp_comparator(end_time, target) >= 0
+        return low and high
+
+    @property
+    def is_alive(self):
+        return True if self._adb_logcat_process else False
+
+    def clear_adb_log(self):
+        # Clears cached adb content.
+        self._ad.adb.logcat('-c')
+
+    def cat_adb_log(self, tag, begin_time):
+        """Takes an excerpt of the adb logcat log from a certain time point to
+        current time.
+
+        Args:
+            tag: An identifier of the time period, usualy the name of a test.
+            begin_time: Logline format timestamp of the beginning of the time
+                period.
+        """
+        if not self.adb_logcat_file_path:
+            raise Error(
+                self._ad,
+                'Attempting to cat adb log when none has been collected.')
+        end_time = mobly_logger.get_log_line_timestamp()
+        self._ad.log.debug('Extracting adb log from logcat.')
+        adb_excerpt_path = os.path.join(self._ad.log_path, 'AdbLogExcerpts')
+        utils.create_dir(adb_excerpt_path)
+        f_name = os.path.basename(self.adb_logcat_file_path)
+        out_name = f_name.replace('adblog,', '').replace('.txt', '')
+        out_name = ',%s,%s.txt' % (begin_time, out_name)
+        out_name = out_name.replace(':', '-')
+        tag_len = utils.MAX_FILENAME_LEN - len(out_name)
+        tag = tag[:tag_len]
+        out_name = tag + out_name
+        full_adblog_path = os.path.join(adb_excerpt_path, out_name)
+        with io.open(full_adblog_path, 'w', encoding='utf-8') as out:
+            in_file = self.adb_logcat_file_path
+            with io.open(
+                    in_file, 'r', encoding='utf-8', errors='replace') as f:
+                in_range = False
+                while True:
+                    line = None
+                    try:
+                        line = f.readline()
+                        if not line:
+                            break
+                    except:
+                        continue
+                    line_time = line[:mobly_logger.log_line_timestamp_len]
+                    if not mobly_logger.is_valid_logline_timestamp(line_time):
+                        continue
+                    if self._is_timestamp_in_range(line_time, begin_time,
+                                                   end_time):
+                        in_range = True
+                        if not line.endswith('\n'):
+                            line += '\n'
+                        out.write(line)
+                    else:
+                        if in_range:
+                            break
+
+    def start(self, configs=None):
+        """Starts a standing adb logcat collection.
+
+        The collection runs in a separate subprocess and saves logs in a file.
+
+        Args:
+            configs: Conifg object.
+        """
+        if self._adb_logcat_process:
+            raise Error(
+                self._ad,
+                'Logcat thread is already running, cannot start another one.')
+        configs = configs if configs else self._configs
+        if configs.clear_log:
+            self.clear_adb_log()
+
+        self._enable_logpersist()
+
+        f_name = 'adblog,%s,%s.txt' % (self._ad.model,
+                                       self._ad._normalized_serial)
+        utils.create_dir(self._ad.log_path)
+        logcat_file_path = os.path.join(self._ad.log_path, f_name)
+        cmd = '"%s" -s %s logcat -v threadtime %s >> "%s"' % (
+            adb.ADB, self._ad.serial, configs.logcat_params, logcat_file_path)
+        process = utils.start_standing_subprocess(cmd, shell=True)
+        self._adb_logcat_process = process
+        self.adb_logcat_file_path = logcat_file_path
+
+    def stop(self):
+        """Stops the adb logcat service."""
+        if not self._adb_logcat_process:
+            return
+        try:
+            utils.stop_standing_subprocess(self._adb_logcat_process)
+        except:
+            self._ad.log.exception('Failed to stop adb logcat.')
+        self._adb_logcat_process = None
+
+    def pause(self):
+        """Pauses logcat for usb disconnect."""
+        self.stop()
+        # Clears cached adb content, so that the next time start_adb_logcat()
+        # won't produce duplicated logs to log file.
+        # This helps disconnection that caused by, e.g., USB off; at the
+        # cost of losing logs at disconnection caused by reboot.
+        self.clear_adb_log()
+
+    def resume(self):
+        """Resumes a paused logcat service.
+
+        Args:
+            configs: Not used.
+        """
+        # Do not clear device log at this time. Otherwise the log during USB
+        # disconnection will be lost.
+        resume_configs = copy.copy(self._configs)
+        resume_configs.clear_log = False
+        self.start(resume_configs)
diff --git a/tests/mobly/controllers/android_device_lib/errors_test.py b/tests/mobly/controllers/android_device_lib/errors_test.py
new file mode 100755
index 0000000..d88c254
--- /dev/null
+++ b/tests/mobly/controllers/android_device_lib/errors_test.py
@@ -0,0 +1,49 @@
+# Copyright 2018 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Unit tests for Mobly android_device_lib.errors."""
+import mock
+
+from future.tests.base import unittest
+
+from mobly.controllers.android_device_lib import errors
+
+
+class ErrorsTest(unittest.TestCase):
+    def test_device_error(self):
+        device = mock.MagicMock()
+        device.__repr__ = lambda _: '[MockDevice]'
+        exception = errors.DeviceError(device, 'Some error message.')
+        self.assertEqual(str(exception), '[MockDevice] Some error message.')
+
+    def test_service_error(self):
+        device = mock.MagicMock()
+        device.__repr__ = lambda _: '[MockDevice]'
+        exception = errors.ServiceError(device, 'Some error message.')
+        self.assertEqual(
+            str(exception), '[MockDevice]::Service<None> Some error message.')
+
+    def test_subclass_service_error(self):
+        class Error(errors.ServiceError):
+            SERVICE_TYPE = 'SomeType'
+
+        device = mock.MagicMock()
+        device.__repr__ = lambda _: '[MockDevice]'
+        exception = Error(device, 'Some error message.')
+        self.assertEqual(
+            str(exception),
+            '[MockDevice]::Service<SomeType> Some error message.')
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/mobly/controllers/android_device_lib/service_manager_test.py b/tests/mobly/controllers/android_device_lib/service_manager_test.py
new file mode 100755
index 0000000..1d4c31c
--- /dev/null
+++ b/tests/mobly/controllers/android_device_lib/service_manager_test.py
@@ -0,0 +1,189 @@
+# Copyright 2018 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Unit tests for Mobly's ServiceManager."""
+import mock
+
+from future.tests.base import unittest
+
+from mobly.controllers.android_device_lib import service_manager
+from mobly.controllers.android_device_lib.services import base_service
+
+
+class MockService(base_service.BaseService):
+    def __init__(self, device, configs=None):
+        self._device = device
+        self._configs = configs
+        self._alive = False
+
+    @property
+    def is_alive(self):
+        return self._alive
+
+    def start(self, configs=None):
+        self._alive = True
+        self._device.start()
+
+    def stop(self):
+        self._alive = False
+        self._device.stop()
+
+
+class ServiceManagerTest(unittest.TestCase):
+    def test_service_manager_instantiation(self):
+        mock_device = mock.MagicMock()
+        manager = service_manager.ServiceManager(mock_device)
+
+    def test_register(self):
+        mock_device = mock.MagicMock()
+        manager = service_manager.ServiceManager(mock_device)
+        manager.register('mock_service', MockService)
+        service = manager.mock_service
+        self.assertTrue(service)
+        self.assertTrue(service.is_alive)
+        self.assertTrue(manager.is_any_alive)
+
+    def test_register_dup_alias(self):
+        mock_device = mock.MagicMock()
+        manager = service_manager.ServiceManager(mock_device)
+        manager.register('mock_service', MockService)
+        msg = '.* A service is already registered with alias "mock_service"'
+        with self.assertRaisesRegex(service_manager.Error, msg):
+            manager.register('mock_service', MockService)
+
+    def test_unregister(self):
+        mock_device = mock.MagicMock()
+        manager = service_manager.ServiceManager(mock_device)
+        manager.register('mock_service', MockService)
+        service = manager.mock_service
+        manager.unregister('mock_service')
+        self.assertFalse(manager.is_any_alive)
+        self.assertFalse(service.is_alive)
+
+    def test_unregister_non_existent(self):
+        mock_device = mock.MagicMock()
+        manager = service_manager.ServiceManager(mock_device)
+        with self.assertRaisesRegex(
+                service_manager.Error,
+                '.* No service is registered with alias "mock_service"'):
+            manager.unregister('mock_service')
+
+    @mock.patch('mobly.expects.expect_no_raises')
+    def test_unregister_handle_error_from_stop(self, mock_expect_func):
+        mock_device = mock.MagicMock()
+        manager = service_manager.ServiceManager(mock_device)
+        manager.register('mock_service', MockService)
+        service = manager.mock_service
+        service._device.stop.side_deffect = Exception(
+            'Something failed in stop.')
+        manager.unregister('mock_service')
+        mock_expect_func.assert_called_once_with(
+            'Failed to stop service instance "mock_service".')
+
+    def test_unregister_all(self):
+        mock_device = mock.MagicMock()
+        manager = service_manager.ServiceManager(mock_device)
+        manager.register('mock_service1', MockService)
+        manager.register('mock_service2', MockService)
+        service1 = manager.mock_service1
+        service2 = manager.mock_service2
+        manager.unregister_all()
+        self.assertFalse(manager.is_any_alive)
+        self.assertFalse(service1.is_alive)
+        self.assertFalse(service2.is_alive)
+
+    def test_unregister_all(self):
+        mock_device = mock.MagicMock()
+        manager = service_manager.ServiceManager(mock_device)
+        manager.register('mock_service1', MockService)
+        manager.register('mock_service2', MockService)
+        service1 = manager.mock_service1
+        service2 = manager.mock_service2
+        manager.unregister_all()
+        self.assertFalse(manager.is_any_alive)
+        self.assertFalse(service1.is_alive)
+        self.assertFalse(service2.is_alive)
+
+    def test_unregister_all_with_some_failed(self):
+        mock_device = mock.MagicMock()
+        manager = service_manager.ServiceManager(mock_device)
+        manager.register('mock_service1', MockService)
+        manager.register('mock_service2', MockService)
+        service1 = manager.mock_service1
+        service1._device.stop.side_deffect = Exception(
+            'Something failed in stop.')
+        service2 = manager.mock_service2
+        manager.unregister_all()
+        self.assertFalse(manager.is_any_alive)
+        self.assertFalse(service1.is_alive)
+        self.assertFalse(service2.is_alive)
+
+    def test_pause_all(self):
+        mock_device = mock.MagicMock()
+        manager = service_manager.ServiceManager(mock_device)
+        manager.register('mock_service1', MockService)
+        manager.register('mock_service2', MockService)
+        service1 = manager.mock_service1
+        service2 = manager.mock_service2
+        manager.pause_all()
+        self.assertFalse(manager.is_any_alive)
+        self.assertFalse(service1.is_alive)
+        self.assertFalse(service2.is_alive)
+
+    def test_pause_all_with_some_failed(self):
+        mock_device = mock.MagicMock()
+        manager = service_manager.ServiceManager(mock_device)
+        manager.register('mock_service1', MockService)
+        manager.register('mock_service2', MockService)
+        service1 = manager.mock_service1
+        service1._device.pause.side_deffect = Exception(
+            'Something failed in stop.')
+        service2 = manager.mock_service2
+        manager.pause_all()
+        self.assertFalse(manager.is_any_alive)
+        # state of service1 is undefined
+        # verify state of service2
+        self.assertFalse(service2.is_alive)
+
+    def test_resume_all(self):
+        mock_device = mock.MagicMock()
+        manager = service_manager.ServiceManager(mock_device)
+        manager.register('mock_service1', MockService)
+        manager.register('mock_service2', MockService)
+        service1 = manager.mock_service1
+        service2 = manager.mock_service2
+        manager.pause_all()
+        manager.resume_all()
+        self.assertTrue(manager.is_any_alive)
+        self.assertTrue(service1.is_alive)
+        self.assertTrue(service2.is_alive)
+
+    def test_resume_all_with_some_failed(self):
+        mock_device = mock.MagicMock()
+        manager = service_manager.ServiceManager(mock_device)
+        manager.register('mock_service1', MockService)
+        manager.register('mock_service2', MockService)
+        service1 = manager.mock_service1
+        service1._device.resume.side_deffect = Exception(
+            'Something failed in stop.')
+        service2 = manager.mock_service2
+        manager.pause_all()
+        manager.resume_all()
+        self.assertTrue(manager.is_any_alive)
+        # state of service1 is undefined
+        # verify state of service2
+        self.assertTrue(service2.is_alive)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/mobly/controllers/android_device_lib/services/__init__.py b/tests/mobly/controllers/android_device_lib/services/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/mobly/controllers/android_device_lib/services/__init__.py
diff --git a/tests/mobly/controllers/android_device_lib/services/logcat_test.py b/tests/mobly/controllers/android_device_lib/services/logcat_test.py
new file mode 100755
index 0000000..52ec2b2
--- /dev/null
+++ b/tests/mobly/controllers/android_device_lib/services/logcat_test.py
@@ -0,0 +1,438 @@
+# Copyright 2018 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import io
+import logging
+import mock
+import os
+import shutil
+import tempfile
+
+from future.tests.base import unittest
+
+from mobly import utils
+from mobly.controllers import android_device
+from mobly.controllers.android_device_lib import adb
+from mobly.controllers.android_device_lib.services import logcat
+
+from tests.lib import mock_android_device
+
+# The expected result of the cat adb operation.
+MOCK_ADB_LOGCAT_CAT_RESULT = [
+    '02-29 14:02:21.456  4454  Something\n',
+    '02-29 14:02:21.789  4454  Something again\n'
+]
+# A mocked piece of adb logcat output.
+MOCK_ADB_LOGCAT = (u'02-29 14:02:19.123  4454  Nothing\n'
+                   u'%s'
+                   u'02-29 14:02:22.123  4454  Something again and again\n'
+                   ) % u''.join(MOCK_ADB_LOGCAT_CAT_RESULT)
+# The expected result of the cat adb operation.
+MOCK_ADB_UNICODE_LOGCAT_CAT_RESULT = [
+    '02-29 14:02:21.456  4454  Something \u901a\n',
+    '02-29 14:02:21.789  4454  Something again\n'
+]
+# A mocked piece of adb logcat output.
+MOCK_ADB_UNICODE_LOGCAT = (
+    u'02-29 14:02:19.123  4454  Nothing\n'
+    u'%s'
+    u'02-29 14:02:22.123  4454  Something again and again\n'
+) % u''.join(MOCK_ADB_UNICODE_LOGCAT_CAT_RESULT)
+
+# Mock start and end time of the adb cat.
+MOCK_ADB_LOGCAT_BEGIN_TIME = '02-29 14:02:20.123'
+MOCK_ADB_LOGCAT_END_TIME = '02-29 14:02:22.000'
+
+# Mock AdbError for missing logpersist scripts
+MOCK_LOGPERSIST_STOP_MISSING_ADB_ERROR = adb.AdbError(
+    'logpersist.stop --clear', '',
+    '/system/bin/sh: logpersist.stop: not found', 0)
+MOCK_LOGPERSIST_START_MISSING_ADB_ERROR = adb.AdbError(
+    'logpersist.start --clear', '',
+    '/system/bin/sh: logpersist.stop: not found', 0)
+
+
+class LogcatTest(unittest.TestCase):
+    """Tests for Logcat service and its integration with AndroidDevice."""
+
+    def setUp(self):
+        # Set log_path to logging since mobly logger setup is not called.
+        if not hasattr(logging, 'log_path'):
+            setattr(logging, 'log_path', '/tmp/logs')
+        # Creates a temp dir to be used by tests in this test class.
+        self.tmp_dir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        """Removes the temp dir.
+        """
+        shutil.rmtree(self.tmp_dir)
+
+    @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'))
+    @mock.patch('mobly.utils.create_dir')
+    @mock.patch(
+        'mobly.utils.start_standing_subprocess', return_value='process')
+    @mock.patch('mobly.utils.stop_standing_subprocess')
+    def test_logcat_service_start_and_stop(self, stop_proc_mock,
+                                           start_proc_mock, creat_dir_mock,
+                                           FastbootProxy, MockAdbProxy):
+        """Verifies the steps of collecting adb logcat on an AndroidDevice
+        object, including various function calls and the expected behaviors of
+        the calls.
+        """
+        mock_serial = '1'
+        ad = android_device.AndroidDevice(serial=mock_serial)
+        logcat_service = logcat.Logcat(ad)
+        logcat_service.start()
+        # Verify start did the correct operations.
+        self.assertTrue(logcat_service._adb_logcat_process)
+        expected_log_path = os.path.join(logging.log_path,
+                                         'AndroidDevice%s' % ad.serial,
+                                         'adblog,fakemodel,%s.txt' % ad.serial)
+        creat_dir_mock.assert_called_with(os.path.dirname(expected_log_path))
+        adb_cmd = '"adb" -s %s logcat -v threadtime  >> %s'
+        start_proc_mock.assert_called_with(
+            adb_cmd % (ad.serial, '"%s"' % expected_log_path), shell=True)
+        self.assertEqual(logcat_service.adb_logcat_file_path,
+                         expected_log_path)
+        expected_msg = (
+            'Logcat thread is already running, cannot start another'
+            ' one.')
+        # Expect error if start is called back to back.
+        with self.assertRaisesRegex(logcat.Error, expected_msg):
+            logcat_service.start()
+        # Verify stop did the correct operations.
+        logcat_service.stop()
+        stop_proc_mock.assert_called_with('process')
+        self.assertIsNone(logcat_service._adb_logcat_process)
+        self.assertEqual(logcat_service.adb_logcat_file_path,
+                         expected_log_path)
+
+    @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'))
+    @mock.patch('mobly.utils.create_dir')
+    @mock.patch(
+        'mobly.utils.start_standing_subprocess', return_value='process')
+    @mock.patch('mobly.utils.stop_standing_subprocess')
+    @mock.patch(
+        'mobly.controllers.android_device_lib.services.logcat.Logcat.clear_adb_log',
+        return_value=mock_android_device.MockAdbProxy('1'))
+    def test_logcat_service_pause_and_resume(
+            self, clear_adb_mock, stop_proc_mock, start_proc_mock,
+            creat_dir_mock, FastbootProxy, MockAdbProxy):
+        mock_serial = '1'
+        ad = android_device.AndroidDevice(serial=mock_serial)
+        logcat_service = logcat.Logcat(ad)
+        logcat_service.start()
+        clear_adb_mock.assert_called_once_with()
+        self.assertTrue(logcat_service.is_alive)
+        logcat_service.pause()
+        self.assertFalse(logcat_service.is_alive)
+        stop_proc_mock.assert_called_with('process')
+        self.assertIsNone(logcat_service._adb_logcat_process)
+        clear_adb_mock.reset_mock()
+        logcat_service.resume()
+        self.assertTrue(logcat_service.is_alive)
+        clear_adb_mock.assert_not_called()
+
+    @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'))
+    @mock.patch('mobly.utils.create_dir')
+    @mock.patch(
+        'mobly.utils.start_standing_subprocess', return_value='process')
+    @mock.patch('mobly.utils.stop_standing_subprocess')
+    def test_logcat_service_take_logcat_with_extra_params(
+            self, stop_proc_mock, start_proc_mock, creat_dir_mock,
+            FastbootProxy, MockAdbProxy):
+        """Verifies the steps of collecting adb logcat on an AndroidDevice
+        object, including various function calls and the expected behaviors of
+        the calls.
+        """
+        mock_serial = '1'
+        ad = android_device.AndroidDevice(serial=mock_serial)
+        configs = logcat.Config()
+        configs.logcat_params = '-b radio'
+        logcat_service = logcat.Logcat(ad, configs)
+        logcat_service.start()
+        # Verify start did the correct operations.
+        self.assertTrue(logcat_service._adb_logcat_process)
+        expected_log_path = os.path.join(logging.log_path,
+                                         'AndroidDevice%s' % ad.serial,
+                                         'adblog,fakemodel,%s.txt' % ad.serial)
+        creat_dir_mock.assert_called_with(os.path.dirname(expected_log_path))
+        adb_cmd = '"adb" -s %s logcat -v threadtime -b radio >> %s'
+        start_proc_mock.assert_called_with(
+            adb_cmd % (ad.serial, '"%s"' % expected_log_path), shell=True)
+        self.assertEqual(logcat_service.adb_logcat_file_path,
+                         expected_log_path)
+
+    @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'))
+    @mock.patch('mobly.utils.create_dir')
+    @mock.patch(
+        'mobly.utils.start_standing_subprocess', return_value='process')
+    @mock.patch('mobly.utils.stop_standing_subprocess')
+    def test_logcat_service_take_logcat_with_logcat_params_override_in_start(
+            self, stop_proc_mock, start_proc_mock, creat_dir_mock,
+            FastbootProxy, MockAdbProxy):
+        """Verifies the steps of collecting adb logcat on an AndroidDevice
+        object, including various function calls and the expected behaviors of
+        the calls.
+        """
+        mock_serial = '1'
+        ad = android_device.AndroidDevice(serial=mock_serial)
+        configs = logcat.Config()
+        configs.logcat_params = '-b radio'
+        logcat_service = logcat.Logcat(ad, configs)
+        new_configs = logcat.Config()
+        new_configs.logcat_params = '-b something_else'
+        logcat_service.start(configs=new_configs)
+        # Verify start did the correct operations.
+        self.assertTrue(logcat_service._adb_logcat_process)
+        expected_log_path = os.path.join(logging.log_path,
+                                         'AndroidDevice%s' % ad.serial,
+                                         'adblog,fakemodel,%s.txt' % ad.serial)
+        creat_dir_mock.assert_called_with(os.path.dirname(expected_log_path))
+        adb_cmd = '"adb" -s %s logcat -v threadtime -b something_else >> %s'
+        start_proc_mock.assert_called_with(
+            adb_cmd % (ad.serial, '"%s"' % expected_log_path), shell=True)
+        self.assertEqual(logcat_service.adb_logcat_file_path,
+                         expected_log_path)
+
+    @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_logcat_service_instantiation(self, MockFastboot, MockAdbProxy):
+        """Verifies the AndroidDevice object's basic attributes are correctly
+        set after instantiation.
+        """
+        mock_serial = 1
+        ad = android_device.AndroidDevice(serial=mock_serial)
+        logcat_service = logcat.Logcat(ad)
+        self.assertIsNone(logcat_service._adb_logcat_process)
+        self.assertIsNone(logcat_service.adb_logcat_file_path)
+
+    @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'))
+    @mock.patch(
+        'mobly.utils.start_standing_subprocess', return_value='process')
+    @mock.patch('mobly.utils.stop_standing_subprocess')
+    @mock.patch(
+        'mobly.logger.get_log_line_timestamp',
+        return_value=MOCK_ADB_LOGCAT_END_TIME)
+    def test_logcat_service_cat_adb_log(self, mock_timestamp_getter,
+                                        stop_proc_mock, start_proc_mock,
+                                        FastbootProxy, MockAdbProxy):
+        """Verifies that AndroidDevice.cat_adb_log loads the correct adb log
+        file, locates the correct adb log lines within the given time range,
+        and writes the lines to the correct output file.
+        """
+        mock_serial = '1'
+        ad = android_device.AndroidDevice(serial=mock_serial)
+        logcat_service = logcat.Logcat(ad)
+        logcat_service._enable_logpersist()
+        # Direct the log path of the ad to a temp dir to avoid racing.
+        logcat_service._ad._log_path = self.tmp_dir
+        # Expect error if attempted to cat adb log before starting adb logcat.
+        expected_msg = ('.* Attempting to cat adb log when none'
+                        ' has been collected.')
+        with self.assertRaisesRegex(logcat.Error, expected_msg):
+            logcat_service.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
+        logcat_service.start()
+        utils.create_dir(ad.log_path)
+        mock_adb_log_path = os.path.join(ad.log_path, 'adblog,%s,%s.txt' %
+                                         (ad.model, ad.serial))
+        with io.open(mock_adb_log_path, 'w', encoding='utf-8') as f:
+            f.write(MOCK_ADB_LOGCAT)
+        logcat_service.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
+        cat_file_path = os.path.join(
+            ad.log_path, 'AdbLogExcerpts',
+            ('some_test,02-29 14-02-20.123,%s,%s.txt') % (ad.model, ad.serial))
+        with io.open(cat_file_path, 'r', encoding='utf-8') as f:
+            actual_cat = f.read()
+        self.assertEqual(actual_cat, ''.join(MOCK_ADB_LOGCAT_CAT_RESULT))
+        # Stops adb logcat.
+        logcat_service.stop()
+
+    @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'))
+    @mock.patch(
+        'mobly.utils.start_standing_subprocess', return_value='process')
+    @mock.patch('mobly.utils.stop_standing_subprocess')
+    @mock.patch(
+        'mobly.logger.get_log_line_timestamp',
+        return_value=MOCK_ADB_LOGCAT_END_TIME)
+    def test_logcat_service_cat_adb_log_with_unicode(
+            self, mock_timestamp_getter, stop_proc_mock, start_proc_mock,
+            FastbootProxy, MockAdbProxy):
+        """Verifies that AndroidDevice.cat_adb_log loads the correct adb log
+        file, locates the correct adb log lines within the given time range,
+        and writes the lines to the correct output file.
+        """
+        mock_serial = '1'
+        ad = android_device.AndroidDevice(serial=mock_serial)
+        logcat_service = logcat.Logcat(ad)
+        logcat_service._enable_logpersist()
+        # Direct the log path of the ad to a temp dir to avoid racing.
+        logcat_service._ad._log_path = self.tmp_dir
+        # Expect error if attempted to cat adb log before starting adb logcat.
+        expected_msg = ('.* Attempting to cat adb log when none'
+                        ' has been collected.')
+        with self.assertRaisesRegex(logcat.Error, expected_msg):
+            logcat_service.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
+        logcat_service.start()
+        utils.create_dir(ad.log_path)
+        mock_adb_log_path = os.path.join(ad.log_path, 'adblog,%s,%s.txt' %
+                                         (ad.model, ad.serial))
+        with io.open(mock_adb_log_path, 'w', encoding='utf-8') as f:
+            f.write(MOCK_ADB_UNICODE_LOGCAT)
+        logcat_service.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
+        cat_file_path = os.path.join(
+            ad.log_path, 'AdbLogExcerpts',
+            ('some_test,02-29 14-02-20.123,%s,%s.txt') % (ad.model, ad.serial))
+        with io.open(cat_file_path, 'r', encoding='utf-8') as f:
+            actual_cat = f.read()
+        self.assertEqual(actual_cat,
+                         ''.join(MOCK_ADB_UNICODE_LOGCAT_CAT_RESULT))
+        # Stops adb logcat.
+        logcat_service.stop()
+
+    @mock.patch(
+        'mobly.controllers.android_device_lib.adb.AdbProxy',
+        return_value=mock.MagicMock())
+    @mock.patch(
+        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+        return_value=mock_android_device.MockFastbootProxy('1'))
+    def test_logcat_service__enable_logpersist_with_logpersist(
+            self, MockFastboot, MockAdbProxy):
+        mock_serial = '1'
+        mock_adb_proxy = MockAdbProxy.return_value
+        # Set getprop to return '1' to indicate the device is rootable.
+        mock_adb_proxy.getprop.return_value = '1'
+        mock_adb_proxy.has_shell_command.side_effect = lambda command: {
+            'logpersist.start': True,
+            'logpersist.stop': True, }[command]
+        ad = android_device.AndroidDevice(serial=mock_serial)
+        logcat_service = logcat.Logcat(ad)
+        logcat_service._enable_logpersist()
+        mock_adb_proxy.shell.assert_has_calls([
+            mock.call('logpersist.stop --clear'),
+            mock.call('logpersist.start'),
+        ])
+
+    @mock.patch(
+        'mobly.controllers.android_device_lib.adb.AdbProxy',
+        return_value=mock.MagicMock())
+    @mock.patch(
+        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+        return_value=mock_android_device.MockFastbootProxy('1'))
+    def test_logcat_service__enable_logpersist_with_missing_all_logpersist(
+            self, MockFastboot, MockAdbProxy):
+        def adb_shell_helper(command):
+            if command == 'logpersist.start':
+                raise MOCK_LOGPERSIST_START_MISSING_ADB_ERROR
+            elif command == 'logpersist.stop --clear':
+                raise MOCK_LOGPERSIST_STOP_MISSING_ADB_ERROR
+            else:
+                return ''
+
+        mock_serial = '1'
+        mock_adb_proxy = MockAdbProxy.return_value
+        mock_adb_proxy.getprop.return_value = 'userdebug'
+        mock_adb_proxy.has_shell_command.side_effect = lambda command: {
+            'logpersist.start': False,
+            'logpersist.stop': False, }[command]
+        mock_adb_proxy.shell.side_effect = adb_shell_helper
+        ad = android_device.AndroidDevice(serial=mock_serial)
+        logcat_service = logcat.Logcat(ad)
+        logcat_service._enable_logpersist()
+
+    @mock.patch(
+        'mobly.controllers.android_device_lib.adb.AdbProxy',
+        return_value=mock.MagicMock())
+    @mock.patch(
+        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+        return_value=mock_android_device.MockFastbootProxy('1'))
+    def test_logcat_service__enable_logpersist_with_missing_logpersist_stop(
+            self, MockFastboot, MockAdbProxy):
+        def adb_shell_helper(command):
+            if command == 'logpersist.stop --clear':
+                raise MOCK_LOGPERSIST_STOP_MISSING_ADB_ERROR
+            else:
+                return ''
+
+        mock_serial = '1'
+        mock_adb_proxy = MockAdbProxy.return_value
+        mock_adb_proxy.getprop.return_value = 'userdebug'
+        mock_adb_proxy.has_shell_command.side_effect = lambda command: {
+            'logpersist.start': True,
+            'logpersist.stop': False, }[command]
+        mock_adb_proxy.shell.side_effect = adb_shell_helper
+        ad = android_device.AndroidDevice(serial=mock_serial)
+        logcat_service = logcat.Logcat(ad)
+        logcat_service._enable_logpersist()
+
+    @mock.patch(
+        'mobly.controllers.android_device_lib.adb.AdbProxy',
+        return_value=mock.MagicMock())
+    @mock.patch('mobly.utils.stop_standing_subprocess')
+    def test_logcat_service__enable_logpersist_with_missing_logpersist_start(
+            self, MockFastboot, MockAdbProxy):
+        def adb_shell_helper(command):
+            if command == 'logpersist.start':
+                raise MOCK_LOGPERSIST_START_MISSING_ADB_ERROR
+            else:
+                return ''
+
+        mock_serial = '1'
+        mock_adb_proxy = MockAdbProxy.return_value
+        mock_adb_proxy.getprop.return_value = 'userdebug'
+        mock_adb_proxy.has_shell_command.side_effect = lambda command: {
+            'logpersist.start': False,
+            'logpersist.stop': True, }[command]
+        mock_adb_proxy.shell.side_effect = adb_shell_helper
+        ad = android_device.AndroidDevice(serial=mock_serial)
+        logcat_service = logcat.Logcat(ad)
+        logcat_service._enable_logpersist()
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/mobly/controllers/android_device_test.py b/tests/mobly/controllers/android_device_test.py
index d3221a4..ffb6a94 100755
--- a/tests/mobly/controllers/android_device_test.py
+++ b/tests/mobly/controllers/android_device_test.py
@@ -25,54 +25,20 @@
 
 from future.tests.base import unittest
 
-from mobly import utils
 from mobly.controllers import android_device
 from mobly.controllers.android_device_lib import adb
 from mobly.controllers.android_device_lib import snippet_client
+from mobly.controllers.android_device_lib.services import base_service
+from mobly.controllers.android_device_lib.services import logcat
 
 from tests.lib import mock_android_device
 
-# Mock log path for a test run.
-MOCK_LOG_PATH = '/tmp/logs/MockTest/xx-xx-xx_xx-xx-xx/'
-# The expected result of the cat adb operation.
-MOCK_ADB_LOGCAT_CAT_RESULT = [
-    '02-29 14:02:21.456  4454  Something\n',
-    '02-29 14:02:21.789  4454  Something again\n'
-]
-# A mocked piece of adb logcat output.
-MOCK_ADB_LOGCAT = (u'02-29 14:02:19.123  4454  Nothing\n'
-                   u'%s'
-                   u'02-29 14:02:22.123  4454  Something again and again\n'
-                   ) % u''.join(MOCK_ADB_LOGCAT_CAT_RESULT)
-# The expected result of the cat adb operation.
-MOCK_ADB_UNICODE_LOGCAT_CAT_RESULT = [
-    '02-29 14:02:21.456  4454  Something \u901a\n',
-    '02-29 14:02:21.789  4454  Something again\n'
-]
-# A mocked piece of adb logcat output.
-MOCK_ADB_UNICODE_LOGCAT = (
-    u'02-29 14:02:19.123  4454  Nothing\n'
-    u'%s'
-    u'02-29 14:02:22.123  4454  Something again and again\n'
-) % u''.join(MOCK_ADB_UNICODE_LOGCAT_CAT_RESULT)
-
-# Mock start and end time of the adb cat.
-MOCK_ADB_LOGCAT_BEGIN_TIME = '02-29 14:02:20.123'
-MOCK_ADB_LOGCAT_END_TIME = '02-29 14:02:22.000'
 MOCK_SNIPPET_PACKAGE_NAME = 'com.my.snippet'
 
 # A mock SnippetClient used for testing snippet management logic.
 MockSnippetClient = mock.MagicMock()
 MockSnippetClient.package = MOCK_SNIPPET_PACKAGE_NAME
 
-# Mock AdbError for missing logpersist scripts
-MOCK_LOGPERSIST_STOP_MISSING_ADB_ERROR = adb.AdbError(
-    'logpersist.stop --clear', '',
-    '/system/bin/sh: logpersist.stop: not found', 0)
-MOCK_LOGPERSIST_START_MISSING_ADB_ERROR = adb.AdbError(
-    'logpersist.start --clear', '',
-    '/system/bin/sh: logpersist.stop: not found', 0)
-
 
 class AndroidDeviceTest(unittest.TestCase):
     """This test class has unit tests for the implementation of everything
@@ -272,8 +238,6 @@
         ad = android_device.AndroidDevice(serial=mock_serial)
         self.assertEqual(ad.serial, '1')
         self.assertEqual(ad.model, 'fakemodel')
-        self.assertIsNone(ad._adb_logcat_process)
-        self.assertIsNone(ad.adb_logcat_file_path)
         expected_lp = os.path.join(logging.log_path,
                                    'AndroidDevice%s' % mock_serial)
         self.assertEqual(ad.log_path, expected_lp)
@@ -410,122 +374,6 @@
     @mock.patch(
         'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
         return_value=mock_android_device.MockFastbootProxy('1'))
-    @mock.patch('mobly.utils.create_dir')
-    @mock.patch(
-        'mobly.utils.start_standing_subprocess', return_value='process')
-    @mock.patch('mobly.utils.stop_standing_subprocess')
-    def test_AndroidDevice_take_logcat(self, stop_proc_mock, start_proc_mock,
-                                       creat_dir_mock, FastbootProxy,
-                                       MockAdbProxy):
-        """Verifies the steps of collecting adb logcat on an AndroidDevice
-        object, including various function calls and the expected behaviors of
-        the calls.
-        """
-        mock_serial = '1'
-        ad = android_device.AndroidDevice(serial=mock_serial)
-        expected_msg = '.* No ongoing adb logcat collection found.'
-        # Expect error if stop is called before start.
-        with self.assertRaisesRegex(android_device.Error, expected_msg):
-            ad.stop_adb_logcat()
-        ad.start_adb_logcat()
-        # Verify start did the correct operations.
-        self.assertTrue(ad._adb_logcat_process)
-        expected_log_path = os.path.join(logging.log_path,
-                                         'AndroidDevice%s' % ad.serial,
-                                         'adblog,fakemodel,%s.txt' % ad.serial)
-        creat_dir_mock.assert_called_with(os.path.dirname(expected_log_path))
-        adb_cmd = '"adb" -s %s logcat -v threadtime  >> %s'
-        start_proc_mock.assert_called_with(
-            adb_cmd % (ad.serial, '"%s"' % expected_log_path), shell=True)
-        self.assertEqual(ad.adb_logcat_file_path, expected_log_path)
-        expected_msg = (
-            'Logcat thread is already running, cannot start another'
-            ' one.')
-        # Expect error if start is called back to back.
-        with self.assertRaisesRegex(android_device.Error, expected_msg):
-            ad.start_adb_logcat()
-        # Verify stop did the correct operations.
-        ad.stop_adb_logcat()
-        stop_proc_mock.assert_called_with('process')
-        self.assertIsNone(ad._adb_logcat_process)
-        self.assertEqual(ad.adb_logcat_file_path, expected_log_path)
-
-    @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'))
-    @mock.patch('mobly.utils.create_dir')
-    @mock.patch(
-        'mobly.utils.start_standing_subprocess', return_value='process')
-    @mock.patch('mobly.utils.stop_standing_subprocess')
-    @mock.patch(
-        'mobly.controllers.android_device.AndroidDevice._clear_adb_log')
-    def test_AndroidDevice_start_adb_logcat_clear_log_fails(
-            self, mock_clear_adb_log, stop_proc_mock, start_proc_mock,
-            creat_dir_mock, FastbootProxy, MockAdbProxy):
-        """Verifies the steps of collecting adb logcat on an AndroidDevice
-        object, including various function calls and the expected behaviors of
-        the calls.
-        """
-        mock_clear_adb_log.side_effect = adb.AdbError(
-            cmd='adb -s xx logcat -c',
-            stdout='',
-            stderr="failed to clear 'main' log",
-            ret_code=1)
-        mock_serial = '1'
-        ad = android_device.AndroidDevice(serial=mock_serial)
-        ad.start_adb_logcat()
-        # Verify start did the correct operations.
-        self.assertTrue(ad._adb_logcat_process)
-        expected_log_path = os.path.join(logging.log_path,
-                                         'AndroidDevice%s' % ad.serial,
-                                         'adblog,fakemodel,%s.txt' % ad.serial)
-        creat_dir_mock.assert_called_with(os.path.dirname(expected_log_path))
-
-    @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'))
-    @mock.patch('mobly.utils.create_dir')
-    @mock.patch(
-        'mobly.utils.start_standing_subprocess', return_value='process')
-    @mock.patch('mobly.utils.stop_standing_subprocess')
-    def test_AndroidDevice_take_logcat_with_user_param(
-            self, stop_proc_mock, start_proc_mock, creat_dir_mock,
-            FastbootProxy, MockAdbProxy):
-        """Verifies the steps of collecting adb logcat on an AndroidDevice
-        object, including various function calls and the expected behaviors of
-        the calls.
-        """
-        mock_serial = '1'
-        ad = android_device.AndroidDevice(serial=mock_serial)
-        ad.adb_logcat_param = '-b radio'
-        expected_msg = '.* No ongoing adb logcat collection found.'
-        # Expect error if stop is called before start.
-        with self.assertRaisesRegex(android_device.Error, expected_msg):
-            ad.stop_adb_logcat()
-        ad.start_adb_logcat()
-        # Verify start did the correct operations.
-        self.assertTrue(ad._adb_logcat_process)
-        expected_log_path = os.path.join(logging.log_path,
-                                         'AndroidDevice%s' % ad.serial,
-                                         'adblog,fakemodel,%s.txt' % ad.serial)
-        creat_dir_mock.assert_called_with(os.path.dirname(expected_log_path))
-        adb_cmd = '"adb" -s %s logcat -v threadtime -b radio >> %s'
-        start_proc_mock.assert_called_with(
-            adb_cmd % (ad.serial, '"%s"' % expected_log_path), shell=True)
-        self.assertEqual(ad.adb_logcat_file_path, expected_log_path)
-
-    @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'))
     @mock.patch(
         'mobly.utils.start_standing_subprocess', return_value='process')
     @mock.patch('mobly.utils.stop_standing_subprocess')
@@ -533,8 +381,8 @@
                                            start_proc_mock, FastbootProxy,
                                            MockAdbProxy):
         ad = android_device.AndroidDevice(serial='1')
-        ad.start_adb_logcat()
-        ad.stop_adb_logcat()
+        ad.start_services()
+        ad.services.unregister('logcat')
         old_path = ad.log_path
         new_log_path = tempfile.mkdtemp()
         ad.log_path = new_log_path
@@ -590,7 +438,7 @@
             self, stop_proc_mock, start_proc_mock, creat_dir_mock,
             FastbootProxy, MockAdbProxy):
         ad = android_device.AndroidDevice(serial='1')
-        ad.start_adb_logcat()
+        ad.start_services()
         new_log_path = tempfile.mkdtemp()
         expected_msg = '.* Cannot change `log_path` when there is service running.'
         with self.assertRaisesRegex(android_device.Error, expected_msg):
@@ -652,7 +500,7 @@
             self, stop_proc_mock, start_proc_mock, creat_dir_mock,
             FastbootProxy, MockAdbProxy):
         ad = android_device.AndroidDevice(serial='1')
-        ad.start_adb_logcat()
+        ad.start_services()
         expected_msg = '.* Cannot change device serial number when there is service running.'
         with self.assertRaisesRegex(android_device.Error, expected_msg):
             ad.update_serial('2')
@@ -664,189 +512,6 @@
         'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
         return_value=mock_android_device.MockFastbootProxy('1'))
     @mock.patch(
-        'mobly.utils.start_standing_subprocess', return_value='process')
-    @mock.patch('mobly.utils.stop_standing_subprocess')
-    @mock.patch(
-        'mobly.logger.get_log_line_timestamp',
-        return_value=MOCK_ADB_LOGCAT_END_TIME)
-    def test_AndroidDevice_cat_adb_log(self, mock_timestamp_getter,
-                                       stop_proc_mock, start_proc_mock,
-                                       FastbootProxy, MockAdbProxy):
-        """Verifies that AndroidDevice.cat_adb_log loads the correct adb log
-        file, locates the correct adb log lines within the given time range,
-        and writes the lines to the correct output file.
-        """
-        mock_serial = '1'
-        ad = android_device.AndroidDevice(serial=mock_serial)
-        # Direct the log path of the ad to a temp dir to avoid racing.
-        ad._log_path_base = self.tmp_dir
-        # Expect error if attempted to cat adb log before starting adb logcat.
-        expected_msg = ('.* Attempting to cat adb log when none'
-                        ' has been collected.')
-        with self.assertRaisesRegex(android_device.Error, expected_msg):
-            ad.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
-        ad.start_adb_logcat()
-        utils.create_dir(ad.log_path)
-        mock_adb_log_path = os.path.join(ad.log_path, 'adblog,%s,%s.txt' %
-                                         (ad.model, ad.serial))
-        with io.open(mock_adb_log_path, 'w', encoding='utf-8') as f:
-            f.write(MOCK_ADB_LOGCAT)
-        ad.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
-        cat_file_path = os.path.join(
-            ad.log_path, 'AdbLogExcerpts',
-            ('some_test,02-29 14-02-20.123,%s,%s.txt') % (ad.model, ad.serial))
-        with io.open(cat_file_path, 'r', encoding='utf-8') as f:
-            actual_cat = f.read()
-        self.assertEqual(actual_cat, ''.join(MOCK_ADB_LOGCAT_CAT_RESULT))
-        # Stops adb logcat.
-        ad.stop_adb_logcat()
-
-    @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'))
-    @mock.patch(
-        'mobly.utils.start_standing_subprocess', return_value='process')
-    @mock.patch('mobly.utils.stop_standing_subprocess')
-    @mock.patch(
-        'mobly.logger.get_log_line_timestamp',
-        return_value=MOCK_ADB_LOGCAT_END_TIME)
-    def test_AndroidDevice_cat_adb_log_with_unicode(
-            self, mock_timestamp_getter, stop_proc_mock, start_proc_mock,
-            FastbootProxy, MockAdbProxy):
-        """Verifies that AndroidDevice.cat_adb_log loads the correct adb log
-        file, locates the correct adb log lines within the given time range,
-        and writes the lines to the correct output file.
-        """
-        mock_serial = '1'
-        ad = android_device.AndroidDevice(serial=mock_serial)
-        # Direct the log path of the ad to a temp dir to avoid racing.
-        ad._log_path_base = self.tmp_dir
-        # Expect error if attempted to cat adb log before starting adb logcat.
-        expected_msg = ('.* Attempting to cat adb log when none'
-                        ' has been collected.')
-        with self.assertRaisesRegex(android_device.Error, expected_msg):
-            ad.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
-        ad.start_adb_logcat()
-        utils.create_dir(ad.log_path)
-        mock_adb_log_path = os.path.join(ad.log_path, 'adblog,%s,%s.txt' %
-                                         (ad.model, ad.serial))
-        with io.open(mock_adb_log_path, 'w', encoding='utf-8') as f:
-            f.write(MOCK_ADB_UNICODE_LOGCAT)
-        ad.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
-        cat_file_path = os.path.join(
-            ad.log_path, 'AdbLogExcerpts',
-            ('some_test,02-29 14-02-20.123,%s,%s.txt') % (ad.model, ad.serial))
-        with io.open(cat_file_path, 'r', encoding='utf-8') as f:
-            actual_cat = f.read()
-        self.assertEqual(actual_cat,
-                         ''.join(MOCK_ADB_UNICODE_LOGCAT_CAT_RESULT))
-        # Stops adb logcat.
-        ad.stop_adb_logcat()
-
-    @mock.patch(
-        'mobly.controllers.android_device_lib.adb.AdbProxy',
-        return_value=mock.MagicMock())
-    @mock.patch(
-        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
-        return_value=mock_android_device.MockFastbootProxy('1'))
-    def test_AndroidDevice__enable_logpersist_with_logpersist(
-            self, MockFastboot, MockAdbProxy):
-        mock_serial = '1'
-        mock_adb_proxy = MockAdbProxy.return_value
-        # Set getprop to return '1' to indicate the device is rootable.
-        mock_adb_proxy.getprop.return_value = '1'
-        mock_adb_proxy.has_shell_command.side_effect = lambda command: {
-            'logpersist.start': True,
-            'logpersist.stop': True, }[command]
-        ad = android_device.AndroidDevice(serial=mock_serial)
-        ad._enable_logpersist()
-        mock_adb_proxy.shell.assert_has_calls([
-            mock.call('logpersist.stop --clear'),
-            mock.call('logpersist.start'),
-        ])
-
-    @mock.patch(
-        'mobly.controllers.android_device_lib.adb.AdbProxy',
-        return_value=mock.MagicMock())
-    @mock.patch(
-        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
-        return_value=mock_android_device.MockFastbootProxy('1'))
-    def test_AndroidDevice__enable_logpersist_with_missing_all_logpersist(
-            self, MockFastboot, MockAdbProxy):
-        def adb_shell_helper(command):
-            if command == 'logpersist.start':
-                raise MOCK_LOGPERSIST_START_MISSING_ADB_ERROR
-            elif command == 'logpersist.stop --clear':
-                raise MOCK_LOGPERSIST_STOP_MISSING_ADB_ERROR
-            else:
-                return ''
-
-        mock_serial = '1'
-        mock_adb_proxy = MockAdbProxy.return_value
-        mock_adb_proxy.getprop.return_value = 'userdebug'
-        mock_adb_proxy.has_shell_command.side_effect = lambda command: {
-            'logpersist.start': False,
-            'logpersist.stop': False, }[command]
-        mock_adb_proxy.shell.side_effect = adb_shell_helper
-        ad = android_device.AndroidDevice(serial=mock_serial)
-        ad._enable_logpersist()
-
-    @mock.patch(
-        'mobly.controllers.android_device_lib.adb.AdbProxy',
-        return_value=mock.MagicMock())
-    @mock.patch(
-        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
-        return_value=mock_android_device.MockFastbootProxy('1'))
-    def test_AndroidDevice__enable_logpersist_with_missing_logpersist_stop(
-            self, MockFastboot, MockAdbProxy):
-        def adb_shell_helper(command):
-            if command == 'logpersist.stop --clear':
-                raise MOCK_LOGPERSIST_STOP_MISSING_ADB_ERROR
-            else:
-                return ''
-
-        mock_serial = '1'
-        mock_adb_proxy = MockAdbProxy.return_value
-        mock_adb_proxy.getprop.return_value = 'userdebug'
-        mock_adb_proxy.has_shell_command.side_effect = lambda command: {
-            'logpersist.start': True,
-            'logpersist.stop': False, }[command]
-        mock_adb_proxy.shell.side_effect = adb_shell_helper
-        ad = android_device.AndroidDevice(serial=mock_serial)
-        ad._enable_logpersist()
-
-    @mock.patch(
-        'mobly.controllers.android_device_lib.adb.AdbProxy',
-        return_value=mock.MagicMock())
-    @mock.patch('mobly.utils.stop_standing_subprocess')
-    def test_AndroidDevice__enable_logpersist_with_missing_logpersist_start(
-            self, MockFastboot, MockAdbProxy):
-        def adb_shell_helper(command):
-            if command == 'logpersist.start':
-                raise MOCK_LOGPERSIST_START_MISSING_ADB_ERROR
-            else:
-                return ''
-
-        mock_serial = '1'
-        mock_adb_proxy = MockAdbProxy.return_value
-        mock_adb_proxy.getprop.return_value = 'userdebug'
-        mock_adb_proxy.has_shell_command.side_effect = lambda command: {
-            'logpersist.start': False,
-            'logpersist.stop': True, }[command]
-        mock_adb_proxy.shell.side_effect = adb_shell_helper
-        ad = android_device.AndroidDevice(serial=mock_serial)
-        ad._enable_logpersist()
-
-    @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'))
-    @mock.patch(
         'mobly.controllers.android_device_lib.snippet_client.SnippetClient')
     @mock.patch('mobly.utils.get_available_host_port')
     def test_AndroidDevice_load_snippet(self, MockGetPort, MockSnippetClient,
@@ -1037,6 +702,7 @@
     def test_AndroidDevice_snippet_cleanup(
             self, MockGetPort, MockSnippetClient, MockFastboot, MockAdbProxy):
         ad = android_device.AndroidDevice(serial='1')
+        ad.start_services()
         ad.load_snippet('snippet', MOCK_SNIPPET_PACKAGE_NAME)
         ad.stop_services()
         self.assertFalse(hasattr(ad, 'snippet'))
@@ -1067,6 +733,52 @@
         except Exception as e:
             self.assertEqual("(<AndroidDevice|Mememe>, 'Something')", str(e))
 
+    @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'))
+    @mock.patch(
+        'mobly.utils.start_standing_subprocess', return_value='process')
+    @mock.patch('mobly.utils.stop_standing_subprocess')
+    def test_AndroidDevice_handle_usb_disconnect(self, stop_proc_mock,
+                                                 start_proc_mock,
+                                                 FastbootProxy, MockAdbProxy):
+        class MockService(base_service.BaseService):
+            def __init__(self, device, configs=None):
+                self._alive = False
+                self.pause_called = False
+                self.resume_called = False
+
+            @property
+            def is_alive(self):
+                return self._alive
+
+            def start(self, configs=None):
+                self._alive = True
+
+            def stop(self):
+                self._alive = False
+
+            def pause(self):
+                self._alive = False
+                self.pause_called = True
+
+            def resume(self):
+                self._alive = True
+                self.resume_called = True
+
+        ad = android_device.AndroidDevice(serial='1')
+        ad.start_services()
+        ad.services.register('mock_service', MockService)
+        with ad.handle_usb_disconnect():
+            self.assertFalse(ad.services.is_any_alive)
+            self.assertTrue(ad.services.mock_service.pause_called)
+            self.assertFalse(ad.services.mock_service.resume_called)
+        self.assertTrue(ad.services.is_any_alive)
+        self.assertTrue(ad.services.mock_service.resume_called)
+
 
 if __name__ == '__main__':
     unittest.main()