Merge "Handle large protobuf transfer with vts shell driver"
diff --git a/harnesses/host_controller/invocation_thread.py b/harnesses/host_controller/invocation_thread.py
new file mode 100644
index 0000000..9673862
--- /dev/null
+++ b/harnesses/host_controller/invocation_thread.py
@@ -0,0 +1,169 @@
+#
+# Copyright (C) 2017 The Android Open Source Project
+#
+# 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 logging
+import socket
+import threading
+
+import httplib2
+from apiclient import errors
+
+from vts.harnesses.host_controller.tfc import command_attempt
+from vts.harnesses.host_controller.tradefed import remote_operation
+
+
+class InvocationThread(threading.Thread):
+    """The thread that remotely executes a command task.
+
+    Attributes:
+        _remote_client: The RemoteClient which executes the command.
+        _tfc_client: The TfcClient to which the command events are sent.
+        _attempt: The CommandAttempt whose events are sent to TFC.
+        _command: A list of strings, the command and arguments.
+        device_serials: A list of strings, the serial numbers of the devices
+                        which need to be allocated to the task.
+        _allocated_serials: A list of strings, the serial numbers of the devices
+                            which are successfully allocated.
+        _tfc_heartbeat_interval: The interval of TestRunInProgress events in
+                                 seconds.
+    """
+
+    def __init__(self,
+                 remote_client,
+                 tfc_client,
+                 attempt,
+                 command,
+                 device_serials,
+                 tfc_heartbeat_interval=5 * 60):
+        """Initializes the attributes."""
+        super(InvocationThread, self).__init__()
+        self._remote_client = remote_client
+        self._tfc_client = tfc_client
+        self._attempt = attempt
+        self._command = command
+        self.device_serials = device_serials
+        self._allocated_serials = None
+        # The value in Java implementation is 5 minutes.
+        self._tfc_heartbeat_interval = tfc_heartbeat_interval
+
+    def _AllocateDevices(self):
+        """Allocates all of device_serial."""
+        for serial in self.device_serials:
+            self._remote_client.SendOperation(
+                    remote_operation.AllocateDevice(serial))
+            self._allocated_serials.append(serial)
+
+    def _StartInvocation(self):
+        """Starts executing command and sends the event to TFC."""
+        self._remote_client.SendOperation(
+                remote_operation.ExecuteCommand(self.device_serials[0],
+                                                *self._command))
+        event = self._attempt.CreateCommandEvent(
+                command_attempt.EventType.INVOCATION_STARTED)
+        self._tfc_client.SubmitCommandEvents([event])
+
+    def _WaitForCommandResult(self):
+        """Waits for command result and keeps sending heartbeat to TFC
+
+        Returns:
+            A JSON object returned from TradeFed remote manager.
+        """
+        while True:
+            result = self._remote_client.WaitForCommandResult(
+                    self.device_serials[0], self._tfc_heartbeat_interval)
+            if result:
+                return result
+            event = self._attempt.CreateCommandEvent(
+                    command_attempt.EventType.TEST_RUN_IN_PROGRESS)
+            self._tfc_client.SubmitCommandEvents([event])
+
+    def _CompleteInvocation(self, result):
+        """Sends InvocationCompleted event according to the result.
+
+        Args:
+            result: A JSON object returned from TradeFed remote manager.
+        """
+        if result["status"] == "INVOCATION_SUCCESS":
+            event = self._attempt.CreateInvocationCompletedEvent(
+                    str(result), 1, 0)
+        else:
+            event = self._attempt.CreateInvocationCompletedEvent(
+                    str(result), 1, 1, error=str(result))
+        self._tfc_client.SubmitCommandEvents([event])
+
+    def _FreeAllocatedDevices(self):
+        """Frees allocated devices and tolerates RemoteOperationException."""
+        for serial in self._allocated_serials:
+            try:
+                self._remote_client.SendOperation(
+                        remote_operation.FreeDevice(serial))
+            except remote_operation.RemoteOperationException as e:
+                logging.exception(e)
+            except socket.error as e:
+                logging.exception(e)
+                break
+        self._allocated_serials = []
+
+    def _SubmitErrorEvent(self, event_type, error_msg):
+        """Submits an error event and tolerates http exceptions.
+
+        Args:
+            event_type: A string, the type of the command event.
+            error_msg: A string, the error message.
+        """
+        try:
+            self._tfc_client.SubmitCommandEvents(
+                [self._attempt.CreateCommandEvent(event_type, error_msg)])
+        except (httplib2.HttpLib2Error, errors.HttpError) as e:
+            logging.exception(e)
+
+    # @Override
+    def run(self):
+        """Executes a command task with exception handling."""
+        self._allocated_serials = []
+        last_error = None
+        error_event = command_attempt.EventType.ALLOCATION_FAILED
+        try:
+            self._AllocateDevices()
+            error_event = command_attempt.EventType.EXECUTE_FAILED
+            self._StartInvocation()
+            result = self._WaitForCommandResult()
+            self._CompleteInvocation(result)
+            error_event = None
+        except errors.HttpError as e:
+            logging.exception(e)
+            last_error = e
+        except remote_operation.RemoteOperationException as e:
+            logging.exception(e)
+            last_error = e
+            # ConfigurationException on TradeFed remote manager.
+            if str(e).startswith("Config error: "):
+                error_event = command_attempt.EventType.CONFIGURATION_ERROR
+        except httplib2.HttpLib2Error as e:
+            logging.exception("Cannot communicate with TradeFed cluster: %s\n"
+                              "Skip submitting event %s.", e, error_event)
+            last_error = e
+            error_event = None
+        except socket.error as e:
+            logging.exception("Cannot communicate with TradeFed remote "
+                              "manager: %s\nSkip freeing devices %s.",
+                              e, self._allocated_serials)
+            last_error = e
+            self._allocated_serials = []
+        finally:
+            if error_event:
+                self._SubmitErrorEvent(error_event, str(last_error))
+            self._FreeAllocatedDevices()
diff --git a/harnesses/host_controller/invocation_thread_test.py b/harnesses/host_controller/invocation_thread_test.py
new file mode 100644
index 0000000..c940329
--- /dev/null
+++ b/harnesses/host_controller/invocation_thread_test.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2017 The Android Open Source Project
+#
+# 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 unittest
+
+try:
+    from unittest import mock
+except ImportError:
+    import mock
+
+from vts.harnesses.host_controller import invocation_thread
+from vts.harnesses.host_controller.tfc import command_attempt
+from vts.harnesses.host_controller.tradefed import remote_operation
+
+
+class InvocationThreadTest(unittest.TestCase):
+    """A test for invocation_thread.InvocationThread.
+
+    Attributes:
+        _remote_client: A mock remote_client.RemoteClient.
+        _tfc_client: A mock tfc_client.TfcClient.
+        _inv_thread: The InvocationThread being tested.
+    """
+
+    def setUp(self):
+        """Creates the InvocationThread."""
+        self._remote_client = mock.Mock()
+        self._tfc_client = mock.Mock()
+        attempt = command_attempt.CommandAttempt(
+                task_id="321-0",
+                attempt_id="abcd-1234",
+                hostname="host0",
+                device_serial="ABCDEF")
+        command = ["vts", "-m", "SampleShellTest"]
+        serials = ["serial123", "serial456"]
+        self._inv_thread = invocation_thread.InvocationThread(
+                self._remote_client, self._tfc_client,
+                attempt, command, serials)
+
+    def _GetSubmittedEventTypes(self):
+        """Gets the types of the events submitted by the mock TfcClient.
+
+        Returns:
+            A list of strings, the event types.
+        """
+        event_types = []
+        for args, kwargs in self._tfc_client.SubmitCommandEvents.call_args_list:
+            event_types.extend(event["type"] for event in args[0])
+        return event_types
+
+    def _GetSentOperationTypes(self):
+        """Gets the types of the operations sent by the mock RemoteClient.
+
+        Returns:
+            A list of strings, the operation types.
+        """
+        operation_types = [args[0].type for args, kwargs in
+                           self._remote_client.SendOperation.call_args_list]
+        return operation_types
+
+    def testAllocationFailed(self):
+        """Tests AllocationFailed event."""
+        self._remote_client.SendOperation.side_effect = (
+                lambda op: _RaiseExceptionForOperation(op, "ALLOCATE_DEVICE"))
+        self._inv_thread.run()
+        self.assertEqual([command_attempt.EventType.ALLOCATION_FAILED],
+                         self._GetSubmittedEventTypes())
+        self.assertEqual(["ALLOCATE_DEVICE"],
+                         self._GetSentOperationTypes())
+
+    def testExecuteFailed(self):
+        """Tests ExecuteFailed event."""
+        self._remote_client.SendOperation.side_effect = (
+                lambda op: _RaiseExceptionForOperation(op, "EXEC_COMMAND"))
+        self._inv_thread.run()
+        self.assertEqual([command_attempt.EventType.EXECUTE_FAILED],
+                         self._GetSubmittedEventTypes())
+        self.assertEqual(["ALLOCATE_DEVICE",
+                          "ALLOCATE_DEVICE",
+                          "EXEC_COMMAND",
+                          "FREE_DEVICE",
+                          "FREE_DEVICE"],
+                         self._GetSentOperationTypes())
+
+    def testConfigurationError(self):
+        """Tests ConfigurationError event."""
+        self._remote_client.SendOperation.side_effect = (
+                lambda op: _RaiseExceptionForOperation(op, "EXEC_COMMAND",
+                                                       "Config error: test"))
+        self._inv_thread.run()
+        self.assertEqual([command_attempt.EventType.CONFIGURATION_ERROR],
+                         self._GetSubmittedEventTypes())
+        self.assertEqual(["ALLOCATE_DEVICE",
+                          "ALLOCATE_DEVICE",
+                          "EXEC_COMMAND",
+                          "FREE_DEVICE",
+                          "FREE_DEVICE"],
+                         self._GetSentOperationTypes())
+
+    def testInvocationCompleted(self):
+        """Tests InvocationCompleted event."""
+        self._remote_client.WaitForCommandResult.side_effect = (
+                None, {"status": "INVOCATION_SUCCESS"})
+        self._inv_thread.run()
+        self.assertEqual([command_attempt.EventType.INVOCATION_STARTED,
+                          command_attempt.EventType.TEST_RUN_IN_PROGRESS,
+                          command_attempt.EventType.INVOCATION_COMPLETED],
+                         self._GetSubmittedEventTypes())
+        # GET_LAST_COMMAND_RESULT isn't called in mock WaitForCommandResult.
+        self.assertEqual(["ALLOCATE_DEVICE",
+                          "ALLOCATE_DEVICE",
+                          "EXEC_COMMAND",
+                          "FREE_DEVICE",
+                          "FREE_DEVICE"],
+                         self._GetSentOperationTypes())
+
+
+def _RaiseExceptionForOperation(operation, op_type, error_msg="unit test"):
+    """Raises exception for specific operation type.
+
+    Args:
+        operation: A remote_operation.RemoteOperation object.
+        op_type: A string, the expected type.
+        error_msg: The message in the exception.
+
+    Raises:
+        RemoteOperationException if the operation's type matches op_type.
+    """
+    if operation.type == op_type:
+        raise remote_operation.RemoteOperationException(error_msg)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/harnesses/host_controller/tfc/command_attempt.py b/harnesses/host_controller/tfc/command_attempt.py
new file mode 100644
index 0000000..91b7551
--- /dev/null
+++ b/harnesses/host_controller/tfc/command_attempt.py
@@ -0,0 +1,137 @@
+#
+# Copyright (C) 2017 The Android Open Source Project
+#
+# 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 time
+
+from vts.harnesses.host_controller.tfc import api_message
+
+
+class EventType(object):
+    """The types of command events."""
+    ALLOCATION_FAILED = "AllocationFailed"
+    CONFIGURATION_ERROR = "ConfigurationError"
+    EXECUTE_FAILED = "ExecuteFailed"
+    FETCH_FAILED = "FetchFailed"
+    INVOCATION_COMPLETED = "InvocationCompleted"
+    INVOCATION_STARTED = "InvocationStarted"
+    TEST_RUN_IN_PROGRESS = "TestRunInProgress"
+
+
+class CommandAttempt(api_message.ApiMessage):
+    """The command attempt defined by TFC API.
+
+    Attributes:
+        _COMMAND_EVENT: The parameters of command_events.submit.
+        _COMMAND_EVENT_DATA: The fields in "data" parameter of command_events.
+        _LIST_ATTEMPT: The fields returned by commandAttempts.list.
+    """
+    _COMMAND_EVENT = {
+            "attempt_id",
+            "data",
+            "device_serial",
+            "hostname",
+            "task_id",
+            "time",
+            "type"}
+    _COMMAND_EVENT_DATA = {
+            "error",
+            "failed_test_count",
+            "summary",
+            "test_run_name",
+            "total_test_count"}
+    _LIST_ATTEMPT = {
+            "attempt_id",
+            "command_id",
+            "create_time",
+            "end_time",
+            "error",
+            "device_serial",
+            "failed_test_count",
+            "hostname",
+            "request_id",
+            "start_time",
+            "state",
+            "status",
+            "summary",
+            "task_id",
+            "total_test_count",
+            "update_time"}
+
+    def __init__(self, task_id, attempt_id, hostname, device_serial, **kwargs):
+        """Initializes the attributes.
+
+        Args:
+            task_id: A string, the task id assigned by the server.
+            attempt_id: A string or UUID, the attempt id generated by the host.
+            hostname: The name of the TradeFed host.
+            device_serial: The serial number of the device.
+            **kwargs: The optional attributes.
+        """
+        super(CommandAttempt, self).__init__(self._LIST_ATTEMPT,
+                                             task_id=task_id,
+                                             attempt_id=str(attempt_id),
+                                             hostname=hostname,
+                                             device_serial=device_serial,
+                                             **kwargs)
+
+    def CreateCommandEvent(self, event_type, error=None, event_time=None):
+        """Creates an event defined by command_events.submit.
+
+        Args:
+            event_type: A string in EventType.
+            error: A string, the error message for *Failed, *Error, and
+                   *Completed events.
+            event_time: A float, Unix timestamp of the event in seconds.
+
+        Returns:
+            A JSON object.
+        """
+        obj = self.ToJson(self._COMMAND_EVENT)
+        obj["type"] = event_type
+        obj["time"] = int(event_time if event_time is not None else time.time())
+        data_obj = self.ToJson(self._COMMAND_EVENT_DATA)
+        if error is not None:
+            data_obj["error"] = error
+        if data_obj:
+            obj["data"] = data_obj
+        return obj
+
+    def CreateInvocationCompletedEvent(self,
+                                       summary,
+                                       total_test_count,
+                                       failed_test_count,
+                                       error=None,
+                                       event_time=None):
+        """Creates an InvocationCompleted event.
+
+        Args:
+            summary: A string, the result of the command.
+            total_test_count: Number of test cases.
+            failed_test_count: Number of failed test cases.
+            error: A string, the error message.
+            event_time: A float, Unix timestamp of the event in seconds.
+
+        Returns:
+            A JSON object.
+        """
+        obj = self.CreateCommandEvent(EventType.INVOCATION_COMPLETED,
+                                      error, event_time)
+        if "data" not in obj:
+            obj["data"] = dict()
+        obj["data"].update({"summary": summary,
+                            "total_test_count": total_test_count,
+                            "failed_test_count": failed_test_count})
+        return obj
diff --git a/harnesses/host_controller/tfc/tfc_client.py b/harnesses/host_controller/tfc/tfc_client.py
index 0ccf276..fe42bf1 100644
--- a/harnesses/host_controller/tfc/tfc_client.py
+++ b/harnesses/host_controller/tfc/tfc_client.py
@@ -98,6 +98,16 @@
         logging.info("host_events.submit body=%s", json_obj)
         self._service.host_events().submit(body=json_obj).execute()
 
+    def SubmitCommandEvents(self, command_events):
+        """Calls command_events.submit.
+
+        Args:
+            command_events: A list of JSON objects converted from CommandAttempt.
+        """
+        json_obj = {"command_events": command_events}
+        logging.info("command_events.submit body=%s", json_obj)
+        self._service.command_events().submit(body=json_obj).execute()
+
     def NewRequest(self, request):
         """Calls requests.new.
 
diff --git a/harnesses/host_controller/tfc/tfc_client_test.py b/harnesses/host_controller/tfc/tfc_client_test.py
index fd87df7..411d039 100644
--- a/harnesses/host_controller/tfc/tfc_client_test.py
+++ b/harnesses/host_controller/tfc/tfc_client_test.py
@@ -23,6 +23,7 @@
     import mock
 
 from vts.harnesses.host_controller.tfc import tfc_client
+from vts.harnesses.host_controller.tfc import command_attempt
 from vts.harnesses.host_controller.tfc import device_info
 from vts.harnesses.host_controller.tfc import request
 
@@ -77,6 +78,48 @@
         self._service.assert_has_calls([
                 mock.call.host_events().submit().execute()])
 
+    def testCommandEvents(self):
+        """Tests command_events.submit."""
+        cmd = command_attempt.CommandAttempt(
+                task_id="321-0",
+                attempt_id="abcd-1234",
+                hostname="host0",
+                device_serial="ABCDEF")
+        expected_event = {
+                "task_id": "321-0",
+                "attempt_id": "abcd-1234",
+                "hostname": "host0",
+                "device_serial": "ABCDEF",
+                "time": 1}
+
+        normal_event = cmd.CreateCommandEvent(
+                command_attempt.EventType.INVOCATION_STARTED,
+                event_time=1)
+        expected_event["type"] = command_attempt.EventType.INVOCATION_STARTED
+        self.assertDictEqual(expected_event, normal_event)
+
+        error_event = cmd.CreateCommandEvent(
+                command_attempt.EventType.EXECUTE_FAILED,
+                error="unit test", event_time=1.1)
+        expected_event["type"] = command_attempt.EventType.EXECUTE_FAILED
+        expected_event["data"] = {"error":"unit test"}
+        self.assertDictEqual(expected_event, error_event)
+
+        complete_event = cmd.CreateInvocationCompletedEvent(
+                summary="complete", total_test_count=2, failed_test_count=1,
+                error="unit test")
+        expected_event["type"] = command_attempt.EventType.INVOCATION_COMPLETED
+        expected_event["data"] = {"summary": "complete", "error": "unit test",
+                                  "total_test_count": 2, "failed_test_count": 1}
+        del expected_event["time"]
+        self.assertDictContainsSubset(expected_event, complete_event)
+        self.assertIn("time", complete_event)
+
+        self._client.SubmitCommandEvents([
+                normal_event, error_event, complete_event])
+        self._service.assert_has_calls([
+                mock.call.command_events().submit().execute()])
+
     def testWrongParameter(self):
         """Tests raising exception for wrong parameter name."""
         self.assertRaisesRegexp(KeyError, "sdk", device_info.DeviceInfo,
diff --git a/harnesses/host_controller/tradefed/remote_client.py b/harnesses/host_controller/tradefed/remote_client.py
index ffa4b52..6427676 100644
--- a/harnesses/host_controller/tradefed/remote_client.py
+++ b/harnesses/host_controller/tradefed/remote_client.py
@@ -105,21 +105,9 @@
         json_obj = self.SendOperation(remote_operation.ListDevices())
         return remote_operation.ParseListDevicesResponse(json_obj)
 
-    def RunCommand(self, serial, *command):
-        """Sends a series of operations to run a command.
-
-        Args:
-            serial: The serial number of the device.
-            *command: A list of strings which is the command to execute.
-        """
-        self.SendOperation(remote_operation.AllocateDevice(serial))
-        self.SendOperation(remote_operation.ExecuteCommand(serial, *command))
-
     def WaitForCommandResult(self, serial, timeout, poll_interval=5):
         """Sends a series of operations to wait until a command finishes.
 
-        This method frees the device if the command finishes before timeout.
-
         Args:
             serial: The serial number of the device.
             timeout: A float, the timeout in seconds.
@@ -129,13 +117,16 @@
         Returns:
             A JSON object which is the result of the command.
             None if timeout.
+
+            Sample
+            {'status': 'INVOCATION_SUCCESS',
+             'free_device_state': 'AVAILABLE'}
         """
         deadline = time.time() + timeout
         get_result_op = remote_operation.GetLastCommandResult(serial)
         while True:
             result = self.SendOperation(get_result_op)
             if result["status"] != "EXECUTING":
-                self.SendOperation(remote_operation.FreeDevice(serial))
                 return result
             if time.time() > deadline:
                 return None
diff --git a/harnesses/host_controller/tradefed/remote_client_test.py b/harnesses/host_controller/tradefed/remote_client_test.py
index 5e1bbbc..c4d2e5d 100644
--- a/harnesses/host_controller/tradefed/remote_client_test.py
+++ b/harnesses/host_controller/tradefed/remote_client_test.py
@@ -144,8 +144,10 @@
     def testExecuteCommand(self):
         """Tests executing a command and waiting for result."""
         self._remote_mgr_thread.AddResponse('{}')
+        self._client.SendOperation(remote_operation.AllocateDevice("serial123"))
         self._remote_mgr_thread.AddResponse('{}')
-        self._client.RunCommand("serial123", "vts", "-m", "SampleShellTest")
+        self._client.SendOperation(remote_operation.ExecuteCommand(
+                "serial123", "vts", "-m", "SampleShellTest"))
 
         self._remote_mgr_thread.AddResponse('{"status": "EXECUTING"}')
         result = self._client.WaitForCommandResult("serial123",
@@ -154,9 +156,10 @@
 
         self._remote_mgr_thread.AddResponse('{"status": "EXECUTING"}')
         self._remote_mgr_thread.AddResponse('{"status": "INVOCATION_SUCCESS"}')
-        self._remote_mgr_thread.AddResponse('{}')
         result = self._client.WaitForCommandResult("serial123",
                                                    timeout=5, poll_interval=1)
+        self._remote_mgr_thread.AddResponse('{}')
+        self._client.SendOperation(remote_operation.FreeDevice("serial123"))
         self.assertIsNotNone(result, "Client doesn't return command result.")
 
     def testSocketError(self):
diff --git a/harnesses/host_controller/tradefed/remote_operation.py b/harnesses/host_controller/tradefed/remote_operation.py
index 3c077bd..a398954 100644
--- a/harnesses/host_controller/tradefed/remote_operation.py
+++ b/harnesses/host_controller/tradefed/remote_operation.py
@@ -27,7 +27,8 @@
 class RemoteOperation(object):
     """The operation sent to TradeFed remote manager.
 
-    An operation is a JSON object with 2 common entries "type" and "version".
+    Args:
+        _obj: A JSON object with at least 2 entries, "type" and "version".
     """
     CURRENT_PROTOCOL_VERSION = 8
 
@@ -38,10 +39,10 @@
             type: A string, the type of the operation.
             **kwargs: The arguments which are specific to the operation type.
         """
-        self.obj = kwargs
-        self.obj["type"] = type
-        if "version" not in self.obj:
-            self.obj["version"] = self.CURRENT_PROTOCOL_VERSION
+        self._obj = kwargs
+        self._obj["type"] = type
+        if "version" not in self._obj:
+            self._obj["version"] = self.CURRENT_PROTOCOL_VERSION
 
     def ParseResponse(self, response_str):
         """Parses the response to the operation.
@@ -60,9 +61,14 @@
             raise RemoteOperationException(response["error"])
         return response
 
+    @property
+    def type(self):
+        """Returns the type of this operation."""
+        return self._obj["type"]
+
     def __str__(self):
         """Converts the JSON object to string."""
-        return json.dumps(self.obj)
+        return json.dumps(self._obj)
 
 
 def ListDevices():
diff --git a/tools/build/tasks/vts_package.mk b/tools/build/tasks/vts_package.mk
index 525c8f3..887a1dd 100644
--- a/tools/build/tasks/vts_package.mk
+++ b/tools/build/tasks/vts_package.mk
@@ -186,6 +186,20 @@
   $(foreach f,$(kernel_rootdir_test_rc_files),\
     system/core/rootdir/$(f):$(VTS_TESTCASES_OUT)/vts/testcases/kernel/api/rootdir/init_rc_files/$(f)) \
 
+acts_framework_files := \
+  $(call find-files-in-subdirs,tools/test/connectivity/acts/framework/acts,"*.py" -and -type f,.)
+
+acts_framework_copy_pairs := \
+  $(foreach f,$(acts_framework_files),\
+    tools/test/connectivity/acts/framework/acts/$(f):$(VTS_TESTCASES_OUT)/acts/$(f))
+
+acts_testcases_files := \
+  $(call find-files-in-subdirs,tools/test/connectivity/acts/tests/google,"*.py" -and -type f,.)
+
+acts_testcases_copy_pairs := \
+  $(foreach f,$(acts_testcases_files),\
+    tools/test/connectivity/acts/tests/google/$(f):$(VTS_TESTCASES_OUT)/vts/testcases/acts/$(f))
+
 $(compatibility_zip): \
   $(call copy-many-files,$(target_native_copy_pairs)) \
   $(call copy-many-files,$(target_spec_copy_pairs)) \
@@ -202,5 +216,7 @@
   $(call copy-many-files,$(performance_test_res_copy_pairs)) \
   $(call copy-many-files,$(audio_test_res_copy_pairs)) \
   $(call copy-many-files,$(kernel_rootdir_test_rc_copy_pairs)) \
+  $(call copy-many-files,$(acts_framework_copy_pairs)) \
+  $(call copy-many-files,$(acts_testcases_copy_pairs)) \
 
 -include vendor/google_vts/tools/build/vts_package_vendor.mk
diff --git a/tools/vts-tradefed/res/push_groups/VtsAgent.push b/tools/vts-tradefed/res/push_groups/VtsAgent.push
index a0321e6..a0c0830 100644
--- a/tools/vts-tradefed/res/push_groups/VtsAgent.push
+++ b/tools/vts-tradefed/res/push_groups/VtsAgent.push
@@ -16,6 +16,7 @@
 DATA/bin/vts_hal_agent32->/data/local/tmp/32/vts_hal_agent32
 DATA/bin/vts_hal_agent64->/data/local/tmp/64/vts_hal_agent64
 
-DATA/bin/vts_testability_checker->/data/local/tmp/vts_testability_checker
+DATA/bin/vts_testability_checker32->/data/local/tmp/vts_testability_checker32
+DATA/bin/vts_testability_checker64->/data/local/tmp/vts_testability_checker64
 
 DATA/app/VtsAgentApp/VtsAgentApp.apk->/data/local/tmp/VtsAgentApp.apk
diff --git a/utils/native/testability_checker/Android.bp b/utils/native/testability_checker/Android.bp
index d66c5be..ed24285 100644
--- a/utils/native/testability_checker/Android.bp
+++ b/utils/native/testability_checker/Android.bp
@@ -56,7 +56,15 @@
     name: "vts_testability_checker",
     defaults : ["VtsTestabilityCheckerDefaults"],
     srcs: ["VtsTestabilityCheckerMain.cpp"],
-
+    multilib: {
+        lib64: {
+            suffix: "64",
+        },
+        lib32: {
+            suffix: "32",
+        },
+    },
+    compile_multilib: "both",
     static_libs: [
         "libhidl-gen-utils",
         "libjsoncpp",
diff --git a/utils/python/hal/hal_service_name_utils.py b/utils/python/hal/hal_service_name_utils.py
index c53e46d..0651a9b 100644
--- a/utils/python/hal/hal_service_name_utils.py
+++ b/utils/python/hal/hal_service_name_utils.py
@@ -19,9 +19,10 @@
 from vts.runners.host import asserts
 from vts.runners.host import const
 
-VTS_TESTABILITY_CHECKER = "/data/local/tmp/vts_testability_checker"
+VTS_TESTABILITY_CHECKER_32 = "/data/local/tmp/vts_testability_checker32"
+VTS_TESTABILITY_CHECKER_64 = "/data/local/tmp/vts_testability_checker64"
 
-def GetHalServiceName(shell, hal, bitness="32", run_as_compliance_test=False):
+def GetHalServiceName(shell, hal, bitness="64", run_as_compliance_test=False):
     """Determine whether to run a VTS test against a HAL and get the service
     names of the given hal if determine to run.
 
@@ -37,7 +38,9 @@
         a set containing all service names for the given HAL.
     """
 
-    cmd = VTS_TESTABILITY_CHECKER
+    cmd = VTS_TESTABILITY_CHECKER_64
+    if bitness == "32":
+        cmd = VTS_TESTABILITY_CHECKER_32
     if run_as_compliance_test:
         cmd += " -c "
     cmd += " -b " + bitness + " " + hal