Simplify service interface and post-service clean ups. (#515)

* Disallow changing config in `start`.
* Update logcat service to conform to the new interface.
* Give the option to not start service upon registration.
diff --git a/mobly/controllers/android_device.py b/mobly/controllers/android_device.py
index 430ece9..178fd7b 100644
--- a/mobly/controllers/android_device.py
+++ b/mobly/controllers/android_device.py
@@ -54,6 +54,7 @@
 # Default is False.
 KEY_SKIP_LOGCAT = 'skip_logcat'
 DEFAULT_VALUE_SKIP_LOGCAT = False
+SERVICE_NAME_LOGCAT = 'logcat'
 
 # Default Timeout to wait for boot completion
 DEFAULT_TIMEOUT_BOOT_COMPLETION_SECOND = 15 * 60
@@ -135,11 +136,11 @@
     running_ads = []
     for ad in ads:
         running_ads.append(ad)
-        skip_logcat = getattr(ad, KEY_SKIP_LOGCAT, DEFAULT_VALUE_SKIP_LOGCAT)
-        if skip_logcat:
-            continue
+        start_logcat = not getattr(ad, KEY_SKIP_LOGCAT,
+                                   DEFAULT_VALUE_SKIP_LOGCAT)
         try:
-            ad.start_services()
+            ad.services.register(
+                SERVICE_NAME_LOGCAT, logcat.Logcat, start_service=start_logcat)
         except Exception:
             is_required = getattr(ad, KEY_DEVICE_REQUIRED,
                                   DEFAULT_VALUE_DEVICE_REQUIRED)
@@ -449,7 +450,7 @@
 
     @property
     def adb_logcat_file_path(self):
-        if self.services.has_service_by_name('logcat'):
+        if self.services.has_service_by_name(SERVICE_NAME_LOGCAT):
             return self.services.logcat.adb_logcat_file_path
 
     @property
@@ -629,8 +630,6 @@
         Starts long running services on the android device, like adb logcat
         capture.
         """
-        configs = logcat.Config(clear_log=clear_log)
-        self.services.register('logcat', logcat.Logcat, configs)
         self.services.start_all()
 
     def start_adb_logcat(self, clear_log=True):
@@ -638,8 +637,7 @@
 
         Use `self.services.logcat.start` instead.
         """
-        configs = logcat.Config(clear_log=clear_log)
-        self.services.logcat.start(configs)
+        self.services.logcat.start()
 
     def stop_adb_logcat(self):
         """.. deprecated:: 1.8
diff --git a/mobly/controllers/android_device_lib/jsonrpc_shell_base.py b/mobly/controllers/android_device_lib/jsonrpc_shell_base.py
index e596d8c..162813a 100755
--- a/mobly/controllers/android_device_lib/jsonrpc_shell_base.py
+++ b/mobly/controllers/android_device_lib/jsonrpc_shell_base.py
@@ -77,7 +77,7 @@
         code.interact(banner=console_banner, local=console_env)
 
         # Tear everything down
-        self._ad.stop_services()
+        self._ad.services.stop_all()
 
     def main(self, serial=None):
         try:
diff --git a/mobly/controllers/android_device_lib/service_manager.py b/mobly/controllers/android_device_lib/service_manager.py
index bb65be8..0d93ad5 100644
--- a/mobly/controllers/android_device_lib/service_manager.py
+++ b/mobly/controllers/android_device_lib/service_manager.py
@@ -56,7 +56,7 @@
                 return True
         return False
 
-    def register(self, alias, service_class, configs=None):
+    def register(self, alias, service_class, configs=None, start_service=True):
         """Registers a service.
 
         This will create a service instance, starts the service, and adds the
@@ -67,6 +67,8 @@
             service_class: class, the service class to instantiate.
             configs: (optional) config object to pass to the service class's
                 constructor.
+            start_service: bool, whether to start the service instance or not.
+                Default is True.
         """
         if not inspect.isclass(service_class):
             raise Error(self._device, '"%s" is not a class!' % service_class)
@@ -79,7 +81,8 @@
                 self._device,
                 'A service is already registered with alias "%s".' % alias)
         service_obj = service_class(self._device, configs)
-        service_obj.start()
+        if start_service:
+            service_obj.start()
         self._service_objects[alias] = service_obj
 
     def unregister(self, alias):
diff --git a/mobly/controllers/android_device_lib/services/base_service.py b/mobly/controllers/android_device_lib/services/base_service.py
index 1806ae2..14380df 100644
--- a/mobly/controllers/android_device_lib/services/base_service.py
+++ b/mobly/controllers/android_device_lib/services/base_service.py
@@ -23,6 +23,10 @@
     def __init__(self, device, configs=None):
         """Constructor of the class.
 
+        The constructor is the only place to pass in a config. If you need to
+        change the config later, you should unregister the service instance
+        from `ServiceManager` and register again with the new config.
+
         Args:
           device: the device object this service is associated with.
           config: optional configuration defined by the author of the service
@@ -36,12 +40,8 @@
         """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.
-        """
+    def start(self):
+        """Starts the service."""
         raise NotImplementedError('"start" is a required service method.')
 
     def stop(self):
@@ -84,4 +84,4 @@
         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)
+        self.start()
diff --git a/mobly/controllers/android_device_lib/services/logcat.py b/mobly/controllers/android_device_lib/services/logcat.py
index 9549c75..f1b021f 100644
--- a/mobly/controllers/android_device_lib/services/logcat.py
+++ b/mobly/controllers/android_device_lib/services/logcat.py
@@ -34,11 +34,17 @@
     Attributes:
         clear_log: bool, clears the logcat before collection if True.
         logcat_params: string, extra params to be added to logcat command.
+        output_file_path: string, the path on the host to write the log file
+            to. The service will automatically generate one if not specified.
     """
 
-    def __init__(self, params=None, clear_log=True):
+    def __init__(self,
+                 logcat_params=None,
+                 clear_log=True,
+                 output_file_path=None):
         self.clear_log = clear_log
-        self.logcat_params = params if params else ''
+        self.logcat_params = logcat_params if logcat_params else ''
+        self.output_file_path = output_file_path
 
 
 class Logcat(base_service.BaseService):
@@ -87,7 +93,7 @@
         return True if self._adb_logcat_process else False
 
     def clear_adb_log(self):
-        # Clears cached adb content.
+        """Clears cached adb content."""
         try:
             self._ad.adb.logcat('-c')
         except adb.AdbError as e:
@@ -150,30 +156,40 @@
                         if in_range:
                             break
 
-    def start(self, configs=None):
-        """Starts a standing adb logcat collection.
+    def _assert_not_running(self):
+        """Asserts the logcat service is not running.
 
-        The collection runs in a separate subprocess and saves logs in a file.
-
-        Args:
-            configs: Conifg object.
+        Raises:
+            Error, if the logcat service is running.
         """
-        if self._adb_logcat_process:
+        if self.is_alive:
             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()
 
+    def start(self):
+        """Starts a standing adb logcat collection.
+
+        The collection runs in a separate subprocess and saves logs in a file.
+        """
+        self._assert_not_running()
+        if self._configs.clear_log:
+            self.clear_adb_log()
+        self._start()
+
+    def _start(self):
+        """The actual logic of starting logcat."""
         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)
+        logcat_file_path = self._configs.output_file_path
+        if not logcat_file_path:
+            f_name = 'adblog,%s,%s.txt' % (self._ad.model,
+                                           self._ad._normalized_serial)
+            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)
+            adb.ADB, self._ad.serial, self._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
@@ -191,20 +207,15 @@
     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.
+        # Clears cached adb content, so that the next time logcat is started,
+        # we 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)
+        """Resumes a paused logcat service."""
+        self._assert_not_running()
+        # Not clearing the log regardless of the config when resuming.
+        # Otherwise the logs during the paused time will be lost.
+        self._start()
diff --git a/tests/mobly/controllers/android_device_lib/services/logcat_test.py b/tests/mobly/controllers/android_device_lib/services/logcat_test.py
index 1871c43..2cf6923 100755
--- a/tests/mobly/controllers/android_device_lib/services/logcat_test.py
+++ b/tests/mobly/controllers/android_device_lib/services/logcat_test.py
@@ -88,9 +88,8 @@
     @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):
+    def test_start_and_stop(self, stop_proc_mock, start_proc_mock,
+                            create_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.
@@ -104,7 +103,7 @@
         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))
+        create_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)
@@ -136,12 +135,12 @@
     @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):
+    def test_pause_and_resume(self, clear_adb_mock, stop_proc_mock,
+                              start_proc_mock, create_dir_mock, FastbootProxy,
+                              MockAdbProxy):
         mock_serial = '1'
         ad = android_device.AndroidDevice(serial=mock_serial)
-        logcat_service = logcat.Logcat(ad)
+        logcat_service = logcat.Logcat(ad, logcat.Config(clear_log=True))
         logcat_service.start()
         clear_adb_mock.assert_called_once_with()
         self.assertTrue(logcat_service.is_alive)
@@ -164,9 +163,9 @@
     @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):
+    def test_take_logcat_with_extra_params(self, stop_proc_mock,
+                                           start_proc_mock, create_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.
@@ -182,7 +181,7 @@
         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))
+        create_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)
@@ -195,44 +194,7 @@
     @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):
+    def test_instantiation(self, MockFastboot, MockAdbProxy):
         """Verifies the AndroidDevice object's basic attributes are correctly
         set after instantiation.
         """
@@ -254,9 +216,8 @@
     @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):
+    def test_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.
@@ -300,9 +261,9 @@
     @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):
+    def test_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.
@@ -341,8 +302,8 @@
     @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):
+    def test__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.
@@ -364,7 +325,7 @@
     @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(
+    def test__enable_logpersist_with_missing_all_logpersist(
             self, MockFastboot, MockAdbProxy):
         def adb_shell_helper(command):
             if command == 'logpersist.start':
@@ -391,7 +352,7 @@
     @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(
+    def test__enable_logpersist_with_missing_logpersist_stop(
             self, MockFastboot, MockAdbProxy):
         def adb_shell_helper(command):
             if command == 'logpersist.stop --clear':
@@ -414,7 +375,7 @@
         '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(
+    def test__enable_logpersist_with_missing_logpersist_start(
             self, MockFastboot, MockAdbProxy):
         def adb_shell_helper(command):
             if command == 'logpersist.start':
@@ -437,7 +398,7 @@
     @mock.patch(
         'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
         return_value=mock_android_device.MockFastbootProxy('1'))
-    def test_logcat_service_clear_adb_log(self, MockFastboot, MockAdbProxy):
+    def test_clear_adb_log(self, MockFastboot, MockAdbProxy):
         mock_serial = '1'
         ad = android_device.AndroidDevice(serial=mock_serial)
         ad.adb.logcat = mock.MagicMock()
diff --git a/tests/mobly/controllers/android_device_test.py b/tests/mobly/controllers/android_device_test.py
index 818d10c..f83c56f 100755
--- a/tests/mobly/controllers/android_device_test.py
+++ b/tests/mobly/controllers/android_device_test.py
@@ -198,11 +198,11 @@
         """
         msg = 'Some error happened.'
         ads = mock_android_device.get_mock_ads(3)
-        ads[0].start_services = mock.MagicMock()
+        ads[0].services.register = mock.MagicMock()
         ads[0].stop_services = mock.MagicMock()
-        ads[1].start_services = mock.MagicMock()
+        ads[1].services.register = mock.MagicMock()
         ads[1].stop_services = mock.MagicMock()
-        ads[2].start_services = mock.MagicMock(
+        ads[2].services.register = mock.MagicMock(
             side_effect=android_device.Error(msg))
         ads[2].stop_services = mock.MagicMock()
         with self.assertRaisesRegex(android_device.Error, msg):
@@ -213,9 +213,9 @@
 
     def test_start_services_on_ads_skip_logcat(self):
         ads = mock_android_device.get_mock_ads(3)
-        ads[0].start_services = mock.MagicMock()
-        ads[1].start_services = mock.MagicMock()
-        ads[2].start_services = mock.MagicMock(
+        ads[0].services.logcat.start = mock.MagicMock()
+        ads[1].services.logcat.start = mock.MagicMock()
+        ads[2].services.logcat.start = mock.MagicMock(
             side_effect=Exception('Should not have called this.'))
         ads[2].skip_logcat = True
         android_device._start_services_on_ads(ads)
@@ -381,8 +381,6 @@
                                            start_proc_mock, FastbootProxy,
                                            MockAdbProxy):
         ad = android_device.AndroidDevice(serial='1')
-        ad.start_services()
-        ad.services.unregister('logcat')
         old_path = ad.log_path
         new_log_path = tempfile.mkdtemp()
         ad.log_path = new_log_path
@@ -438,7 +436,7 @@
             self, stop_proc_mock, start_proc_mock, creat_dir_mock,
             FastbootProxy, MockAdbProxy):
         ad = android_device.AndroidDevice(serial='1')
-        ad.start_services()
+        ad.services.register('logcat', logcat.Logcat)
         new_log_path = tempfile.mkdtemp()
         expected_msg = '.* Cannot change `log_path` when there is service running.'
         with self.assertRaisesRegex(android_device.Error, expected_msg):
@@ -500,7 +498,7 @@
             self, stop_proc_mock, start_proc_mock, creat_dir_mock,
             FastbootProxy, MockAdbProxy):
         ad = android_device.AndroidDevice(serial='1')
-        ad.start_services()
+        ad.services.register('logcat', logcat.Logcat)
         expected_msg = '.* Cannot change device serial number when there is service running.'
         with self.assertRaisesRegex(android_device.Error, expected_msg):
             ad.update_serial('2')