Fix unregisterListener bug in CarPropertyService.

There is a bug in CarPropertyService that might cause property
unsubscribed from PropertyHalService when unregisterListener
is called even when there are other listeners for the property.

This bug is due to using upateMaxRate == 0 to check whether there
are other clients. However, sampleRate might be 0 for on_change
event so even if updateMaxRate is 0, it doesn't mean the client
array is empty.

Test: Add unit test in CarPropertyServiceTest
Bug: 229327788
Change-Id: Iff5d18a2f5d05ea9be9b8af28908026306a7a99e
diff --git a/service/src/com/android/car/CarPropertyService.java b/service/src/com/android/car/CarPropertyService.java
index 90e404c..df11ba9 100644
--- a/service/src/com/android/car/CarPropertyService.java
+++ b/service/src/com/android/car/CarPropertyService.java
@@ -54,7 +54,7 @@
  */
 public class CarPropertyService extends ICarProperty.Stub
         implements CarServiceBase, PropertyHalService.PropertyHalListener {
-    private static final boolean DBG = true;
+    private static final boolean DBG = false;
     private static final String TAG = CarLog.tagFor(CarPropertyService.class);
     private final Context mContext;
     private final PropertyHalService mHal;
@@ -349,36 +349,35 @@
         if ((client == null) || (propertyClients == null)) {
             Slogf.e(TAG, "unregisterListenerBinderLocked: Listener was not previously "
                     + "registered.");
-        } else {
-            if (propertyClients.remove(client)) {
-                int propLeft = client.removeProperty(propId);
-                if (propLeft == 0) {
-                    mClientMap.remove(listenerBinder);
-                }
-                clearSetOperationRecorderLocked(propId, client);
-
-            } else {
-                Slogf.e(TAG, "unregisterListenerBinderLocked: Listener was not registered for "
-                        + "propId=0x" + toHexString(propId));
-            }
-
-            if (propertyClients.isEmpty()) {
-                // Last listener for this property unsubscribed.  Clean up
-                mPropIdClientMap.remove(propId);
-                mSetOperationClientMap.remove(propId);
-            } else {
-                // Other listeners are still subscribed.  Calculate the new rate
-                for (int i = 0; i < propertyClients.size(); i++) {
-                    Client c = propertyClients.get(i);
-                    float rate = c.getRate(propId);
-                    updateMaxRate = Math.max(rate, updateMaxRate);
-                }
-            }
+            return;
         }
-        if (Float.compare(updateMaxRate, 0f) == 0) {
-            // Unsubscribe property if we did not find any other client register to this property
+        if (propertyClients.remove(client)) {
+            int propLeft = client.removeProperty(propId);
+            if (propLeft == 0) {
+                mClientMap.remove(listenerBinder);
+            }
+            clearSetOperationRecorderLocked(propId, client);
+
+        } else {
+            Slogf.e(TAG, "unregisterListenerBinderLocked: Listener was not registered for "
+                    + "propId=0x" + toHexString(propId));
+            return;
+        }
+
+        if (propertyClients.isEmpty()) {
+            // Last listener for this property unsubscribed.  Clean up
+            mPropIdClientMap.remove(propId);
+            mSetOperationClientMap.remove(propId);
             mHal.unsubscribeProperty(propId);
-        } else if (Float.compare(updateMaxRate, mHal.getSampleRate(propId)) != 0) {
+            return;
+        }
+        // Other listeners are still subscribed.  Calculate the new rate
+        for (int i = 0; i < propertyClients.size(); i++) {
+            Client c = propertyClients.get(i);
+            float rate = c.getRate(propId);
+            updateMaxRate = Math.max(rate, updateMaxRate);
+        }
+        if (Float.compare(updateMaxRate, mHal.getSampleRate(propId)) != 0) {
             try {
                 // Only reset the sample rate if needed
                 mHal.subscribeProperty(propId, updateMaxRate);
diff --git a/tests/carservice_unit_test/src/com/android/car/CarPropertyServiceUnitTest.java b/tests/carservice_unit_test/src/com/android/car/CarPropertyServiceUnitTest.java
new file mode 100644
index 0000000..a60201f
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/CarPropertyServiceUnitTest.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+package com.android.car;
+
+import static android.car.hardware.property.CarPropertyManager.SENSOR_RATE_ONCHANGE;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.car.VehicleAreaType;
+import android.car.VehiclePropertyIds;
+import android.car.hardware.CarPropertyConfig;
+import android.car.hardware.CarPropertyValue;
+import android.car.hardware.property.ICarPropertyEventListener;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+import android.util.SparseArray;
+
+import com.android.car.hal.PropertyHalService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class CarPropertyServiceUnitTest {
+
+    @Mock
+    private Context mContext;
+    @Mock
+    private PropertyHalService mHalService;
+
+    private String mReadPermission;
+    private CarPropertyService mService;
+
+    private static final int SPEED_ID = VehiclePropertyIds.PERF_VEHICLE_SPEED;
+    private static final int HVAC_TEMP = VehiclePropertyIds.HVAC_TEMPERATURE_SET;
+
+    @Before
+    public void setUp() {
+        mReadPermission = "READ_PERMISSION";
+
+        when(mContext.checkCallingOrSelfPermission(mReadPermission)).thenReturn(
+                PackageManager.PERMISSION_GRANTED);
+
+        SparseArray<CarPropertyConfig<?>> configs = new SparseArray<>();
+        configs.put(SPEED_ID, CarPropertyConfig.newBuilder(
+                Float.class, SPEED_ID,
+                VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL).build());
+        when(mHalService.getReadPermission(SPEED_ID)).thenReturn(mReadPermission);
+        // HVAC_TEMP is actually not a global property, but for simplicity, make it global here.
+        configs.put(HVAC_TEMP, CarPropertyConfig.newBuilder(
+                Float.class, HVAC_TEMP,
+                VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL).build());
+        when(mHalService.getReadPermission(HVAC_TEMP)).thenReturn(mReadPermission);
+        when(mHalService.getPropertyList()).thenReturn(configs);
+
+        mService = new CarPropertyService(mContext, mHalService);
+        mService.init();
+    }
+
+    @Test
+    public void testRegisterListenerNull() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mService.registerListener(SPEED_ID, /* rate=*/ 10, /* listener= */ null));
+    }
+
+    @Test
+    public void testRegisterUnregisterForContinuousProperty() throws Exception {
+        ICarPropertyEventListener mMockHandler1 = mock(ICarPropertyEventListener.class);
+        ICarPropertyEventListener mMockHandler2 = mock(ICarPropertyEventListener.class);
+        // Must use two different binders because listener is uniquely identified by binder.
+        IBinder mBinder1 = mock(IBinder.class);
+        IBinder mBinder2 = mock(IBinder.class);
+        when(mMockHandler1.asBinder()).thenReturn(mBinder1);
+        when(mMockHandler2.asBinder()).thenReturn(mBinder2);
+        // Initially SPEED_ID is not subscribed, so should return -1.
+        when(mHalService.getSampleRate(SPEED_ID)).thenReturn(-1f);
+        CarPropertyValue mValue = mock(CarPropertyValue.class);
+        when(mHalService.getPropertySafe(SPEED_ID, 0)).thenReturn(mValue);
+
+        // Register the first listener.
+        mService.registerListener(SPEED_ID, /* rate= */ 10, mMockHandler1);
+
+        // Wait until we get the on property change event for the initial value.
+        verify(mMockHandler1, timeout(5000)).onEvent(any());
+
+        verify(mHalService).subscribeProperty(SPEED_ID, 10f);
+        verify(mHalService).getPropertySafe(SPEED_ID, 0);
+
+        // Clean up invocation state.
+        clearInvocations(mHalService);
+        when(mHalService.getSampleRate(SPEED_ID)).thenReturn(10f);
+
+        // Register the second listener.
+        mService.registerListener(SPEED_ID, /* rate= */ 20, mMockHandler2);
+
+        // Wait until we get the on property change event for the initial value.
+        verify(mMockHandler2, timeout(5000)).onEvent(any());
+
+        verify(mHalService).subscribeProperty(SPEED_ID, 20f);
+        verify(mHalService).getPropertySafe(SPEED_ID, 0);
+
+        // Clean up invocation state.
+        clearInvocations(mHalService);
+        when(mHalService.getSampleRate(SPEED_ID)).thenReturn(20f);
+
+        // Unregister the second listener, the first listener must still be registered.
+        mService.unregisterListener(SPEED_ID, mMockHandler2);
+
+        // The property must not be unsubscribed.
+        verify(mHalService, never()).unsubscribeProperty(anyInt());
+        // The subscription rate must be updated.
+        verify(mHalService).subscribeProperty(SPEED_ID, 10f);
+        when(mHalService.getSampleRate(SPEED_ID)).thenReturn(10f);
+
+        // Unregister the first listener. We have no more listeners, must cause unsubscription.
+        mService.unregisterListener(SPEED_ID, mMockHandler1);
+
+        verify(mHalService).unsubscribeProperty(SPEED_ID);
+    }
+
+    @Test
+    public void testRegisterUnregisterForOnChangeProperty() throws Exception {
+        ICarPropertyEventListener mMockHandler1 = mock(ICarPropertyEventListener.class);
+        ICarPropertyEventListener mMockHandler2 = mock(ICarPropertyEventListener.class);
+        // Must use two different binders because listener is uniquely identified by binder.
+        IBinder mBinder1 = mock(IBinder.class);
+        IBinder mBinder2 = mock(IBinder.class);
+        when(mMockHandler1.asBinder()).thenReturn(mBinder1);
+        when(mMockHandler2.asBinder()).thenReturn(mBinder2);
+        // Initially HVAC_TEMP is not subscribed, so should return -1.
+        when(mHalService.getSampleRate(HVAC_TEMP)).thenReturn(-1f);
+        CarPropertyValue mValue = mock(CarPropertyValue.class);
+        when(mHalService.getPropertySafe(HVAC_TEMP, 0)).thenReturn(mValue);
+
+        // Register the first listener.
+        mService.registerListener(HVAC_TEMP, /* rate= */ SENSOR_RATE_ONCHANGE, mMockHandler1);
+
+        // Wait until we get the on property change event for the initial value.
+        verify(mMockHandler1, timeout(5000)).onEvent(any());
+
+        verify(mHalService).subscribeProperty(HVAC_TEMP, 0f);
+        verify(mHalService).getPropertySafe(HVAC_TEMP, 0);
+
+        // Clean up invocation state.
+        clearInvocations(mHalService);
+        when(mHalService.getSampleRate(HVAC_TEMP)).thenReturn(0f);
+
+        // Register the second listener.
+        mService.registerListener(HVAC_TEMP, /* rate= */ SENSOR_RATE_ONCHANGE, mMockHandler2);
+
+        // Wait until we get the on property change event for the initial value.
+        verify(mMockHandler2, timeout(5000)).onEvent(any());
+
+        // Must not subscribe again.
+        verify(mHalService, never()).subscribeProperty(anyInt(), anyFloat());
+        verify(mHalService).getPropertySafe(HVAC_TEMP, 0);
+
+        // Clean up invocation state.
+        clearInvocations(mHalService);
+
+        // Unregister the second listener, the first listener must still be registered.
+        mService.unregisterListener(HVAC_TEMP, mMockHandler2);
+
+        // The property must not be unsubscribed.
+        verify(mHalService, never()).unsubscribeProperty(anyInt());
+
+        // Unregister the first listener. We have no more listeners, must cause unsubscription.
+        mService.unregisterListener(HVAC_TEMP, mMockHandler1);
+
+        verify(mHalService).unsubscribeProperty(HVAC_TEMP);
+    }
+}