Dump VMS HAL client stats obtained by reading a vendor property.

Bug: 139153868
Test: Added new unit tests to VmsHalServiceTest
Test: atest AndroidCarApiTest CarServiceTest CarServiceUnitTest
Change-Id: I97fcd730c7433ea6753719207d967e119bcc6a4f
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 95383dc..370fe5d 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -78,6 +78,8 @@
     </string-array>
     <!-- Default home activity -->
     <string name="defaultHomeActivity" translatable="false"><!--com.your.package/com.your.package.Activity--></string>
+    <!-- The vendor-defined HAL property used to collect VMS client metrics. Disabled by default.-->
+    <integer name="vmsHalClientMetricsProperty">0</integer>
     <!--  The com.android.car.vms.VmsClientManager will bind to this list of clients running as system user -->
     <string-array translatable="false" name="vmsPublisherSystemClients">
     </string-array>
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 5c170d9..8527208 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -118,7 +118,7 @@
             CanBusErrorNotifier errorNotifier, String vehicleInterfaceName) {
         mContext = serviceContext;
         mSystemInterface = systemInterface;
-        mHal = new VehicleHal(vehicle);
+        mHal = new VehicleHal(serviceContext, vehicle);
         mVehicleInterfaceName = vehicleInterfaceName;
         mUserManagerHelper = new CarUserManagerHelper(serviceContext);
         final Resources res = mContext.getResources();
@@ -473,6 +473,8 @@
         } else if ("--metrics".equals(args[0])) {
             writer.println("*Dump car service metrics*");
             dumpAllServices(writer, true);
+        } else if ("--vms-hal".equals(args[0])) {
+            mHal.getVmsHal().dumpMetrics(fd);
         } else if (Build.IS_USERDEBUG || Build.IS_ENG) {
             execShellCmd(args, writer);
         } else {
diff --git a/service/src/com/android/car/hal/VehicleHal.java b/service/src/com/android/car/hal/VehicleHal.java
index 374ae7b..d85a357 100644
--- a/service/src/com/android/car/hal/VehicleHal.java
+++ b/service/src/com/android/car/hal/VehicleHal.java
@@ -23,6 +23,7 @@
 import static java.lang.Integer.toHexString;
 
 import android.annotation.CheckResult;
+import android.content.Context;
 import android.hardware.automotive.vehicle.V2_0.IVehicle;
 import android.hardware.automotive.vehicle.V2_0.IVehicleCallback;
 import android.hardware.automotive.vehicle.V2_0.SubscribeFlags;
@@ -89,14 +90,14 @@
     // Used by injectVHALEvent for testing purposes.  Delimiter for an array of data
     private static final String DATA_DELIMITER = ",";
 
-    public VehicleHal(IVehicle vehicle) {
+    public VehicleHal(Context context, IVehicle vehicle) {
         mHandlerThread = new HandlerThread("VEHICLE-HAL");
         mHandlerThread.start();
         // passing this should be safe as long as it is just kept and not used in constructor
         mPowerHal = new PowerHalService(this);
         mPropertyHal = new PropertyHalService(this);
         mInputHal = new InputHalService(this);
-        mVmsHal = new VmsHalService(this);
+        mVmsHal = new VmsHalService(context, this);
         mDiagnosticHal = new DiagnosticHalService(this);
         mAllServices.addAll(Arrays.asList(mPowerHal,
                 mInputHal,
diff --git a/service/src/com/android/car/hal/VmsHalService.java b/service/src/com/android/car/hal/VmsHalService.java
index fcf717f..40982c8 100644
--- a/service/src/com/android/car/hal/VmsHalService.java
+++ b/service/src/com/android/car/hal/VmsHalService.java
@@ -31,9 +31,11 @@
 import android.car.vms.VmsLayersOffering;
 import android.car.vms.VmsOperationRecorder;
 import android.car.vms.VmsSubscriptionState;
+import android.content.Context;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropConfig;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
 import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropertyGroup;
 import android.hardware.automotive.vehicle.V2_0.VmsBaseMessageIntegerValuesIndex;
 import android.hardware.automotive.vehicle.V2_0.VmsMessageType;
 import android.hardware.automotive.vehicle.V2_0.VmsMessageWithLayerAndPublisherIdIntegerValuesIndex;
@@ -55,6 +57,9 @@
 import com.android.car.CarLog;
 import com.android.car.vms.VmsClientManager;
 
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -81,6 +86,7 @@
     private final VehicleHal mVehicleHal;
     private final int mCoreId;
     private final MessageQueue mMessageQueue;
+    private final int mClientMetricsProperty;
     private volatile boolean mIsSupported = false;
 
     private VmsClientManager mClientManager;
@@ -192,15 +198,33 @@
     /**
      * Constructor used by {@link VehicleHal}
      */
-    VmsHalService(VehicleHal vehicleHal) {
-        this(vehicleHal, SystemClock::uptimeMillis);
+    VmsHalService(Context context, VehicleHal vehicleHal) {
+        this(context, vehicleHal, SystemClock::uptimeMillis);
     }
 
     @VisibleForTesting
-    VmsHalService(VehicleHal vehicleHal, Supplier<Long> getCoreId) {
+    VmsHalService(Context context, VehicleHal vehicleHal, Supplier<Long> getCoreId) {
         mVehicleHal = vehicleHal;
         mCoreId = (int) (getCoreId.get() % Integer.MAX_VALUE);
         mMessageQueue = new MessageQueue();
+        mClientMetricsProperty = getClientMetricsProperty(context);
+    }
+
+    private static int getClientMetricsProperty(Context context) {
+        int propId = context.getResources().getInteger(
+                com.android.car.R.integer.vmsHalClientMetricsProperty);
+        if (propId == 0) {
+            Log.i(TAG, "Metrics collection disabled");
+            return 0;
+        }
+        if ((propId & VehiclePropertyGroup.MASK) != VehiclePropertyGroup.VENDOR) {
+            Log.w(TAG, String.format("Metrics collection disabled, non-vendor property: 0x%x",
+                    propId));
+            return 0;
+        }
+
+        Log.i(TAG, String.format("Metrics collection property: 0x%x", propId));
+        return propId;
     }
 
     /**
@@ -289,6 +313,37 @@
     }
 
     /**
+     * Dumps HAL client metrics obtained by reading the VMS HAL property.
+     *
+     * @param fd Dumpsys file descriptor to write client metrics to.
+     */
+    public void dumpMetrics(FileDescriptor fd) {
+        if (mClientMetricsProperty == 0) {
+            Log.w(TAG, "Metrics collection is disabled");
+            return;
+        }
+
+        VehiclePropValue vehicleProp = null;
+        try {
+            vehicleProp = mVehicleHal.get(mClientMetricsProperty);
+        } catch (PropertyTimeoutException e) {
+            Log.e(TAG, "Timeout while reading metrics from client");
+        }
+        if (vehicleProp == null) {
+            if (DBG) Log.d(TAG, "Metrics unavailable");
+            return;
+        }
+
+        FileOutputStream fout = new FileOutputStream(fd);
+        try {
+            fout.write(toByteArray(vehicleProp.value.bytes));
+            fout.flush();
+        } catch (IOException e) {
+            Log.e(TAG, "Error writing metrics to output stream");
+        }
+    }
+
+    /**
      * Consumes/produces HAL messages.
      *
      * The format of these messages is defined in:
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/VmsHalServiceTest.java b/tests/carservice_unit_test/src/com/android/car/hal/VmsHalServiceTest.java
index 2ef469c..7571867 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/VmsHalServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/VmsHalServiceTest.java
@@ -15,10 +15,13 @@
  */
 package com.android.car.hal;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 import android.car.vms.IVmsPublisherClient;
@@ -31,15 +34,20 @@
 import android.car.vms.VmsLayerDependency;
 import android.car.vms.VmsLayersOffering;
 import android.car.vms.VmsSubscriptionState;
+import android.content.Context;
+import android.content.res.Resources;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropConfig;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
 import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropertyGroup;
 import android.hardware.automotive.vehicle.V2_0.VmsMessageType;
 import android.os.Binder;
 import android.os.IBinder;
 
 import androidx.test.filters.RequiresDevice;
 
+import com.android.car.R;
+import com.android.car.test.utils.TemporaryFile;
 import com.android.car.vms.VmsClientManager;
 
 import org.junit.Before;
@@ -52,6 +60,9 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashSet;
@@ -73,6 +84,10 @@
     @Rule
     public MockitoRule mockito = MockitoJUnit.rule();
     @Mock
+    private Context mContext;
+    @Mock
+    private Resources mResources;
+    @Mock
     private VehicleHal mVehicleHal;
     @Mock
     private VmsClientManager mClientManager;
@@ -88,7 +103,8 @@
 
     @Before
     public void setUp() throws Exception {
-        mHalService = new VmsHalService(mVehicleHal, () -> (long) CORE_ID);
+        when(mContext.getResources()).thenReturn(mResources);
+        mHalService = new VmsHalService(mContext, mVehicleHal, () -> (long) CORE_ID);
         mHalService.setClientManager(mClientManager);
         mHalService.setVmsSubscriberService(mSubscriberService);
 
@@ -148,7 +164,8 @@
 
     @Test
     public void testCoreId_IntegerOverflow() throws Exception {
-        mHalService = new VmsHalService(mVehicleHal, () -> (long) Integer.MAX_VALUE + CORE_ID);
+        mHalService = new VmsHalService(mContext, mVehicleHal,
+                () -> (long) Integer.MAX_VALUE + CORE_ID);
 
         VehiclePropConfig propConfig = new VehiclePropConfig();
         propConfig.prop = VehicleProperty.VEHICLE_MAP_SERVICE;
@@ -944,6 +961,80 @@
         verify(mVehicleHal).set(response);
     }
 
+    @Test
+    public void testDumpMetrics_DefaultConfig() {
+        mHalService.dumpMetrics(new FileDescriptor());
+        verifyZeroInteractions(mVehicleHal);
+    }
+
+    @Test
+    public void testDumpMetrics_NonVendorProperty() throws Exception {
+        VehiclePropValue vehicleProp = new VehiclePropValue();
+        vehicleProp.value.bytes.addAll(PAYLOAD_AS_LIST);
+        when(mVehicleHal.get(anyInt())).thenReturn(vehicleProp);
+
+        when(mResources.getInteger(
+                R.integer.vmsHalClientMetricsProperty)).thenReturn(
+                VehicleProperty.VEHICLE_MAP_SERVICE);
+        setUp();
+
+        mHalService.dumpMetrics(new FileDescriptor());
+        verifyZeroInteractions(mVehicleHal);
+    }
+
+    @Test
+    public void testDumpMetrics_VendorProperty() throws Exception {
+        int metricsPropertyId = VehiclePropertyGroup.VENDOR | 1;
+        when(mResources.getInteger(
+                R.integer.vmsHalClientMetricsProperty)).thenReturn(
+                metricsPropertyId);
+        setUp();
+
+        VehiclePropValue metricsProperty = new VehiclePropValue();
+        metricsProperty.value.bytes.addAll(PAYLOAD_AS_LIST);
+        when(mVehicleHal.get(metricsPropertyId)).thenReturn(metricsProperty);
+
+        try (TemporaryFile dumpsysFile = new TemporaryFile("VmsHalServiceTest")) {
+            FileOutputStream outputStream = new FileOutputStream(dumpsysFile.getFile());
+            mHalService.dumpMetrics(outputStream.getFD());
+
+            verify(mVehicleHal).get(metricsPropertyId);
+            FileInputStream inputStream = new FileInputStream(dumpsysFile.getFile());
+            byte[] dumpsysOutput = new byte[PAYLOAD.length];
+            assertEquals(PAYLOAD.length, inputStream.read(dumpsysOutput));
+            assertArrayEquals(PAYLOAD, dumpsysOutput);
+        }
+    }
+
+    @Test
+    public void testDumpMetrics_VendorProperty_Timeout() throws Exception {
+        int metricsPropertyId = VehiclePropertyGroup.VENDOR | 1;
+        when(mResources.getInteger(
+                R.integer.vmsHalClientMetricsProperty)).thenReturn(
+                metricsPropertyId);
+        setUp();
+
+        when(mVehicleHal.get(metricsPropertyId))
+                .thenThrow(new PropertyTimeoutException(metricsPropertyId));
+
+        mHalService.dumpMetrics(new FileDescriptor());
+        verify(mVehicleHal).get(metricsPropertyId);
+    }
+
+    @Test
+    public void testDumpMetrics_VendorProperty_Unavailable() throws Exception {
+        int metricsPropertyId = VehiclePropertyGroup.VENDOR | 1;
+        when(mResources.getInteger(
+                R.integer.vmsHalClientMetricsProperty)).thenReturn(
+                metricsPropertyId);
+        setUp();
+
+        when(mVehicleHal.get(metricsPropertyId)).thenReturn(null);
+
+        mHalService.dumpMetrics(new FileDescriptor());
+        verify(mVehicleHal).get(metricsPropertyId);
+    }
+
     private static VehiclePropValue createHalMessage(Integer... message) {
         VehiclePropValue result = new VehiclePropValue();
         result.prop = VehicleProperty.VEHICLE_MAP_SERVICE;