/*
 * Copyright (C) 2019 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.hal;

import static android.car.Car.PERMISSION_VENDOR_EXTENSION;
import static android.car.VehiclePropertyIds.EPOCH_TIME;
import static android.car.VehiclePropertyIds.EV_CHARGE_CURRENT_DRAW_LIMIT;
import static android.car.VehiclePropertyIds.EV_CHARGE_PERCENT_LIMIT;
import static android.car.VehiclePropertyIds.HVAC_FAN_DIRECTION;
import static android.car.VehiclePropertyIds.HVAC_FAN_SPEED;
import static android.car.VehiclePropertyIds.HVAC_TEMPERATURE_SET;
import static android.car.VehiclePropertyIds.INFO_FUEL_DOOR_LOCATION;
import static android.car.VehiclePropertyIds.PERF_VEHICLE_SPEED;
import static android.car.VehiclePropertyIds.VEHICLE_SPEED_DISPLAY_UNITS;
import static android.car.feature.Flags.FLAG_PROPERTY_VALUE_USE_DIRECT_EXECUTOR;
import static android.car.hardware.CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ;
import static android.car.hardware.property.VehicleHalStatusCode.STATUS_INTERNAL_ERROR;
import static android.car.hardware.property.VehicleHalStatusCode.STATUS_NOT_AVAILABLE;
import static android.car.hardware.property.VehicleVendorPermission.PERMISSION_GET_CAR_VENDOR_CATEGORY_ENGINE;
import static android.car.hardware.property.VehicleVendorPermission.PERMISSION_GET_CAR_VENDOR_CATEGORY_INFO;
import static android.car.hardware.property.VehicleVendorPermission.PERMISSION_SET_CAR_VENDOR_CATEGORY_ENGINE;
import static android.hardware.automotive.vehicle.VehicleProperty.SUPPORT_CUSTOMIZE_VENDOR_PERMISSION;

import static com.android.car.internal.property.CarPropertyErrorCodes.STATUS_OK;
import static com.android.car.internal.property.CarPropertyErrorCodes.STATUS_OK_NO_ERROR;
import static com.android.car.internal.property.CarPropertyErrorCodes.ERROR_CODES_TRY_AGAIN;
import static com.android.car.internal.property.CarPropertyErrorCodes.ERROR_CODES_INTERNAL;
import static com.android.car.internal.property.CarPropertyHelper.newPropIdAreaId;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.car.VehiclePropertyIds;
import android.car.hardware.CarPropertyConfig;
import android.car.hardware.CarPropertyValue;
import android.car.hardware.property.AreaIdConfig;
import android.car.hardware.property.CarPropertyManager;
import android.car.hardware.property.ICarPropertyEventListener;
import android.car.test.AbstractExpectableTestCase;
import android.car.test.NoActiveHandlerThreadCheckerRule;
import android.hardware.automotive.vehicle.RawPropValues;
import android.hardware.automotive.vehicle.VehicleAreaConfig;
import android.hardware.automotive.vehicle.VehicleAreaSeat;
import android.hardware.automotive.vehicle.VehiclePropConfig;
import android.hardware.automotive.vehicle.VehiclePropError;
import android.hardware.automotive.vehicle.VehicleProperty;
import android.hardware.automotive.vehicle.VehiclePropertyAccess;
import android.hardware.automotive.vehicle.VehiclePropertyChangeMode;
import android.hardware.automotive.vehicle.VehiclePropertyStatus;
import android.hardware.automotive.vehicle.VehicleVendorPermission;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.util.ArraySet;

import androidx.test.runner.AndroidJUnit4;

import com.android.car.VehicleStub.AsyncGetSetRequest;
import com.android.car.VehicleStub.GetVehicleStubAsyncResult;
import com.android.car.VehicleStub.MinMaxSupportedRawPropValues;
import com.android.car.VehicleStub.SetVehicleStubAsyncResult;
import com.android.car.VehicleStub.VehicleStubCallbackInterface;
import com.android.car.hal.VehicleHal.HalSubscribeOptions;
import com.android.car.hal.property.PropertyHalServiceConfigs;
import com.android.car.hal.test.AidlVehiclePropValueBuilder;
import com.android.car.internal.property.AsyncPropertyServiceRequest;
import com.android.car.internal.property.CarPropertyErrorCodes;
import com.android.car.internal.property.CarSubscription;
import com.android.car.internal.property.GetSetValueResult;
import com.android.car.internal.property.GetSetValueResultList;
import com.android.car.internal.property.IAsyncPropertyResultCallback;
import com.android.car.internal.property.ISupportedValuesChangeCallback;
import com.android.car.internal.property.PropIdAreaId;
import com.android.car.internal.property.RawPropertyValue;
import com.android.internal.annotations.VisibleForTesting;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.stubbing.Answer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

@RunWith(AndroidJUnit4.class)
public class PropertyHalServiceTest extends AbstractExpectableTestCase{
    @Rule
    public MockitoRule mMockitoRule = MockitoJUnit.rule();
    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    @Rule
    public NoActiveHandlerThreadCheckerRule mNoActiveHandlerThreadCheckerRule =
            new NoActiveHandlerThreadCheckerRule();

    @Mock
    private VehicleHal mVehicleHal;

    @Mock
    private IAsyncPropertyResultCallback mGetAsyncPropertyResultCallback;
    @Mock
    private IAsyncPropertyResultCallback mSetAsyncPropertyResultCallback;
    @Mock
    private IBinder mGetAsyncPropertyResultBinder;
    @Mock
    private IBinder mSetAsyncPropertyResultBinder;
    @Mock
    private PropertyHalService.PropertyHalListener mPropertyHalListener;
    @Captor
    private ArgumentCaptor<GetSetValueResultList> mAsyncResultCaptor;
    @Captor
    private ArgumentCaptor<IBinder.DeathRecipient> mDeathRecipientCaptor;
    @Captor
    private ArgumentCaptor<List> mListArgumentCaptor;
    @Mock
    private CarPropertyConfig mMockCarPropertyConfig1;
    @Mock
    private CarPropertyConfig mMockCarPropertyConfig2;
    @Mock
    private ISupportedValuesChangeCallback mSupportedValuesChangeCallback;
    @Mock
    private IBinder mSupportedValuesChangeCallbackBinder;
    @Mock
    private HalPropConfig mMockPropConfig1;
    @Mock
    private HalPropConfig mMockPropConfig2;
    @Mock
    private ICarPropertyEventListener mCallback;

    private PropertyHalService mPropertyHalService;
    private static final int REQUEST_ID_1 = 1;
    private static final int REQUEST_ID_2 = 2;
    private static final int REQUEST_ID_3 = 3;
    private static final int REQUEST_ID_4 = 4;
    private static final int REQUEST_ID_5 = 5;
    private static final int RECEIVED_REQUEST_ID_1 = 0;
    private static final int RECEIVED_REQUEST_ID_2 = 1;
    private static final int RECEIVED_REQUEST_ID_3 = 2;
    private static final int INT32_PROP = VehiclePropertyIds.INFO_FUEL_DOOR_LOCATION;
    private static final int PROPERTY_VALUE = 123;
    private static final int VENDOR_ERROR_CODE = 1234;
    private static final int SYSTEM_ERROR_CODE = 4321;
    private static final int VENDOR_PROPERTY_1 = 0x21e01111;
    private static final int VENDOR_PROPERTY_2 = 0x21e01112;
    private static final int VENDOR_PROPERTY_3 = 0x21e01113;
    private static final float SAMPLE_RATE_HZ = 17.0f;
    private static final AsyncPropertyServiceRequest GET_PROPERTY_SERVICE_REQUEST_1 =
            new AsyncPropertyServiceRequest(REQUEST_ID_1, HVAC_TEMPERATURE_SET, /* areaId= */ 0);
    private static final AsyncPropertyServiceRequest GET_PROPERTY_SERVICE_REQUEST_2 =
            new AsyncPropertyServiceRequest(REQUEST_ID_2, HVAC_TEMPERATURE_SET, /* areaId= */ 0);
    private static final AsyncPropertyServiceRequest GET_PROPERTY_SERVICE_REQUEST_STATIC_1 =
            new AsyncPropertyServiceRequest(REQUEST_ID_1, INT32_PROP, /* areaId= */ 0);
    private static final AsyncPropertyServiceRequest SET_HVAC_REQUEST_ID_1 =
            new AsyncPropertyServiceRequest(REQUEST_ID_1, HVAC_TEMPERATURE_SET, /* areaId= */ 0,
                    new CarPropertyValue(HVAC_TEMPERATURE_SET, /* areaId= */ 0, SAMPLE_RATE_HZ));
    private static final AsyncPropertyServiceRequest SET_SPEED_REQUEST_ID_2 =
            new AsyncPropertyServiceRequest(REQUEST_ID_2, PERF_VEHICLE_SPEED, /* areaId= */ 0,
                    new CarPropertyValue(PERF_VEHICLE_SPEED, /* areaId= */ 0, SAMPLE_RATE_HZ));
    private static final AsyncPropertyServiceRequest SET_SPEED_REQUEST_ID_3 =
            new AsyncPropertyServiceRequest(REQUEST_ID_3, PERF_VEHICLE_SPEED, /* areaId= */ 0,
                    new CarPropertyValue(PERF_VEHICLE_SPEED, /* areaId= */ 0, SAMPLE_RATE_HZ));
    private static final AsyncPropertyServiceRequest SET_VEHICLE_SPEED_AREA_ID_1_REQUEST =
            new AsyncPropertyServiceRequest(REQUEST_ID_4, PERF_VEHICLE_SPEED, /* areaId= */ 1,
                    new CarPropertyValue(PERF_VEHICLE_SPEED, /* areaId= */ 1, SAMPLE_RATE_HZ));
    private static final AsyncPropertyServiceRequest SET_VEHICLE_SPEED_AREA_ID_2_REQUEST =
            new AsyncPropertyServiceRequest(REQUEST_ID_5, PERF_VEHICLE_SPEED, /* areaId= */ 2,
                    new CarPropertyValue(PERF_VEHICLE_SPEED, /* areaId= */ 2, SAMPLE_RATE_HZ));

    private static final long TEST_UPDATE_TIMESTAMP_NANOS = 1234;
    private final HalPropValueBuilder mPropValueBuilder = new HalPropValueBuilder(
            /* isAidl= */ true);
    private final HalPropValue mPropValue = mPropValueBuilder.build(
            HVAC_TEMPERATURE_SET, /* areaId= */ 0, TEST_UPDATE_TIMESTAMP_NANOS, /* status= */ 0,
            SAMPLE_RATE_HZ);
    private final HalPropValue mNonTargetPropValue = mPropValueBuilder.build(
            HVAC_TEMPERATURE_SET, /* areaId= */ 0, TEST_UPDATE_TIMESTAMP_NANOS, /* status= */ 0,
            16.0f);
    private final HalPropValue mPropValue2 = mPropValueBuilder.build(
            PERF_VEHICLE_SPEED, /* areaId= */ 0, TEST_UPDATE_TIMESTAMP_NANOS, /* status= */ 0,
            SAMPLE_RATE_HZ);
    private final HalPropValue mPropValue3 = mPropValueBuilder.build(
            INT32_PROP, /* areaId= */ 0, TEST_UPDATE_TIMESTAMP_NANOS, /* status= */ 0,
            3);

    private AsyncPropertyServiceRequest copyRequest(AsyncPropertyServiceRequest request) {
        return new AsyncPropertyServiceRequest(request.getRequestId(), request.getPropertyId(),
                request.getAreaId(), request.getCarPropertyValue());
    }

    @Before
    public void setUp() throws Exception {
        when(mVehicleHal.getHalPropValueBuilder()).thenReturn(mPropValueBuilder);
        mPropertyHalService = new PropertyHalService(mVehicleHal);
        mPropertyHalService.setPropertyHalServiceConfigs(PropertyHalServiceConfigs.newConfigs());
        mPropertyHalService.init();

        // HVAC_TEMPERATURE_SET, ON_CHANGE, global
        when(mMockPropConfig1.getPropId()).thenReturn(VehicleProperty.HVAC_TEMPERATURE_SET);
        when(mMockPropConfig1.getChangeMode()).thenReturn(VehiclePropertyChangeMode.ON_CHANGE);
        when(mMockPropConfig1.toCarPropertyConfig(eq(VehicleProperty.HVAC_TEMPERATURE_SET), any()))
                .thenReturn(mMockCarPropertyConfig1);
        when(mMockCarPropertyConfig1.getChangeMode())
                .thenReturn(VehiclePropertyChangeMode.ON_CHANGE);
        when(mMockCarPropertyConfig1.getAreaIds()).thenReturn(new int[]{0});

        // PERF_VEHICLE_SPEED, CONTINUOUS, global
        when(mMockPropConfig2.getPropId()).thenReturn(VehicleProperty.PERF_VEHICLE_SPEED);
        when(mMockPropConfig2.getChangeMode()).thenReturn(VehiclePropertyChangeMode.CONTINUOUS);
        when(mMockPropConfig2.getMinSampleRate()).thenReturn(20.0f);
        when(mMockPropConfig2.getMaxSampleRate()).thenReturn(100.0f);
        when(mMockPropConfig2.toCarPropertyConfig(eq(VehicleProperty.PERF_VEHICLE_SPEED), any()))
                .thenReturn(mMockCarPropertyConfig2);
        when(mMockCarPropertyConfig2.getChangeMode())
                .thenReturn(VehiclePropertyChangeMode.CONTINUOUS);
        when(mMockCarPropertyConfig2.getMinSampleRate()).thenReturn(20.0f);
        when(mMockCarPropertyConfig2.getMaxSampleRate()).thenReturn(100.0f);
        when(mMockCarPropertyConfig2.getAreaIds()).thenReturn(new int[]{0});

        HalPropConfig mockPropConfig3 = mock(HalPropConfig.class);
        when(mockPropConfig3.getChangeMode()).thenReturn(VehiclePropertyChangeMode.STATIC);
        when(mockPropConfig3.getPropId()).thenReturn(INT32_PROP);

        // HVAC_FAN_SPEED, ON_CHANGE, area IDs: SEAT_ROW_1_LEFT/SEAT_ROW_1_RIGHT
        var vehiclePropConfig4 = new VehiclePropConfig();
        vehiclePropConfig4.prop = HVAC_FAN_SPEED;
        vehiclePropConfig4.access = VehiclePropertyAccess.READ;
        vehiclePropConfig4.changeMode = VehiclePropertyChangeMode.ON_CHANGE;
        vehiclePropConfig4.areaConfigs = new VehicleAreaConfig[2];
        vehiclePropConfig4.areaConfigs[0] = new VehicleAreaConfig();
        vehiclePropConfig4.areaConfigs[0].areaId = VehicleAreaSeat.ROW_1_LEFT;
        vehiclePropConfig4.areaConfigs[0].access = VehiclePropertyAccess.READ;
        vehiclePropConfig4.areaConfigs[1] = new VehicleAreaConfig();
        vehiclePropConfig4.areaConfigs[1].areaId = VehicleAreaSeat.ROW_1_RIGHT;
        vehiclePropConfig4.areaConfigs[1].access = VehiclePropertyAccess.READ;
        HalPropConfig propConfig4 = new AidlHalPropConfig(vehiclePropConfig4);

        // VEHICLE_SPEED_DISPLAY_UNIT, ON_CHANGE, global
        var vehiclePropConfig5 = new VehiclePropConfig();
        vehiclePropConfig5.prop = VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS;
        vehiclePropConfig5.access = VehiclePropertyAccess.READ;
        vehiclePropConfig5.changeMode = VehiclePropertyChangeMode.ON_CHANGE;
        vehiclePropConfig5.areaConfigs = new VehicleAreaConfig[1];
        vehiclePropConfig5.areaConfigs[0] = new VehicleAreaConfig();
        vehiclePropConfig5.areaConfigs[0].areaId = 0;
        vehiclePropConfig5.areaConfigs[0].access = VehiclePropertyAccess.READ;
        HalPropConfig propConfig5 = new AidlHalPropConfig(vehiclePropConfig5);

        // EPOCH_TIME
        var vehiclePropConfig6 = new VehiclePropConfig();
        vehiclePropConfig6.prop = VehicleProperty.ANDROID_EPOCH_TIME;
        vehiclePropConfig6.access = VehiclePropertyAccess.READ;
        vehiclePropConfig6.changeMode = VehiclePropertyChangeMode.ON_CHANGE;
        HalPropConfig propConfig6 = new AidlHalPropConfig(vehiclePropConfig6);

        mPropertyHalService.takeProperties(List.of(mMockPropConfig1, mMockPropConfig2,
                mockPropConfig3, propConfig4, propConfig5, propConfig6));
        mPropertyHalService.getPropertyList();

        when(mSupportedValuesChangeCallback.asBinder()).thenReturn(
                mSupportedValuesChangeCallbackBinder);
    }

    @After
    public void tearDown() throws Exception {
        mPropertyHalService.release();
        mPropertyHalService.destroy();
        mPropertyHalService = null;
    }

    private void verifyNoPendingRequest() {
        assertWithMessage("No pending async requests after test finish")
                .that(mPropertyHalService.countPendingAsyncRequests()).isEqualTo(0);
        assertWithMessage("No pending async set request waiting for update after test finish")
                .that(mPropertyHalService.countHalPropIdToWaitForUpdateRequests()).isEqualTo(0);
    }

    private Object deliverResult(InvocationOnMock invocation, Integer expectedServiceRequestId,
            CarPropertyErrorCodes carPropertyErrorCodes, HalPropValue propValue, boolean get) {
        Object[] args = invocation.getArguments();
        List getVehicleHalRequests = (List) args[0];
        Map<VehicleStubCallbackInterface, List<GetVehicleStubAsyncResult>> callbackToGetResults =
                new HashMap<>();
        Map<VehicleStubCallbackInterface, List<SetVehicleStubAsyncResult>> callbackToSetResults =
                new HashMap<>();
        for (int i = 0; i < getVehicleHalRequests.size(); i++) {
            AsyncGetSetRequest getVehicleHalRequest = (AsyncGetSetRequest)
                    getVehicleHalRequests.get(i);
            VehicleStubCallbackInterface callback = (VehicleStubCallbackInterface) args[1];
            if (callbackToGetResults.get(callback) == null) {
                callbackToGetResults.put(callback, new ArrayList<>());
            }
            if (callbackToSetResults.get(callback) == null) {
                callbackToSetResults.put(callback, new ArrayList<>());
            }

            int serviceRequestId = getVehicleHalRequest.getServiceRequestId();
            if (expectedServiceRequestId != null) {
                assertThat(serviceRequestId).isEqualTo(expectedServiceRequestId);
            }

            if (get) {
                if (propValue != null) {
                    callbackToGetResults.get(callback).add(new GetVehicleStubAsyncResult(
                            serviceRequestId, propValue));
                } else {
                    callbackToGetResults.get(callback).add(new GetVehicleStubAsyncResult(
                            serviceRequestId, carPropertyErrorCodes));
                }
            } else {
                callbackToSetResults.get(callback).add(new SetVehicleStubAsyncResult(
                        serviceRequestId, carPropertyErrorCodes));
            }
        }

        for (VehicleStubCallbackInterface callback : callbackToGetResults.keySet()) {
            callback.onGetAsyncResults(callbackToGetResults.get(callback));
        }
        for (VehicleStubCallbackInterface callback : callbackToSetResults.keySet()) {
            callback.onSetAsyncResults(callbackToSetResults.get(callback));
        }

        return null;
    }

    private Object deliverOkayGetResult(InvocationOnMock invocation) {
        return deliverOkayGetResult(invocation, mPropValue);
    }

    private Object deliverOkayGetResult(InvocationOnMock invocation, HalPropValue propValue) {
        return deliverResult(invocation, /* expectedServiceRequestId= */ null,
                STATUS_OK_NO_ERROR, propValue, true);
    }

    private Object deliverOkayGetResult(InvocationOnMock invocation,
            Integer expectedServiceRequestId) {
        return deliverResult(invocation, expectedServiceRequestId, STATUS_OK_NO_ERROR,
                mPropValue, true);
    }

    private Object deliverOkaySetResult(InvocationOnMock invocation) {
        return deliverOkaySetResult(invocation, /* expectedServiceRequestId= */ null);
    }

    private Object deliverOkaySetResult(InvocationOnMock invocation,
            Integer expectedServiceRequestId) {
        return deliverResult(invocation, expectedServiceRequestId, STATUS_OK_NO_ERROR,
                null, false);
    }

    private Object deliverTryAgainGetResult(InvocationOnMock invocation) {
        return deliverTryAgainGetResult(invocation, /* expectedServiceRequestId= */ null);
    }

    private Object deliverTryAgainGetResult(InvocationOnMock invocation,
            Integer expectedServiceRequestId) {
        return deliverResult(invocation, expectedServiceRequestId,
                ERROR_CODES_TRY_AGAIN, null, true);
    }

    private Object deliverTryAgainSetResult(InvocationOnMock invocation) {
        return deliverTryAgainSetResult(invocation, /* expectedServiceRequestId= */ null);
    }

    private Object deliverTryAgainSetResult(InvocationOnMock invocation,
            Integer expectedServiceRequestId) {
        return deliverResult(invocation, expectedServiceRequestId,
                ERROR_CODES_TRY_AGAIN, null, false);
    }

    private Object deliverErrorGetResult(InvocationOnMock invocation,
            CarPropertyErrorCodes errorCode) {
        return deliverErrorGetResult(invocation, /* expectedServiceRequestId= */ null, errorCode);
    }

    private Object deliverErrorGetResult(InvocationOnMock invocation,
            Integer expectedServiceRequestId, CarPropertyErrorCodes errorCode) {
        return deliverResult(invocation, expectedServiceRequestId, errorCode, null, true);
    }

    private Object deliverErrorSetResult(InvocationOnMock invocation,
            CarPropertyErrorCodes errorCode) {
        return deliverErrorSetResult(invocation, /* expectedServiceRequestId= */ null, errorCode);
    }

    private Object deliverErrorSetResult(InvocationOnMock invocation,
            Integer expectedServiceRequestId, CarPropertyErrorCodes errorCode) {
        return deliverResult(invocation, expectedServiceRequestId, errorCode, null, false);
    }

    @Test
    public void testGetCarPropertyValuesAsync() {
        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.getCarPropertyValuesAsync(List.of(GET_PROPERTY_SERVICE_REQUEST_1),
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        ArgumentCaptor<List<AsyncGetSetRequest>> captor =
                ArgumentCaptor.forClass(List.class);
        verify(mVehicleHal).getAsync(captor.capture(),
                any(VehicleStubCallbackInterface.class));
        AsyncGetSetRequest gotRequest = captor.getValue().get(0);
        assertThat(gotRequest.getServiceRequestId()).isEqualTo(RECEIVED_REQUEST_ID_1);
        assertThat(gotRequest.getHalPropValue().getPropId()).isEqualTo(HVAC_TEMPERATURE_SET);
        assertThat(gotRequest.getTimeoutUptimeMs()).isGreaterThan(1000);

        mPropertyHalService.cancelRequests(new int[]{REQUEST_ID_1});
        verify(mVehicleHal, timeout(1000)).cancelRequests(List.of(0));

        verifyNoPendingRequest();
    }

    @Test
    public void testGetCarPropertyValuesAsync_multipleRequests() {
        List<AsyncPropertyServiceRequest> getPropertyServiceRequests = new ArrayList<>();
        getPropertyServiceRequests.add(GET_PROPERTY_SERVICE_REQUEST_1);
        getPropertyServiceRequests.add(GET_PROPERTY_SERVICE_REQUEST_2);
        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.getCarPropertyValuesAsync(getPropertyServiceRequests,
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        ArgumentCaptor<List<AsyncGetSetRequest>> captor =
                ArgumentCaptor.forClass(List.class);
        verify(mVehicleHal).getAsync(captor.capture(),
                any(VehicleStubCallbackInterface.class));
        AsyncGetSetRequest gotRequest0 = captor.getValue().get(0);
        assertThat(gotRequest0.getServiceRequestId()).isEqualTo(RECEIVED_REQUEST_ID_1);
        assertThat(gotRequest0.getHalPropValue().getPropId()).isEqualTo(
                HVAC_TEMPERATURE_SET);
        assertThat(gotRequest0.getHalPropValue().getAreaId()).isEqualTo(0);
        assertThat(gotRequest0.getTimeoutUptimeMs()).isGreaterThan(1000);
        AsyncGetSetRequest gotRequest1 = captor.getValue().get(1);
        assertThat(gotRequest1.getServiceRequestId()).isEqualTo(RECEIVED_REQUEST_ID_2);
        assertThat(gotRequest1.getHalPropValue().getPropId()).isEqualTo(
                HVAC_TEMPERATURE_SET);
        assertThat(gotRequest1.getHalPropValue().getAreaId()).isEqualTo(0);
        assertThat(gotRequest1.getTimeoutUptimeMs()).isGreaterThan(1000);

        mPropertyHalService.cancelRequests(new int[]{REQUEST_ID_1, REQUEST_ID_2});

        verifyNoPendingRequest();
    }

    @Test
    public void testGetCarPropertyValuesAsync_staticCacheMultipleRequests() throws Exception {
        doAnswer((invocation) -> {
            return deliverOkayGetResult(invocation, mPropValue3);
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));

        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.getCarPropertyValuesAsync(
                List.of(GET_PROPERTY_SERVICE_REQUEST_STATIC_1), mGetAsyncPropertyResultCallback,
                /* timeoutInMs= */ 1000, /* asyncRequestStartTime= */ 0);

        verify(mGetAsyncPropertyResultCallback, timeout(1000)).onGetValueResults(
                mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyValue().getValue()).isEqualTo(3);
        assertThat(result.getCarPropertyValue().getAreaId()).isEqualTo(0);
        reset(mVehicleHal);
        reset(mGetAsyncPropertyResultCallback);
        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.getCarPropertyValuesAsync(
                List.of(GET_PROPERTY_SERVICE_REQUEST_STATIC_1), mGetAsyncPropertyResultCallback,
                /* timeoutInMs= */ 1000, /* asyncRequestStartTime= */ 0);
        verify(mVehicleHal, never()).getAsync(any(), any());
        verify(mGetAsyncPropertyResultCallback, timeout(1000)).onGetValueResults(
                mAsyncResultCaptor.capture());
        GetSetValueResult cachedResult = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(cachedResult.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(cachedResult.getCarPropertyValue().getValue()).isEqualTo(3);
        assertThat(cachedResult.getCarPropertyValue().getAreaId()).isEqualTo(0);
    }

    @Test
    public void testGetCarPropertyValuesAsync_linkToDeath() throws RemoteException {
        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();
        List<AsyncPropertyServiceRequest> getPropertyServiceRequests = mock(List.class);
        mPropertyHalService.getCarPropertyValuesAsync(getPropertyServiceRequests,
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);
        verify(mGetAsyncPropertyResultBinder).linkToDeath(any(IBinder.DeathRecipient.class),
                anyInt());

        mPropertyHalService.cancelRequests(new int[]{REQUEST_ID_1});

        verifyNoPendingRequest();
    }

    @Test
    public void testGetCarPropertyValuesAsync_binderDiedBeforeLinkToDeath() throws RemoteException {
        IBinder mockBinder = mock(IBinder.class);
        when(mGetAsyncPropertyResultCallback.asBinder()).thenReturn(mockBinder);
        doThrow(new RemoteException()).when(mockBinder).linkToDeath(any(), anyInt());
        List<AsyncPropertyServiceRequest> getPropertyServiceRequests = mock(List.class);

        assertThrows(IllegalStateException.class, () -> {
            mPropertyHalService.getCarPropertyValuesAsync(getPropertyServiceRequests,
                    mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                    /* asyncRequestStartTime= */ 0);
        });

        verifyNoPendingRequest();
    }

    @Test
    public void testGetCarPropertyValuesAsync_unlinkToDeath_onBinderDied() throws RemoteException {
        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();
        List<AsyncPropertyServiceRequest> getPropertyServiceRequests = mock(List.class);
        mPropertyHalService.getCarPropertyValuesAsync(getPropertyServiceRequests,
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        ArgumentCaptor<IBinder.DeathRecipient> recipientCaptor = ArgumentCaptor.forClass(
                IBinder.DeathRecipient.class);
        verify(mGetAsyncPropertyResultBinder).linkToDeath(recipientCaptor.capture(), eq(0));

        recipientCaptor.getValue().binderDied();

        verify(mGetAsyncPropertyResultBinder).unlinkToDeath(recipientCaptor.getValue(), 0);

        verifyNoPendingRequest();
    }

    @Test
    public void testGetCarPropertyValuesAsync_normalResult() throws RemoteException {
        doAnswer((invocation) -> {
            return deliverOkayGetResult(invocation, RECEIVED_REQUEST_ID_1);
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));

        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.getCarPropertyValuesAsync(List.of(GET_PROPERTY_SERVICE_REQUEST_1),
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);


        verify(mGetAsyncPropertyResultCallback, timeout(1000)).onGetValueResults(
                mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyValue().getValue()).isEqualTo(SAMPLE_RATE_HZ);
        assertThat(result.getCarPropertyValue().getAreaId()).isEqualTo(0);

        verifyNoPendingRequest();
    }

    @Test
    public void testGetCarPropertyValuesAsync_retryTwiceAndSucceed() throws RemoteException {
        doAnswer(new Answer() {
            private int mCount = 0;

            public Object answer(InvocationOnMock invocation) {
                Object[] args = invocation.getArguments();
                List<AsyncGetSetRequest> getVehicleHalRequests = (List<AsyncGetSetRequest>) args[0];
                assertThat(getVehicleHalRequests.size()).isEqualTo(1);

                switch (mCount++) {
                    case 0:
                        return deliverTryAgainGetResult(invocation, RECEIVED_REQUEST_ID_1);
                    case 1:
                        return deliverTryAgainGetResult(invocation, RECEIVED_REQUEST_ID_2);
                    case 2:
                        return deliverOkayGetResult(invocation, RECEIVED_REQUEST_ID_3);
                    default:
                        throw new IllegalStateException("Only expect 3 calls");
                }
            }
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));

        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.getCarPropertyValuesAsync(List.of(GET_PROPERTY_SERVICE_REQUEST_1),
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mGetAsyncPropertyResultCallback, timeout(1000)).onGetValueResults(
                mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyValue().getValue()).isEqualTo(SAMPLE_RATE_HZ);
        assertThat(result.getCarPropertyValue().getAreaId()).isEqualTo(0);

        verifyNoPendingRequest();
    }

    @Test
    public void testGetCarPropertyValuesAsync_retryAndTimeout() throws RemoteException {
        doAnswer((invocation) -> {
            // For every request, we return retry result.
            return deliverTryAgainGetResult(invocation);
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));

        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.getCarPropertyValuesAsync(List.of(GET_PROPERTY_SERVICE_REQUEST_1),
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 10,
                /* asyncRequestStartTime= */ 0);

        verify(mGetAsyncPropertyResultCallback, timeout(1000)).onGetValueResults(
                mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode()).isEqualTo(
                CarPropertyManager.STATUS_ERROR_TIMEOUT);

        verifyNoPendingRequest();
    }

    @Test
    public void testGetCarPropertyValuesAsync_noResultTimeout() throws RemoteException {
        doNothing().when(mVehicleHal).getAsync(anyList(),
                any(VehicleStubCallbackInterface.class));
        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.getCarPropertyValuesAsync(List.of(GET_PROPERTY_SERVICE_REQUEST_1),
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 10,
                /* asyncRequestStartTime= */ 0);

        verify(mGetAsyncPropertyResultCallback, timeout(1000)).onGetValueResults(
                mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode()).isEqualTo(
                CarPropertyManager.STATUS_ERROR_TIMEOUT);

        verifyNoPendingRequest();
    }

    @Test
    public void testGetCarPropertyValuesAsync_timeoutFromVehicleStub() throws RemoteException {
        doAnswer((invocation) -> {
            Object[] args = invocation.getArguments();
            List getVehicleHalRequests = (List) args[0];
            AsyncGetSetRequest getVehicleHalRequest =
                    (AsyncGetSetRequest) getVehicleHalRequests.get(0);
            VehicleStubCallbackInterface getVehicleStubAsyncCallback =
                    (VehicleStubCallbackInterface) args[1];
            // Simulate the request has already timed-out.
            getVehicleStubAsyncCallback.onRequestsTimeout(List.of(
                    getVehicleHalRequest.getServiceRequestId()));
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));

        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.getCarPropertyValuesAsync(List.of(GET_PROPERTY_SERVICE_REQUEST_1),
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mGetAsyncPropertyResultCallback, timeout(1000)).onGetValueResults(
                mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode()).isEqualTo(
                CarPropertyManager.STATUS_ERROR_TIMEOUT);

        verifyNoPendingRequest();
    }

    @Test
    public void testGetCarPropertyValuesAsync_errorResult() throws RemoteException {
        doAnswer((invocation) -> {
            return deliverErrorGetResult(invocation, RECEIVED_REQUEST_ID_1,
                    ERROR_CODES_INTERNAL);
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));

        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.getCarPropertyValuesAsync(List.of(GET_PROPERTY_SERVICE_REQUEST_1),
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mGetAsyncPropertyResultCallback, timeout(1000)).onGetValueResults(
                mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode()).isEqualTo(
                CarPropertyManager.STATUS_ERROR_INTERNAL_ERROR);
        assertThat(result.getCarPropertyValue()).isEqualTo(null);

        verifyNoPendingRequest();
    }

    @Test
    public void testGetCarPropertyValuesAsync_errorResultVendorErrorCode() throws RemoteException {
        doAnswer((invocation) -> {
            CarPropertyErrorCodes errorCodes = CarPropertyErrorCodes.createFromVhalStatusCode(
                    SYSTEM_ERROR_CODE | (VENDOR_ERROR_CODE << 16));
            return deliverResult(invocation, RECEIVED_REQUEST_ID_1, errorCodes,
                    /* propValue= */ null, /* get= */ true);
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));

        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.getCarPropertyValuesAsync(List.of(GET_PROPERTY_SERVICE_REQUEST_1),
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mGetAsyncPropertyResultCallback, timeout(1000)).onGetValueResults(
                mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode()).isEqualTo(
                CarPropertyManager.STATUS_ERROR_INTERNAL_ERROR);
        assertThat(result.getCarPropertyErrorCodes().getVendorErrorCode())
                .isEqualTo(VENDOR_ERROR_CODE);
        assertThat(result.getCarPropertyErrorCodes().getSystemErrorCode()).isEqualTo(
                SYSTEM_ERROR_CODE);
        assertThat(result.getCarPropertyValue()).isEqualTo(null);

        verifyNoPendingRequest();
    }

    @Test
    public void testGetCarPropertyValuesAsync_propStatusUnavailable() throws RemoteException {
        doAnswer((invocation) -> {
            HalPropValue propValue = mPropValueBuilder.build(
                    HVAC_TEMPERATURE_SET, /* areaId= */ 0, TEST_UPDATE_TIMESTAMP_NANOS,
                    VehiclePropertyStatus.UNAVAILABLE, SAMPLE_RATE_HZ);
            return deliverOkayGetResult(invocation, propValue);
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));

        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.getCarPropertyValuesAsync(List.of(GET_PROPERTY_SERVICE_REQUEST_1),
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mGetAsyncPropertyResultCallback, timeout(1000)).onGetValueResults(
                mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode()).isEqualTo(
                CarPropertyManager.STATUS_ERROR_NOT_AVAILABLE);
        assertThat(result.getCarPropertyErrorCodes().getVendorErrorCode()).isEqualTo(0);
        assertThat(result.getCarPropertyErrorCodes().getSystemErrorCode()).isEqualTo(0);
        assertThat(result.getCarPropertyValue()).isEqualTo(null);

        verifyNoPendingRequest();
    }

    @Test
    public void testGetCarPropertyValuesAsync_propStatusError() throws RemoteException {
        doAnswer((invocation) -> {
            HalPropValue propValue = mPropValueBuilder.build(
                    HVAC_TEMPERATURE_SET, /* areaId= */ 0, TEST_UPDATE_TIMESTAMP_NANOS,
                    VehiclePropertyStatus.ERROR, SAMPLE_RATE_HZ);
            return deliverOkayGetResult(invocation, propValue);
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));

        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.getCarPropertyValuesAsync(List.of(GET_PROPERTY_SERVICE_REQUEST_1),
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mGetAsyncPropertyResultCallback, timeout(1000)).onGetValueResults(
                mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode()).isEqualTo(
                CarPropertyManager.STATUS_ERROR_INTERNAL_ERROR);
        assertThat(result.getCarPropertyErrorCodes().getVendorErrorCode()).isEqualTo(0);
        assertThat(result.getCarPropertyErrorCodes().getSystemErrorCode()).isEqualTo(0);
        assertThat(result.getCarPropertyValue()).isEqualTo(null);

        verifyNoPendingRequest();
    }

    @Test
    public void testGetCarPropertyValuesAsync_multipleResultsSameCall() throws RemoteException {
        doAnswer((invocation) -> {
            Object[] args = invocation.getArguments();
            List<AsyncGetSetRequest> getVehicleHalRequests = (List) args[0];
            VehicleStubCallbackInterface getVehicleStubAsyncCallback =
                    (VehicleStubCallbackInterface) args[1];

            assertThat(getVehicleHalRequests.size()).isEqualTo(2);
            assertThat(getVehicleHalRequests.get(0).getServiceRequestId()).isEqualTo(
                    RECEIVED_REQUEST_ID_1);
            assertThat(getVehicleHalRequests.get(0).getHalPropValue().getPropId()).isEqualTo(
                    HVAC_TEMPERATURE_SET);
            assertThat(getVehicleHalRequests.get(1).getServiceRequestId()).isEqualTo(
                    RECEIVED_REQUEST_ID_2);
            assertThat(getVehicleHalRequests.get(1).getHalPropValue().getPropId()).isEqualTo(
                    HVAC_TEMPERATURE_SET);

            List<GetVehicleStubAsyncResult> getVehicleStubAsyncResults =
                    new ArrayList<>();
            getVehicleStubAsyncResults.add(
                    new GetVehicleStubAsyncResult(RECEIVED_REQUEST_ID_1, mPropValue));
            getVehicleStubAsyncResults.add(
                    new GetVehicleStubAsyncResult(RECEIVED_REQUEST_ID_2, mPropValue));
            getVehicleStubAsyncCallback.onGetAsyncResults(getVehicleStubAsyncResults);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));

        List<AsyncPropertyServiceRequest> getPropertyServiceRequests = new ArrayList<>();
        getPropertyServiceRequests.add(GET_PROPERTY_SERVICE_REQUEST_1);
        getPropertyServiceRequests.add(GET_PROPERTY_SERVICE_REQUEST_2);
        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.getCarPropertyValuesAsync(getPropertyServiceRequests,
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mGetAsyncPropertyResultCallback, timeout(1000)).onGetValueResults(
                mAsyncResultCaptor.capture());
        GetSetValueResult result1 = mAsyncResultCaptor.getValue().getList().get(0);
        GetSetValueResult result2 = mAsyncResultCaptor.getValue().getList().get(1);
        assertThat(result1.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result1.getCarPropertyValue().getValue()).isEqualTo(SAMPLE_RATE_HZ);
        assertThat(result2.getRequestId()).isEqualTo(REQUEST_ID_2);
        assertThat(result2.getCarPropertyValue().getValue()).isEqualTo(SAMPLE_RATE_HZ);

        verifyNoPendingRequest();
    }

    @Test
    public void testGetCarPropertyValuesAsync_multipleResultsDifferentCalls()
            throws RemoteException {
        doAnswer((invocation) -> {
            Object[] args = invocation.getArguments();
            List<AsyncGetSetRequest> getVehicleHalRequests = (List) args[0];
            VehicleStubCallbackInterface getVehicleStubAsyncCallback =
                    (VehicleStubCallbackInterface) args[1];

            assertThat(getVehicleHalRequests.size()).isEqualTo(2);
            assertThat(getVehicleHalRequests.get(0).getServiceRequestId()).isEqualTo(
                    RECEIVED_REQUEST_ID_1);
            assertThat(getVehicleHalRequests.get(0).getHalPropValue().getPropId()).isEqualTo(
                    HVAC_TEMPERATURE_SET);
            assertThat(getVehicleHalRequests.get(1).getServiceRequestId()).isEqualTo(
                    RECEIVED_REQUEST_ID_2);
            assertThat(getVehicleHalRequests.get(1).getHalPropValue().getPropId()).isEqualTo(
                    HVAC_TEMPERATURE_SET);

            getVehicleStubAsyncCallback.onGetAsyncResults(
                    List.of(new GetVehicleStubAsyncResult(RECEIVED_REQUEST_ID_1,
                            mPropValue)));
            getVehicleStubAsyncCallback.onGetAsyncResults(
                    List.of(new GetVehicleStubAsyncResult(RECEIVED_REQUEST_ID_2,
                            mPropValue)));
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));

        List<AsyncPropertyServiceRequest> getPropertyServiceRequests = new ArrayList<>();
        getPropertyServiceRequests.add(GET_PROPERTY_SERVICE_REQUEST_1);
        getPropertyServiceRequests.add(GET_PROPERTY_SERVICE_REQUEST_2);
        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.getCarPropertyValuesAsync(getPropertyServiceRequests,
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mGetAsyncPropertyResultCallback, timeout(1000).times(2))
                .onGetValueResults(mAsyncResultCaptor.capture());
        List<GetSetValueResultList> getValuesResults = mAsyncResultCaptor.getAllValues();
        assertThat(getValuesResults.get(0).getList().get(0).getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(getValuesResults.get(0).getList().get(0).getCarPropertyValue().getValue())
                .isEqualTo(SAMPLE_RATE_HZ);
        assertThat(getValuesResults.get(1).getList().get(0).getRequestId()).isEqualTo(REQUEST_ID_2);
        assertThat(getValuesResults.get(1).getList().get(0).getCarPropertyValue().getValue())
                .isEqualTo(SAMPLE_RATE_HZ);

        verifyNoPendingRequest();
    }

    @Test
    public void testSetCarPropertyValuesAsync() {
        Object lock = new Object();
        Set<Integer> vhalCancelledRequestIds = new ArraySet<>();
        doAnswer((invocation) -> {
            List<Integer> ids = (List<Integer>) invocation.getArgument(0);
            synchronized (lock) {
                for (int i = 0; i < ids.size(); i++) {
                    vhalCancelledRequestIds.add(ids.get(i));
                }
            }
            return null;
        }).when(mVehicleHal).cancelRequests(any());
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        ArgumentCaptor<List<AsyncGetSetRequest>> captor =
                ArgumentCaptor.forClass(List.class);
        verify(mVehicleHal).setAsync(captor.capture(), any(VehicleStubCallbackInterface.class));
        AsyncGetSetRequest gotRequest = captor.getValue().get(0);
        assertThat(gotRequest.getServiceRequestId()).isEqualTo(RECEIVED_REQUEST_ID_1);
        assertThat(gotRequest.getHalPropValue().getPropId()).isEqualTo(HVAC_TEMPERATURE_SET);
        assertThat(gotRequest.getHalPropValue().getFloatValue(0)).isEqualTo(SAMPLE_RATE_HZ);
        assertThat(gotRequest.getTimeoutUptimeMs()).isGreaterThan(1000);

        mPropertyHalService.cancelRequests(new int[]{REQUEST_ID_1});

        // Because we cancel the onging async set property request, the ongoing get initial value
        // request should be cancelled as well.
        verify(mVehicleHal, timeout(1000).times(2)).cancelRequests(any());
        synchronized (lock) {
            assertThat(vhalCancelledRequestIds).containsExactlyElementsIn(new Integer[]{0, 1});
        }

        verifyNoPendingRequest();
    }

    @Test
    public void testSetCarPropertyValuesAsync_configNotFound() {
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();
        AsyncPropertyServiceRequest request = new AsyncPropertyServiceRequest(
                REQUEST_ID_1, /* propertyId= */ 1, /* areaId= */ 0,
                new CarPropertyValue(/* propertyId= */ 1, /* areaId= */ 0, SAMPLE_RATE_HZ));

        assertThrows(IllegalArgumentException.class, () -> {
            mPropertyHalService.setCarPropertyValuesAsync(List.of(request),
                    mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                    /* asyncRequestStartTime= */ 0);
        });

        verifyNoPendingRequest();
    }

    @Test
    public void testSetCarPropertyValuesAsync_linkToDeath() throws RemoteException {
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();
        List<AsyncPropertyServiceRequest> setPropertyServiceRequests = mock(List.class);

        mPropertyHalService.setCarPropertyValuesAsync(setPropertyServiceRequests,
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mSetAsyncPropertyResultBinder).linkToDeath(any(IBinder.DeathRecipient.class),
                anyInt());

        verifyNoPendingRequest();
    }

    // Test the case where we don't wait for property update event. The success callback should be
    // immediately called when the set request succeeded.
    @Test
    public void testSetCarPropertyValuesAsync_noWaitForPropertyUpdate()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        AsyncPropertyServiceRequest request = copyRequest(SET_HVAC_REQUEST_ID_1);
        request.setWaitForPropertyUpdate(false);

        mPropertyHalService.setCarPropertyValuesAsync(List.of(request),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        // Must not subscribe to the property for update events.
        verify(mVehicleHal, never()).subscribeProperty(any(), anyList());
        // Must not send get initial value request.
        verify(mVehicleHal, never()).getAsync(any(), any());
        assertThat(setInvocationWrap).hasSize(1);

        // Returns the set value result.
        deliverOkaySetResult(setInvocationWrap.get(0));

        verify(mSetAsyncPropertyResultCallback).onSetValueResults(mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode())
                .isEqualTo(STATUS_OK);
        // This should be the time when the request is successfully sent.
        assertThat(result.getUpdateTimestampNanos()).isGreaterThan(0);

        verifyNoPendingRequest();
    }

    @Test
    public void testSetCarPropertyValuesAsync_noWaitForPropertyUpdateWithMultipleAreaRequests()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        AsyncPropertyServiceRequest request1 = copyRequest(SET_VEHICLE_SPEED_AREA_ID_1_REQUEST);
        AsyncPropertyServiceRequest request2 = copyRequest(SET_VEHICLE_SPEED_AREA_ID_2_REQUEST);
        request1.setWaitForPropertyUpdate(false);
        request2.setWaitForPropertyUpdate(false);

        mPropertyHalService.setCarPropertyValuesAsync(List.of(request1, request2),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        // Must not subscribe to the property for update events.
        verify(mVehicleHal, never()).subscribeProperty(any(), anyList());
        verify(mVehicleHal, never()).subscribeProperty(any(), anyList());
        // Must not send get initial value request.
        verify(mVehicleHal, never()).getAsync(any(), any());
        assertThat(setInvocationWrap).hasSize(1);

        // Returns the set value result.
        deliverOkaySetResult(setInvocationWrap.get(0));

        verify(mSetAsyncPropertyResultCallback).onSetValueResults(mAsyncResultCaptor.capture());
        GetSetValueResult result1 = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result1.getRequestId()).isEqualTo(REQUEST_ID_4);
        assertThat(result1.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode())
                .isEqualTo(STATUS_OK);
        verify(mSetAsyncPropertyResultCallback).onSetValueResults(mAsyncResultCaptor.capture());
        GetSetValueResult result2 = mAsyncResultCaptor.getValue().getList().get(1);
        assertThat(result2.getRequestId()).isEqualTo(REQUEST_ID_5);
        assertThat(result2.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode())
                .isEqualTo(STATUS_OK);

        // This should be the time when the request is successfully sent.
        assertThat(result1.getUpdateTimestampNanos()).isGreaterThan(0);
        assertThat(result2.getUpdateTimestampNanos()).isGreaterThan(0);

        verifyNoPendingRequest();
    }

    @Test
    public void testSetCarPropertyValuesAsync_mixWaitNoWaitForPropertyUpdate() throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();
        List<HalServiceBase> serviceWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            serviceWrap.add(invocation.getArgument(0));
            return null;
        }).when(mVehicleHal).subscribeProperty(any(), anyList());
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        AsyncPropertyServiceRequest request1 = copyRequest(SET_HVAC_REQUEST_ID_1);
        AsyncPropertyServiceRequest request2 = copyRequest(SET_SPEED_REQUEST_ID_2);
        request2.setWaitForPropertyUpdate(false);

        mPropertyHalService.setCarPropertyValuesAsync(List.of(request1, request2),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(hvacHalSubscribeOption());
        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        // Returns the get initial value result, assume we failed to get initial values.
        deliverErrorGetResult(getInvocationWrap.get(0), ERROR_CODES_INTERNAL);

        verify(mSetAsyncPropertyResultCallback, never()).onSetValueResults(any());

        // Returns the set value result.
        deliverOkaySetResult(setInvocationWrap.get(0));

        // We should receive the success callback for request 2 since the set request is already
        // sent.
        verify(mSetAsyncPropertyResultCallback).onSetValueResults(
                mAsyncResultCaptor.capture());

        // Deliver updated value for request 1. Request 2 don't need an updated value.
        assertThat(serviceWrap).hasSize(1);
        serviceWrap.get(0).onHalEvents(List.of(mPropValue));

        // Get init value result must not be passed to the client.
        verify(mSetAsyncPropertyResultCallback, never()).onGetValueResults(any());
        // We should receive the success callback for request 1.
        verify(mSetAsyncPropertyResultCallback, times(2)).onSetValueResults(
                mAsyncResultCaptor.capture());
        for (GetSetValueResultList results: mAsyncResultCaptor.getAllValues()) {
            GetSetValueResult result = results.getList().get(0);
            assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode())
                    .isEqualTo(STATUS_OK);
            if (result.getRequestId() == REQUEST_ID_1) {
                assertThat(result.getUpdateTimestampNanos()).isEqualTo(
                        TEST_UPDATE_TIMESTAMP_NANOS);
            } else {
                assertThat(result.getUpdateTimestampNanos()).isNotEqualTo(
                        TEST_UPDATE_TIMESTAMP_NANOS);
                // This should be elapsedRealTimeNano when the set is done.
                assertThat(result.getUpdateTimestampNanos()).isGreaterThan(0);
            }
        }
        // After the result comes, we must unsubscribe the property.
        verify(mVehicleHal).unsubscribeProperty(any(), eq(HVAC_TEMPERATURE_SET));

        verifyNoPendingRequest();
    }

    // Test the case where we get the result for the init value before we get the result for
    // async set. The init value is the same as the target value.
    @Test
    public void testSetCarPropertyValuesAsync_initValueSameAsTargetValue_beforeSetResult()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(hvacHalSubscribeOption());
        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        // Returns the get initial value result.
        deliverOkayGetResult(getInvocationWrap.get(0));

        verify(mSetAsyncPropertyResultCallback, never()).onSetValueResults(any());

        // Returns the set value result.
        deliverOkaySetResult(setInvocationWrap.get(0));

        // Get init value result must not be passed to the client.
        verify(mSetAsyncPropertyResultCallback, never()).onGetValueResults(any());
        verify(mSetAsyncPropertyResultCallback).onSetValueResults(mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode())
                .isEqualTo(STATUS_OK);
        assertThat(result.getUpdateTimestampNanos()).isEqualTo(TEST_UPDATE_TIMESTAMP_NANOS);
        // After the result comes, we must unsubscribe the property.
        verify(mVehicleHal).unsubscribeProperty(any(), eq(HVAC_TEMPERATURE_SET));

        verifyNoPendingRequest();
    }

    // Test the case where we get the result for the init value after we get the result for
    // async set. The init value is the same as the target value.
    @Test
    public void testSetCarPropertyValuesAsync_initValueSameAsTargetValue_afterSetResult()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(hvacHalSubscribeOption());
        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        // Returns the set value result.
        deliverOkaySetResult(setInvocationWrap.get(0));

        verify(mSetAsyncPropertyResultCallback, never()).onSetValueResults(any());

        // Returns the get initial value result.
        deliverOkayGetResult(getInvocationWrap.get(0));

        // Get init value result must not be passed to the client.
        verify(mSetAsyncPropertyResultCallback, never()).onGetValueResults(any());
        verify(mSetAsyncPropertyResultCallback).onSetValueResults(mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode())
                .isEqualTo(STATUS_OK);
        assertThat(result.getUpdateTimestampNanos()).isEqualTo(TEST_UPDATE_TIMESTAMP_NANOS);
        // After the result comes, we must unsubscribe the property.
        verify(mVehicleHal).unsubscribeProperty(any(), eq(HVAC_TEMPERATURE_SET));

        verifyNoPendingRequest();
    }

    // Test the case where the get initial value and set value request retried once.
    // The init value is the same as the target value.
    @Test
    public void testSetCarPropertyValuesAsync_initValueSameAsTargetValue_retry()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(hvacHalSubscribeOption());
        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        // Retry get initial value request.
        deliverTryAgainGetResult(getInvocationWrap.get(0));
        // Retry set initial value request.
        deliverTryAgainSetResult(setInvocationWrap.get(0));

        // Wait until the retry happens.
        verify(mVehicleHal, timeout(1000).times(2)).getAsync(any(), any());
        verify(mVehicleHal, timeout(1000).times(2)).setAsync(any(), any());

        // Returns the set value result.
        deliverOkaySetResult(setInvocationWrap.get(1));
        // Returns the get initial value result.
        deliverOkayGetResult(getInvocationWrap.get(1));

        verify(mSetAsyncPropertyResultCallback, never()).onGetValueResults(any());
        verify(mSetAsyncPropertyResultCallback).onSetValueResults(mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode())
                .isEqualTo(STATUS_OK);
        assertThat(result.getUpdateTimestampNanos()).isEqualTo(TEST_UPDATE_TIMESTAMP_NANOS);
        // After the result comes, we must unsubscribe the property.
        verify(mVehicleHal).unsubscribeProperty(any(), eq(HVAC_TEMPERATURE_SET));

        verifyNoPendingRequest();
    }

    // Test the case where the get initial value returns a different value than target value.
    @Test
    public void testSetCarPropertyValuesAsync_initValueDiffTargetValue_timeout()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 100,
                /* asyncRequestStartTime= */ 0);

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(hvacHalSubscribeOption());
        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        // Returns the get initial value result.
        deliverOkayGetResult(getInvocationWrap.get(0), mPropValueBuilder.build(
                HVAC_TEMPERATURE_SET, /* areaId= */ 0, 16.0f));
        // Returns the set value result.
        deliverOkaySetResult(setInvocationWrap.get(0));

        // Get init value result must not be passed to the client.
        verify(mSetAsyncPropertyResultCallback, never()).onGetValueResults(any());
        // Eventually the request should time out.
        verify(mSetAsyncPropertyResultCallback, timeout(1000)).onSetValueResults(
                mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode()).isEqualTo(
                CarPropertyManager.STATUS_ERROR_TIMEOUT);

        verifyNoPendingRequest();
    }

    // Test the case where we get set value error result after we get the initial value result.
    @Test
    public void testSetCarPropertyValuesAsync_errorSetResultAfterTargetInitValueResult()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(hvacHalSubscribeOption());
        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        // Returns the get initial value result.
        deliverOkayGetResult(getInvocationWrap.get(0));
        // Returns the set value result.
        deliverErrorSetResult(setInvocationWrap.get(0), ERROR_CODES_INTERNAL);

        // Get init value result must not be passed to the client.
        verify(mSetAsyncPropertyResultCallback, never()).onGetValueResults(any());
        verify(mSetAsyncPropertyResultCallback).onSetValueResults(mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode())
                .isEqualTo(CarPropertyManager.STATUS_ERROR_INTERNAL_ERROR);
        // After the result comes, we must unsubscribe the property.
        verify(mVehicleHal).unsubscribeProperty(any(), eq(HVAC_TEMPERATURE_SET));

        verifyNoPendingRequest();
    }

    // Test the callback is only invoked once even though the same result is returned multiple
    // times.
    @Test
    public void testSetCarPropertyValuesAsync_callbackOnlyCalledOncePerRequest()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(hvacHalSubscribeOption());
        deliverErrorSetResult(setInvocationWrap.get(0), ERROR_CODES_INTERNAL);

        // The same result is returned again.
        deliverErrorSetResult(setInvocationWrap.get(0), ERROR_CODES_INTERNAL);

        // We must only call callback once.
        verify(mSetAsyncPropertyResultCallback, times(1)).onSetValueResults(any());
        // After the result comes, we must unsubscribe the property.
        verify(mVehicleHal).unsubscribeProperty(any(), eq(HVAC_TEMPERATURE_SET));

        verifyNoPendingRequest();
    }

    // Test the case where the get init value request has error result. The result must be ignored.
    @Test
    public void testSetCarPropertyValuesAsync_errorInitValueResult_timeout()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 100,
                /* asyncRequestStartTime= */ 0);

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(hvacHalSubscribeOption());
        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        // Returns the get initial value result, assume we failed to get initial values.
        deliverErrorGetResult(getInvocationWrap.get(0), ERROR_CODES_INTERNAL);
        // Returns the set value result.
        deliverOkaySetResult(setInvocationWrap.get(0));

        // Get init value result must not be passed to the client.
        verify(mSetAsyncPropertyResultCallback, never()).onGetValueResults(any());
        // Eventually the request should time out.
        verify(mSetAsyncPropertyResultCallback, timeout(1000)).onSetValueResults(
                mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode()).isEqualTo(
                CarPropertyManager.STATUS_ERROR_TIMEOUT);

        verifyNoPendingRequest();
    }

    @Test
    public void testSetCarPropertyValuesAsync_propertyUpdatedThroughEvent()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();
        List<HalServiceBase> serviceWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            serviceWrap.add(invocation.getArgument(0));
            return null;
        }).when(mVehicleHal).subscribeProperty(any(), anyList());
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        float testSampleRate = 20.0f;
        AsyncPropertyServiceRequest request2 = copyRequest(SET_SPEED_REQUEST_ID_2);
        request2.setUpdateRateHz(testSampleRate);

        mPropertyHalService.setCarPropertyValuesAsync(List.of(
                SET_HVAC_REQUEST_ID_1, request2),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        // Returns the get initial value result.
        deliverErrorGetResult(getInvocationWrap.get(0), ERROR_CODES_INTERNAL);
        // Returns the set value result.
        deliverOkaySetResult(setInvocationWrap.get(0));

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(
                hvacHalSubscribeOption(), speedHalSubscribeOption(testSampleRate));

        // Notify the property is updated to the target value.
        assertThat(serviceWrap).hasSize(1);
        serviceWrap.get(0).onHalEvents(List.of(mPropValue));
        serviceWrap.get(0).onHalEvents(List.of(mPropValue2));

        verify(mSetAsyncPropertyResultCallback, times(2)).onSetValueResults(
                mAsyncResultCaptor.capture());
        for (GetSetValueResultList results : mAsyncResultCaptor.getAllValues()) {
            GetSetValueResult result = results.getList().get(0);
            assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode())
                    .isEqualTo(STATUS_OK);
            assertThat(result.getUpdateTimestampNanos()).isEqualTo(TEST_UPDATE_TIMESTAMP_NANOS);
        }
        // After the result comes, we must unsubscribe the property.
        verify(mVehicleHal).unsubscribeProperty(any(), eq(HVAC_TEMPERATURE_SET));
        verify(mVehicleHal).unsubscribeProperty(any(), eq(PERF_VEHICLE_SPEED));

        verifyNoPendingRequest();
    }

    @Test
    public void testSetCarPropertyValuesAsync_propertyUpdatedThroughEvent_ignoreNonTargetEvent()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();
        List<HalServiceBase> serviceWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        // Because HVAC_TEMPERATURE_SET is ON_CHANGE property, the sample rate is 0.
        doAnswer((invocation) -> {
            serviceWrap.add(invocation.getArgument(0));
            return null;
        }).when(mVehicleHal).subscribeProperty(any(), anyList());
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        // Returns the get initial value result. This is not the target value.
        deliverOkayGetResult(getInvocationWrap.get(0), mPropValueBuilder.build(
                HVAC_TEMPERATURE_SET, /* areaId= */ 0, 16.0f));
        // Returns the set value result.
        deliverOkaySetResult(setInvocationWrap.get(0));

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(hvacHalSubscribeOption());

        // Generate a property update event with non-target value.
        serviceWrap.get(0).onHalEvents(List.of(mNonTargetPropValue));

        verify(mSetAsyncPropertyResultCallback, never()).onSetValueResults(any());

        // Notify the property is updated to the target value.
        serviceWrap.get(0).onHalEvents(List.of(mPropValue));

        verify(mSetAsyncPropertyResultCallback).onSetValueResults(mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode())
                .isEqualTo(STATUS_OK);
        assertThat(result.getUpdateTimestampNanos()).isEqualTo(TEST_UPDATE_TIMESTAMP_NANOS);
        // After the result comes, we must unsubscribe the property.
        verify(mVehicleHal).unsubscribeProperty(any(), eq(HVAC_TEMPERATURE_SET));

        verifyNoPendingRequest();
    }

    @Test
    public void testSetCarPropertyValuesAsync_updateSubscriptionRate()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        AsyncPropertyServiceRequest request = copyRequest(SET_SPEED_REQUEST_ID_2);
        request.setUpdateRateHz(20.0f);
        mPropertyHalService.setCarPropertyValuesAsync(List.of(request),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(speedHalSubscribeOption(20f));
        clearInvocations(mVehicleHal);
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        mPropertyHalService.subscribeProperty(List.of(createCarSubscriptionOption(
                PERF_VEHICLE_SPEED, new int[]{0}, /* updateRateHz= */ 40.0f,
                /* enableVur= */ true)));

        // Subscription rate has to be updated according to client subscription.
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(speedHalSubscribeOption(40f));
        clearInvocations(mVehicleHal);
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        // After client unsubscribe, revert back to the internal subscription rate.
        mPropertyHalService.unsubscribeProperty(PERF_VEHICLE_SPEED);

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(speedHalSubscribeOption(20f));
        clearInvocations(mVehicleHal);
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        // New client subscription must overwrite the internal rate.
        mPropertyHalService.subscribeProperty(List.of(createCarSubscriptionOption(
                PERF_VEHICLE_SPEED, new int[]{0}, /* updateRateHz= */ 50.0f,
                /* enableVur= */ true)));

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(speedHalSubscribeOption(50f));
        clearInvocations(mVehicleHal);
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        // Finish the async set request.
        deliverOkaySetResult(setInvocationWrap.get(0));
        deliverOkayGetResult(getInvocationWrap.get(0), mPropValue2);

        verify(mSetAsyncPropertyResultCallback).onSetValueResults(mAsyncResultCaptor.capture());
        assertThat(
                mAsyncResultCaptor.getValue().getList().get(0)
                        .getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode())
                .isEqualTo(STATUS_OK);

        // After the internal subscription is finished, the client subscription must be kept,
        // which causes no update to update rate.
        verify(mVehicleHal, never()).subscribeProperty(any(), any());

        mPropertyHalService.unsubscribeProperty(PERF_VEHICLE_SPEED);

        // After both client and internal unsubscription, the property must be unsubscribed.
        verify(mVehicleHal).unsubscribeProperty(any(), eq(PERF_VEHICLE_SPEED));

        verifyNoPendingRequest();
    }

    @Test
    public void testMultipleSetCarPropertyValuesAsync_updateSubscriptionRateWithDifferentAreaIds()
            throws Exception {
        when(mMockCarPropertyConfig2.getAreaIds()).thenReturn(new int[]{0, 1, 2});
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        AsyncPropertyServiceRequest request1 = copyRequest(SET_VEHICLE_SPEED_AREA_ID_1_REQUEST);
        request1.setUpdateRateHz(20.0f);
        AsyncPropertyServiceRequest request2 = copyRequest(SET_VEHICLE_SPEED_AREA_ID_2_REQUEST);
        request2.setUpdateRateHz(21.0f);
        mPropertyHalService.setCarPropertyValuesAsync(List.of(request1, request2),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);


        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{1}, 20f,
                        /* enableVariableUpdateRate= */ true),
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{2}, 21f,
                        /* enableVariableUpdateRate= */ true));
        clearInvocations(mVehicleHal);
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);


        List<CarSubscription> carSubscriptions = new ArrayList<>();
        carSubscriptions.add(createCarSubscriptionOption(PERF_VEHICLE_SPEED, new int[] {1},
                /* updateRateHz= */ 40.0f, /* enableVur= */ true));
        mPropertyHalService.subscribeProperty(carSubscriptions);
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{1}, 40f,
                        /* enableVariableUpdateRate= */ true));
        clearInvocations(mVehicleHal);
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        carSubscriptions = new ArrayList<>();
        carSubscriptions.add(createCarSubscriptionOption(PERF_VEHICLE_SPEED, new int[] {2},
                /* updateRateHz= */ 41.0f, /* enableVur= */ true));
        mPropertyHalService.subscribeProperty(carSubscriptions);
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{2}, 41f,
                        /* enableVariableUpdateRate= */ true));
        clearInvocations(mVehicleHal);
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        mPropertyHalService.unsubscribeProperty(PERF_VEHICLE_SPEED);

        // After client unsubscribe, revert back to the internal subscription rate

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{1}, 20f,
                        /* enableVariableUpdateRate= */ true),
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{2}, 21f,
                        /* enableVariableUpdateRate= */ true));
        clearInvocations(mVehicleHal);
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);


        carSubscriptions = new ArrayList<>();
        carSubscriptions.add(createCarSubscriptionOption(PERF_VEHICLE_SPEED, new int[] {1},
                /* updateRateHz= */ 30.0f, /* enableVur= */ true));
        mPropertyHalService.subscribeProperty(carSubscriptions);
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{1}, 30f,
                        /* enableVariableUpdateRate= */ true));
        clearInvocations(mVehicleHal);
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        carSubscriptions = new ArrayList<>();
        carSubscriptions.add(createCarSubscriptionOption(PERF_VEHICLE_SPEED, new int[] {2},
                /* updateRateHz= */ 31.0f, /* enableVur= */ true));
        mPropertyHalService.subscribeProperty(carSubscriptions);
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{2}, 31f,
                        /* enableVariableUpdateRate= */ true));
        clearInvocations(mVehicleHal);
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        deliverOkaySetResult(setInvocationWrap.get(0));
        deliverOkayGetResult(getInvocationWrap.get(0), mPropValue2);

        // After the internal subscription is finished, the client subscription must be kept.
        // The internal subscription rate is lower than the client rate, so no rate change.
        verify(mVehicleHal, never()).subscribeProperty(any(), any());

        verify(mSetAsyncPropertyResultCallback).onSetValueResults(mAsyncResultCaptor.capture());
        assertThat(mAsyncResultCaptor.getValue().getList().get(0).getCarPropertyErrorCodes()
                .toCarPropertyAsyncErrorCode()).isEqualTo(STATUS_OK);
        assertThat(mAsyncResultCaptor.getValue().getList().get(1).getCarPropertyErrorCodes()
                .toCarPropertyAsyncErrorCode()).isEqualTo(STATUS_OK);

        mPropertyHalService.unsubscribeProperty(PERF_VEHICLE_SPEED);

        // After both client and internal unsubscription, the property must be unsubscribed.
        verify(mVehicleHal).unsubscribeProperty(any(), eq(PERF_VEHICLE_SPEED));

        verifyNoPendingRequest();
    }

    @Test
    public void testMultipleSetCarPropertyValuesAsync_overlappingSetAsync()
            throws Exception {
        when(mMockCarPropertyConfig2.getAreaIds()).thenReturn(new int[]{0, 1, 2});
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        AsyncPropertyServiceRequest request1 = copyRequest(SET_VEHICLE_SPEED_AREA_ID_1_REQUEST);
        request1.setUpdateRateHz(20.0f);
        AsyncPropertyServiceRequest request2 = copyRequest(SET_VEHICLE_SPEED_AREA_ID_2_REQUEST);
        request2.setUpdateRateHz(21.0f);
        mPropertyHalService.setCarPropertyValuesAsync(List.of(request1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);
        mPropertyHalService.setCarPropertyValuesAsync(List.of(request2),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        assertThat(setInvocationWrap).hasSize(2);
        assertThat(getInvocationWrap).hasSize(2);

        verify(mVehicleHal, times(2)).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getAllValues().get(0)).containsExactly(
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{1}, 20f,
                        /* enableVariableUpdateRate= */ true));
        assertThat(mListArgumentCaptor.getAllValues().get(1)).containsExactly(
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{2}, 21f,
                        /* enableVariableUpdateRate= */ true));
        clearInvocations(mVehicleHal);
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        List<CarSubscription> carSubscriptions = new ArrayList<>();
        carSubscriptions.add(createCarSubscriptionOption(PERF_VEHICLE_SPEED, new int[] {1},
                /* updateRateHz= */ 40.0f, /* enableVur= */ true));
        carSubscriptions.add(createCarSubscriptionOption(PERF_VEHICLE_SPEED, new int[] {2},
                /* updateRateHz= */ 41.0f, /* enableVur= */ true));
        mPropertyHalService.subscribeProperty(carSubscriptions);

        // Subscription rate has to be updated according to client subscription.
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{1}, 40f,
                        /* enableVariableUpdateRate= */ true),
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{2}, 41f,
                        /* enableVariableUpdateRate= */ true));
        clearInvocations(mVehicleHal);

        deliverOkaySetResult(setInvocationWrap.get(0));
        deliverOkayGetResult(getInvocationWrap.get(0), mPropValue2);

        // Unsubscribing to internal rate 20.0f and 21.0f does not change the current client rate
        // 40 and 41.
        verify(mVehicleHal, never()).subscribeProperty(any(), any());

        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        mPropertyHalService.unsubscribeProperty(PERF_VEHICLE_SPEED);
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(new HalSubscribeOptions(
                PERF_VEHICLE_SPEED, new int[]{2}, 21f, /* enableVariableUpdateRate= */ true));
        clearInvocations(mVehicleHal);

        deliverOkaySetResult(setInvocationWrap.get(1));
        deliverOkayGetResult(getInvocationWrap.get(1), mPropValue2);

        // Subscription rate has to be updated according to client subscription.
        verify(mVehicleHal).unsubscribeProperty(any(), eq(PERF_VEHICLE_SPEED));

        verifyNoPendingRequest();
    }

    @Test
    public void testSetCarPropertyValuesAsync_withSubscriptionFromDifferentAreaIds()
            throws Exception {
        when(mMockCarPropertyConfig2.getAreaIds()).thenReturn(new int[]{0, 1});
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        AsyncPropertyServiceRequest request = copyRequest(SET_VEHICLE_SPEED_AREA_ID_1_REQUEST);
        request.setUpdateRateHz(20.0f);
        mPropertyHalService.setCarPropertyValuesAsync(List.of(request),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getAllValues().get(0)).containsExactly(
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{1}, 20f,
                        /* enableVariableUpdateRate= */ true));
        clearInvocations(mVehicleHal);
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        mPropertyHalService.subscribeProperty(List.of(createCarSubscriptionOption(
                PERF_VEHICLE_SPEED, new int[]{0, 1}, /* updateRateHz= */ 40.0f,
                /* enableVur= */ true)));

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getAllValues().get(0)).containsExactly(
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{0, 1}, 40f,
                        /* enableVariableUpdateRate= */ true));
        clearInvocations(mVehicleHal);
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        // After client unsubscribe, revert back to the internal subscription rate.
        mPropertyHalService.unsubscribeProperty(PERF_VEHICLE_SPEED);

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getAllValues().get(0)).containsExactly(
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{1}, 20f,
                        /* enableVariableUpdateRate= */ true));
        clearInvocations(mVehicleHal);
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        // New client subscription must overwrite the internal rate.
        List<CarSubscription> carSubscriptions = new ArrayList<>();
        carSubscriptions.add(createCarSubscriptionOption(PERF_VEHICLE_SPEED, new int[] {1},
                /* updateRateHz= */ 50.0f, /* enableVur= */ true));
        mPropertyHalService.subscribeProperty(carSubscriptions);

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getAllValues().get(0)).containsExactly(
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{1}, 50f,
                        /* enableVariableUpdateRate= */ true));
        clearInvocations(mVehicleHal);
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);
        clearInvocations(mVehicleHal);

        // Finish the async set request.
        deliverOkaySetResult(setInvocationWrap.get(0));
        deliverOkayGetResult(getInvocationWrap.get(0), mPropValue2);

        verify(mSetAsyncPropertyResultCallback).onSetValueResults(mAsyncResultCaptor.capture());
        assertThat(mAsyncResultCaptor.getValue().getList().get(0).getCarPropertyErrorCodes()
                .toCarPropertyAsyncErrorCode()).isEqualTo(STATUS_OK);

        // After the internal subscription is finished, the client is still subscribed at 50hz
        // and no update rate change is required.
        verify(mVehicleHal, never()).subscribeProperty(any(), any());

        mPropertyHalService.unsubscribeProperty(PERF_VEHICLE_SPEED);

        // After both client and internal unsubscription, the property must be unsubscribed.
        verify(mVehicleHal).unsubscribeProperty(any(), eq(PERF_VEHICLE_SPEED));

        verifyNoPendingRequest();
    }

    @Test
    public void testSetCarPropertyValuesAsync_defaultSubscriptionRate()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_SPEED_REQUEST_ID_2),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 10,
                /* asyncRequestStartTime= */ 0);

        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        // Default sample rate is set to max sample rate.
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(speedHalSubscribeOption(100f));

        // Finish the async set request.
        deliverOkaySetResult(setInvocationWrap.get(0));
        deliverOkayGetResult(getInvocationWrap.get(0), mPropValue2);
    }

    @Test
    public void testSetCarPropertyValuesAsync_defaultSubscriptionRateDifferentAreaIds() {
        when(mMockCarPropertyConfig2.getAreaIds()).thenReturn(new int[]{0, 1});
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(
                List.of(SET_VEHICLE_SPEED_AREA_ID_1_REQUEST, SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 10,
                /* asyncRequestStartTime= */ 0);

        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        // Default sample rate is set to max sample rate.
        float maxSampleRate = 100.0f;
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(
                hvacHalSubscribeOption(),
                new HalSubscribeOptions(PERF_VEHICLE_SPEED, new int[]{1}, maxSampleRate,
                        /* enableVariableUpdateRate= */ true));

        // Finish the async set request.
        deliverOkaySetResult(setInvocationWrap.get(0));
        deliverOkayGetResult(getInvocationWrap.get(0), mPropValue2);
    }

    @Test
    public void testSetCarPropertyValuesAsync_setUpdateRateHz()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.subscribeProperty(List.of(createCarSubscriptionOption(
                PERF_VEHICLE_SPEED, new int[]{0}, /* updateRateHz= */ 22.0f,
                /* enableVur= */ true)));

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getAllValues().get(0)).containsExactly(
                speedHalSubscribeOption(22.0f));
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);
        clearInvocations(mVehicleHal);

        AsyncPropertyServiceRequest request1 = copyRequest(SET_SPEED_REQUEST_ID_2);
        request1.setUpdateRateHz(23.1f);
        AsyncPropertyServiceRequest request2 = copyRequest(SET_SPEED_REQUEST_ID_3);
        request2.setUpdateRateHz(23.2f);
        mPropertyHalService.setCarPropertyValuesAsync(List.of(request1, request2),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getAllValues().get(0)).containsExactly(
                speedHalSubscribeOption(23.2f));
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);
        clearInvocations(mVehicleHal);

        // Finish the async set request.
        deliverOkaySetResult(setInvocationWrap.get(0));
        deliverOkayGetResult(getInvocationWrap.get(0), mPropValue2);

        verify(mSetAsyncPropertyResultCallback).onSetValueResults(mAsyncResultCaptor.capture());
        // Both request must succeed.
        assertThat(mAsyncResultCaptor.getValue().getList()).hasSize(2);
        assertThat(mAsyncResultCaptor.getValue().getList().get(0).getCarPropertyErrorCodes()
                .toCarPropertyAsyncErrorCode()).isEqualTo(STATUS_OK);
        assertThat(mAsyncResultCaptor.getValue().getList().get(1).getCarPropertyErrorCodes()
                .toCarPropertyAsyncErrorCode()).isEqualTo(STATUS_OK);

        // After internal subscription complete, the client subscription rate must be kept.
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(speedHalSubscribeOption(22.0f));

        verifyNoPendingRequest();
    }

    @Test
    public void testSetCarPropertyValuesAsync_cancelledBeforePropertyUpdateEvent()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();
        List<HalServiceBase> serviceWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        // Because HVAC_TEMPERATURE_SET is ON_CHANGE property, the sample rate is 0.
        doAnswer((invocation) -> {
            serviceWrap.add(invocation.getArgument(0));
            return null;
        }).when(mVehicleHal).subscribeProperty(any(), anyList());
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        // Returns the set value result.
        deliverOkaySetResult(setInvocationWrap.get(0));

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(hvacHalSubscribeOption());

        // Cancel the ongoing request.
        mPropertyHalService.cancelRequests(new int[]{REQUEST_ID_1});

        // Notify the property is updated to the target value after the request is cancelled.
        serviceWrap.get(0).onHalEvents(List.of(mPropValue));

        verify(mSetAsyncPropertyResultCallback, never()).onSetValueResults(any());
        verify(mVehicleHal).unsubscribeProperty(any(), eq(HVAC_TEMPERATURE_SET));

        verifyNoPendingRequest();
    }

    @Test
    public void testSetCarPropertyValuesAsync_timeoutBeforePropertyUpdateEvent()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();
        List<HalServiceBase> serviceWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));
        // Because HVAC_TEMPERATURE_SET is ON_CHANGE property, the sample rate is 0.
        doAnswer((invocation) -> {
            serviceWrap.add(invocation.getArgument(0));
            return null;
        }).when(mVehicleHal).subscribeProperty(any(), anyList());
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        // Returns the set value result.
        deliverOkaySetResult(setInvocationWrap.get(0));

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(hvacHalSubscribeOption());

        List<AsyncGetSetRequest> setAsyncRequests = setInvocationWrap.get(0).getArgument(0);
        VehicleStubCallbackInterface callback = setInvocationWrap.get(0).getArgument(1);
        callback.onRequestsTimeout(
                List.of(setAsyncRequests.get(0).getServiceRequestId()));

        // Notify the property is updated to the target value after the request is cancelled.
        serviceWrap.get(0).onHalEvents(List.of(mPropValue));

        verify(mSetAsyncPropertyResultCallback).onSetValueResults(mAsyncResultCaptor.capture());
        assertThat(mAsyncResultCaptor.getValue().getList()).hasSize(1);
        assertThat(
                mAsyncResultCaptor.getValue().getList().get(0).getCarPropertyErrorCodes()
                        .toCarPropertyAsyncErrorCode())
                .isEqualTo(CarPropertyManager.STATUS_ERROR_TIMEOUT);
        verify(mVehicleHal).unsubscribeProperty(any(), eq(HVAC_TEMPERATURE_SET));

        verifyNoPendingRequest();
    }

    // If we receive errors for the [propId, areaId] we are setting via onPropertySetError, we must
    // fail the pending request.
    @Test
    public void testSetCarPropertyValuesAsync_onPropertySetError() throws RemoteException {
        when(mMockCarPropertyConfig1.getAreaIds()).thenReturn(new int[]{1});
        List<HalServiceBase> serviceWrap = new ArrayList<>();
        AsyncPropertyServiceRequest setPropertyRequest =
                new AsyncPropertyServiceRequest(1, HVAC_TEMPERATURE_SET, /* areaId= */ 1);

        doNothing().when(mVehicleHal).setAsync(anyList(),
                any(VehicleStubCallbackInterface.class));
        doNothing().when(mVehicleHal).getAsync(anyList(),
                any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            serviceWrap.add(invocation.getArgument(0));
            return null;
        }).when(mVehicleHal).subscribeProperty(any(), anyList());
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(setPropertyRequest),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        ArrayList<VehiclePropError> vehiclePropErrors = new ArrayList<>();
        VehiclePropError error1 = new VehiclePropError();
        error1.propId = HVAC_TEMPERATURE_SET;
        error1.areaId = 1;
        error1.errorCode = STATUS_NOT_AVAILABLE | (0x1234 << 16);
        // Error 2 has the wrong area ID and must be ignored.
        VehiclePropError error2 = new VehiclePropError();
        error2.propId = HVAC_TEMPERATURE_SET;
        error2.areaId = 2;
        error2.errorCode = STATUS_INTERNAL_ERROR;
        vehiclePropErrors.add(error1);
        vehiclePropErrors.add(error2);
        assertThat(serviceWrap).hasSize(1);
        serviceWrap.get(0).onPropertySetError(vehiclePropErrors);

        verify(mSetAsyncPropertyResultCallback).onSetValueResults(mAsyncResultCaptor.capture());
        assertThat(mAsyncResultCaptor.getValue().getList()).hasSize(1);
        assertThat(
                mAsyncResultCaptor.getValue().getList().get(0).getCarPropertyErrorCodes()
                        .toCarPropertyAsyncErrorCode())
                .isEqualTo(CarPropertyManager.STATUS_ERROR_NOT_AVAILABLE);
        assertThat(
                mAsyncResultCaptor.getValue().getList().get(0).getCarPropertyErrorCodes()
                        .getVendorErrorCode())
                .isEqualTo(0x1234);
        assertThat(mAsyncResultCaptor.getValue().getList().get(0).getCarPropertyErrorCodes()
                        .getSystemErrorCode())
                .isEqualTo(STATUS_NOT_AVAILABLE);
        verify(mVehicleHal).unsubscribeProperty(any(), eq(HVAC_TEMPERATURE_SET));

        verifyNoPendingRequest();
    }

    @Test
    public void testOnSetAsyncResults_RetryAndTimeout() throws RemoteException {
        doAnswer((invocation) -> {
            // For every request, we return retry result.
            return deliverTryAgainSetResult(invocation);
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 10,
                /* asyncRequestStartTime= */ 0);

        verify(mSetAsyncPropertyResultCallback, timeout(1000)).onSetValueResults(
                mAsyncResultCaptor.capture());
        assertThat(mAsyncResultCaptor.getValue().getList().get(0).getRequestId())
                .isEqualTo(REQUEST_ID_1);
        assertThat(
                mAsyncResultCaptor.getValue().getList().get(0).getCarPropertyErrorCodes()
                        .toCarPropertyAsyncErrorCode())
                .isEqualTo(CarPropertyManager.STATUS_ERROR_TIMEOUT);

        verifyNoPendingRequest();
    }

    @Test
    public void testOnSetAsyncResults_TimeoutFromVehicleStub() throws RemoteException {
        doAnswer((invocation) -> {
            Object[] args = invocation.getArguments();
            List requests = (List) args[0];
            AsyncGetSetRequest request = (AsyncGetSetRequest) requests.get(0);
            VehicleStubCallbackInterface callback = (VehicleStubCallbackInterface) args[1];
            // Simulate the request has already timed-out.
            callback.onRequestsTimeout(List.of(request.getServiceRequestId()));
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));

        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mSetAsyncPropertyResultCallback, timeout(1000)).onSetValueResults(
                mAsyncResultCaptor.capture());
        assertThat(mAsyncResultCaptor.getValue().getList().get(0).getRequestId())
                .isEqualTo(REQUEST_ID_1);
        assertThat(mAsyncResultCaptor.getValue().getList().get(0).getCarPropertyErrorCodes()
                        .toCarPropertyAsyncErrorCode())
                .isEqualTo(CarPropertyManager.STATUS_ERROR_TIMEOUT);

        verifyNoPendingRequest();
    }

    @Test
    public void testOnSetAsyncResults_errorResult() throws RemoteException {
        doAnswer((invocation) -> {
            return deliverErrorSetResult(invocation, RECEIVED_REQUEST_ID_1,
                    ERROR_CODES_INTERNAL);
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mSetAsyncPropertyResultCallback, timeout(1000)).onSetValueResults(
                mAsyncResultCaptor.capture());
        GetSetValueResult result = mAsyncResultCaptor.getValue().getList().get(0);
        assertThat(result.getRequestId()).isEqualTo(REQUEST_ID_1);
        assertThat(result.getCarPropertyErrorCodes().toCarPropertyAsyncErrorCode())
                .isEqualTo(CarPropertyManager.STATUS_ERROR_INTERNAL_ERROR);
        assertThat(result.getCarPropertyValue()).isEqualTo(null);

        verifyNoPendingRequest();
    }

    @Test
    public void setProperty_handlesHalAndMgrPropIdMismatch() {
        HalPropConfig mockPropConfig = mock(HalPropConfig.class);
        CarPropertyValue mockCarPropertyValue = mock(CarPropertyValue.class);
        when(mockCarPropertyValue.getPropertyId()).thenReturn(
                VehiclePropertyIds.VEHICLE_SPEED_DISPLAY_UNITS);
        when(mockCarPropertyValue.getAreaId()).thenReturn(0);
        when(mockCarPropertyValue.getValue()).thenReturn(1.0f);
        when(mockPropConfig.getPropId()).thenReturn(VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS);
        mPropertyHalService.takeProperties(List.of(mockPropConfig));

        mPropertyHalService.setProperty(mockCarPropertyValue);

        HalPropValue value = mPropValueBuilder.build(
                VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS, /* areaId= */ 0, /* value= */ 1.0f);
        verify(mVehicleHal).set(value);

        verifyNoPendingRequest();
    }

    @Test
    public void testCancelRequests_getCarPropertyValuesAsync() throws Exception {
        doReturn(mGetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();
        List<InvocationOnMock> invocationWrap = new ArrayList<>();
        doAnswer((invocation) -> {
            invocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));

        mPropertyHalService.getCarPropertyValuesAsync(List.of(
                GET_PROPERTY_SERVICE_REQUEST_1, GET_PROPERTY_SERVICE_REQUEST_2),
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        assertThat(invocationWrap).hasSize(1);

        // Cancel the first request.
        mPropertyHalService.cancelRequests(new int[]{REQUEST_ID_1});

        verify(mVehicleHal).cancelRequests(List.of(0));

        // Deliver the results.
        deliverOkayGetResult(invocationWrap.get(0));

        // We should only get the result for request 2.
        verify(mGetAsyncPropertyResultCallback, timeout(1000)).onGetValueResults(
                mAsyncResultCaptor.capture());
        assertThat(mAsyncResultCaptor.getValue().getList().get(0).getRequestId())
                .isEqualTo(REQUEST_ID_2);

        verifyNoPendingRequest();
    }

    @Test
    public void testCancelRequests_setCarPropertyValuesAsync() throws Exception {
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));

        mPropertyHalService.setCarPropertyValuesAsync(List.of(
                SET_HVAC_REQUEST_ID_1, SET_SPEED_REQUEST_ID_2),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        assertThat(setInvocationWrap).hasSize(1);
        assertThat(getInvocationWrap).hasSize(1);

        // Cancel the first request.
        mPropertyHalService.cancelRequests(new int[]{REQUEST_ID_1});

        // Note that because both the async set and the get init value request has the same manager
        // request ID, so they will all be cancelled.
        verify(mVehicleHal, timeout(1000).times(2)).cancelRequests(any());

        // Returns the set value result.
        deliverOkaySetResult(setInvocationWrap.get(0));
        // Returns the get initial value result.
        deliverOkayGetResult(getInvocationWrap.get(0));

        // We should only get the result for request 2.
        verify(mSetAsyncPropertyResultCallback, timeout(1000)).onSetValueResults(
                mAsyncResultCaptor.capture());
        assertThat(mAsyncResultCaptor.getValue().getList().get(0).getRequestId())
                .isEqualTo(REQUEST_ID_2);

        verifyNoPendingRequest();
    }

    @Test
    public void testCancelRequests_noPendingRequests() {
        mPropertyHalService.cancelRequests(new int[]{0});

        verify(mVehicleHal, never()).cancelRequests(any());

        verifyNoPendingRequest();
    }

    // Test that when the client binder died, all pending async requests must be cleared.
    @Test
    public void testOnBinderDied() throws Exception {
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();
        // We return the same binder object for both set and get here.
        doReturn(mSetAsyncPropertyResultBinder).when(mGetAsyncPropertyResultCallback).asBinder();
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<InvocationOnMock> getInvocationWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            getInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).getAsync(anyList(), any(VehicleStubCallbackInterface.class));

        mPropertyHalService.getCarPropertyValuesAsync(List.of(GET_PROPERTY_SERVICE_REQUEST_1),
                mGetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);
        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_SPEED_REQUEST_ID_2),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mSetAsyncPropertyResultBinder).linkToDeath(mDeathRecipientCaptor.capture(),
                anyInt());

        assertThat(setInvocationWrap).hasSize(1);
        // One is for async get, one is for initial value.
        assertThat(getInvocationWrap).hasSize(2);

        // Simulate client binder died.
        mDeathRecipientCaptor.getValue().binderDied();

        verify(mSetAsyncPropertyResultBinder).unlinkToDeath(any(), anyInt());

        // All async pending requests must be cleared.
        verifyNoPendingRequest();
    }

    @Test
    public void testGetPropertySync() throws Exception {
        HalPropValue value = mPropValueBuilder.build(INT32_PROP, /* areaId= */ 0, PROPERTY_VALUE);
        when(mVehicleHal.get(INT32_PROP, /* areaId= */ 0)).thenReturn(value);

        CarPropertyValue carPropValue = mPropertyHalService.getProperty(
                INT32_PROP, /* areaId= */ 0);

        assertThat(carPropValue.getValue()).isEqualTo(PROPERTY_VALUE);
    }

    @Test
    public void testGetPropertySyncWithCache() throws Exception {
        HalPropValue value = mPropValueBuilder.build(INT32_PROP, /* areaId= */ 0, PROPERTY_VALUE);
        when(mVehicleHal.get(INT32_PROP, /* areaId= */ 0)).thenReturn(value);
        mPropertyHalService.getProperty(INT32_PROP, /* areaId= */ 0);
        reset(mVehicleHal);

        CarPropertyValue carPropValue = mPropertyHalService.getProperty(
                INT32_PROP, /* areaId= */ 0);

        assertWithMessage("CarPropertyValue cached value").that(carPropValue.getValue())
                .isEqualTo(PROPERTY_VALUE);
        verify(mVehicleHal, never()).get(anyInt(), anyInt());
    }

    @Test
    public void testGetPropertySyncErrorPropStatus() throws Exception {
        HalPropValue value = mPropValueBuilder.build(
                AidlVehiclePropValueBuilder.newBuilder(INT32_PROP)
                        .setStatus(VehiclePropertyStatus.ERROR).addIntValues(0).build());
        when(mVehicleHal.get(INT32_PROP, /* areaId= */ 0)).thenReturn(value);

        assertThat(mPropertyHalService.getProperty(INT32_PROP, /*areaId=*/0)).isEqualTo(
                new CarPropertyValue<>(INT32_PROP, /*areaId=*/0,
                        CarPropertyValue.STATUS_ERROR, /*timestampNanos=*/0, Integer.valueOf(0)));
    }

    @Test
    public void testGetPropertySyncUnavailablePropStatus() throws Exception {
        HalPropValue value = mPropValueBuilder.build(
                AidlVehiclePropValueBuilder.newBuilder(INT32_PROP)
                        .setStatus(VehiclePropertyStatus.UNAVAILABLE).addIntValues(0).build());
        when(mVehicleHal.get(INT32_PROP, /* areaId= */ 0)).thenReturn(value);

        assertThat(mPropertyHalService.getProperty(INT32_PROP, /*areaId=*/0)).isEqualTo(
                new CarPropertyValue<>(INT32_PROP, /*areaId=*/0,
                        CarPropertyValue.STATUS_UNAVAILABLE, /*timestampNanos=*/0,
                        Integer.valueOf(0)));
    }

    @Test
    public void testGetPropertySyncInvalidProp() throws Exception {
        // This property has no valid int array element.
        HalPropValue value = mPropValueBuilder.build(
                AidlVehiclePropValueBuilder.newBuilder(INT32_PROP).build());
        when(mVehicleHal.get(INT32_PROP, /* areaId= */ 0)).thenReturn(value);

        assertThat(mPropertyHalService.getProperty(INT32_PROP, /*areaId=*/0)).isEqualTo(
                new CarPropertyValue<>(INT32_PROP, /*areaId=*/0,
                        CarPropertyValue.STATUS_ERROR, /*timestampNanos=*/0, Integer.valueOf(0)));
    }

    @Test
    public void testOnPropertySetError() throws Exception {
        ArrayList<VehiclePropError> vehiclePropErrors = new ArrayList<>();
        VehiclePropError error1 = new VehiclePropError();
        error1.propId = HVAC_TEMPERATURE_SET;
        error1.areaId = 1;
        error1.errorCode = STATUS_NOT_AVAILABLE | (0x1234 << 16);
        VehiclePropError error2 = new VehiclePropError();
        error2.propId = PERF_VEHICLE_SPEED;
        error2.areaId = 0;
        error2.errorCode = STATUS_INTERNAL_ERROR;
        vehiclePropErrors.add(error1);
        vehiclePropErrors.add(error2);

        mPropertyHalService.setPropertyHalListener(mPropertyHalListener);
        mPropertyHalService.onPropertySetError(vehiclePropErrors);

        verify(mPropertyHalListener).onPropertySetError(HVAC_TEMPERATURE_SET, 1,
                CarPropertyManager.CAR_SET_PROPERTY_ERROR_CODE_PROPERTY_NOT_AVAILABLE);
        verify(mPropertyHalListener).onPropertySetError(PERF_VEHICLE_SPEED, 0,
                CarPropertyManager.CAR_SET_PROPERTY_ERROR_CODE_UNKNOWN);
    }

    @Test
    public void testNoCustomizeVendorPermission() throws Exception {
        HalPropConfig vendor1Config = mock(HalPropConfig.class);
        when(vendor1Config.getPropId()).thenReturn(VENDOR_PROPERTY_1);
        when(mVehicleHal.getPropConfig(SUPPORT_CUSTOMIZE_VENDOR_PERMISSION)).thenReturn(null);

        mPropertyHalService.takeProperties(List.of(vendor1Config));

        // By default we require PERMISSION_VENDOR_EXTENSION for getting/setting vendor props.
        assertThat(mPropertyHalService.getReadPermission(VENDOR_PROPERTY_1))
                .isEqualTo(PERMISSION_VENDOR_EXTENSION);
        assertThat(mPropertyHalService.getWritePermission(VENDOR_PROPERTY_1))
                .isEqualTo(PERMISSION_VENDOR_EXTENSION);
    }

    @Test
    public void testCustomizeVendorPermission() throws Exception {
        HalPropConfig mockVendorPermConfig = mock(HalPropConfig.class);
        // Use the same test config we used in PropertyHalServiceTest.
        when(mockVendorPermConfig.getConfigArray()).thenReturn(new int[]{
                VENDOR_PROPERTY_1,
                VehicleVendorPermission.PERMISSION_DEFAULT,
                VehicleVendorPermission.PERMISSION_NOT_ACCESSIBLE,
                VENDOR_PROPERTY_2,
                VehicleVendorPermission.PERMISSION_GET_VENDOR_CATEGORY_ENGINE,
                VehicleVendorPermission.PERMISSION_SET_VENDOR_CATEGORY_ENGINE,
                VENDOR_PROPERTY_3,
                VehicleVendorPermission.PERMISSION_GET_VENDOR_CATEGORY_INFO,
                VehicleVendorPermission.PERMISSION_DEFAULT
        });
        when(mVehicleHal.getPropConfig(SUPPORT_CUSTOMIZE_VENDOR_PERMISSION)).thenReturn(
                mockVendorPermConfig);
        HalPropConfig vendor1Config = mock(HalPropConfig.class);
        when(vendor1Config.getPropId()).thenReturn(VENDOR_PROPERTY_1);
        HalPropConfig vendor2Config = mock(HalPropConfig.class);
        when(vendor2Config.getPropId()).thenReturn(VENDOR_PROPERTY_2);
        HalPropConfig vendor3Config = mock(HalPropConfig.class);
        when(vendor3Config.getPropId()).thenReturn(VENDOR_PROPERTY_3);

        mPropertyHalService.takeProperties(List.of(vendor1Config, vendor2Config,
                vendor3Config));

        assertThat(mPropertyHalService.getReadPermission(VENDOR_PROPERTY_1))
                .isEqualTo(PERMISSION_VENDOR_EXTENSION);
        assertThat(mPropertyHalService.getWritePermission(VENDOR_PROPERTY_1)).isNull();
        assertThat(mPropertyHalService.getReadPermission(VENDOR_PROPERTY_2))
                .isEqualTo(PERMISSION_GET_CAR_VENDOR_CATEGORY_ENGINE);
        assertThat(mPropertyHalService.getWritePermission(VENDOR_PROPERTY_2))
                .isEqualTo(PERMISSION_SET_CAR_VENDOR_CATEGORY_ENGINE);
        assertThat(mPropertyHalService.getReadPermission(VENDOR_PROPERTY_3))
                .isEqualTo(PERMISSION_GET_CAR_VENDOR_CATEGORY_INFO);
        assertThat(mPropertyHalService.getWritePermission(VENDOR_PROPERTY_3))
                .isEqualTo(PERMISSION_VENDOR_EXTENSION);
    }

    @Test
    public void testHalSubscribeOptions_equals() {
        HalSubscribeOptions options1 = new HalSubscribeOptions(/* halPropId= */ 5,
                /* areaIds= */ new int[]{0, 3}, /* updateRateHz= */ 53f);
        HalSubscribeOptions options2 = new HalSubscribeOptions(/* halPropId= */ 5,
                /* areaIds= */ new int[]{0, 3}, /* updateRateHz= */ 53f);

        assertWithMessage("Equal hal subscribe options")
                .that(options1.equals(options2)).isTrue();
    }

    @Test
    public void testHalSubscribeOptions_notEquals() {
        HalSubscribeOptions options1 = new HalSubscribeOptions(/* halPropId= */ 5,
                /* areaIds= */ new int[]{0, 3}, /* updateRateHz= */ 55f,
                /* enableVariableUpdateRate= */ true);
        HalSubscribeOptions options2 = new HalSubscribeOptions(/* halPropId= */ 5,
                /* areaIds= */ new int[]{0, 3}, /* updateRateHz= */ 53f,
                /* enableVariableUpdateRate= */ true);

        assertWithMessage("Non-equal hal subscribe options")
                .that(options1.equals(options2)).isFalse();
    }

    @Test
    public void testHalSubscribeOptions_hashcode() {
        HalSubscribeOptions options1 = new HalSubscribeOptions(/* halPropId= */ 5,
                /* areaIds= */ new int[]{0, 3}, /* updateRateHz= */ 53f,
                /* enableVariableUpdateRate= */ true);
        HalSubscribeOptions options2 = new HalSubscribeOptions(/* halPropId= */ 5,
                /* areaIds= */ new int[]{0, 3}, /* updateRateHz= */ 53f,
                /* enableVariableUpdateRate= */ true);

        assertWithMessage("Hashcode hal subscribe options")
                .that(options1.hashCode()).isEqualTo(options2.hashCode());
    }

    /**
     * This test is for verifying when we need to update sample rate for multiple properties, both
     * subscribeProperty and unsubscribeProperty may happen.
     */
    @Test
    public void testOnHalEvnts_unsubscribeAndUpdateSampleRates()
            throws Exception {
        List<InvocationOnMock> setInvocationWrap = new ArrayList<>();
        List<HalServiceBase> serviceWrap = new ArrayList<>();

        doAnswer((invocation) -> {
            setInvocationWrap.add(invocation);
            return null;
        }).when(mVehicleHal).setAsync(anyList(), any(VehicleStubCallbackInterface.class));
        doAnswer((invocation) -> {
            serviceWrap.add(invocation.getArgument(0));
            return null;
        }).when(mVehicleHal).subscribeProperty(any(), anyList());
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();

        // Subscribe to speed at 40hz.
        mPropertyHalService.subscribeProperty(List.of(
                createCarSubscriptionOption(
                        PERF_VEHICLE_SPEED, new int[]{0}, /* updateRateHz= */ 40.0f,
                        /* enableVur= */ true)));
        // Issues set async request, which should cause speed to be subscribed at 100hz.
        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_HVAC_REQUEST_ID_1),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);
        // Issues set async request, which should cause hvac to be subscribed at 0hz.
        mPropertyHalService.setCarPropertyValuesAsync(List.of(SET_SPEED_REQUEST_ID_2),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        // Returns the set value result.
        deliverOkaySetResult(setInvocationWrap.get(0));
        deliverOkaySetResult(setInvocationWrap.get(1));

        verify(mVehicleHal, times(3)).subscribeProperty(any(), mListArgumentCaptor.capture());
        // This is caused by subscribeProperty.
        assertThat(mListArgumentCaptor.getAllValues().get(0)).containsExactly(
                speedHalSubscribeOption(40.0f));
        // This is caused by setCarPropertyValuesAsync.
        assertThat(mListArgumentCaptor.getAllValues().get(1)).containsExactly(
                hvacHalSubscribeOption());
        // This is caused by setCarPropertyValuesAsync.
        assertThat(mListArgumentCaptor.getAllValues().get(2)).containsExactly(
                speedHalSubscribeOption(100.0f));

        clearInvocations(mVehicleHal);

        // Send change value events for set requests.
        serviceWrap.get(0).onHalEvents(List.of(mPropValue, mPropValue2));

        // We no longer need to subscribe to temp.
        verify(mVehicleHal).unsubscribeProperty(any(), eq(HVAC_TEMPERATURE_SET));
        // Speed should be subscribed at 40hz.
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getAllValues().get(3)).containsExactly(
                speedHalSubscribeOption(40.0f));

        verifyNoPendingRequest();
    }

    @Test
    public void testSubscribeProperty_exceptionFromVhal() throws Exception {
        doThrow(new ServiceSpecificException(0)).when(mVehicleHal).subscribeProperty(
                any(), anyList());

        assertThrows(ServiceSpecificException.class, () ->
                mPropertyHalService.subscribeProperty(List.of(
                        createCarSubscriptionOption(
                                PERF_VEHICLE_SPEED, new int[]{0}, /* updateRateHz= */ 40.0f,
                                /* enableVur= */ true))));
    }

    /**
     * Tests that if we receive exception from underlying layer, client must be able to retry the
     * operation and causes a retry to VHAL. If the error goes away in VHAL, the retry must succeed.
     */
    @Test
    public void testSubscribeProperty_exceptionFromVhal_retryFixed() throws Exception {
        doThrow(new ServiceSpecificException(0)).when(mVehicleHal).subscribeProperty(
                any(), anyList());

        assertThrows(ServiceSpecificException.class, () ->
                mPropertyHalService.subscribeProperty(List.of(
                        createCarSubscriptionOption(
                                PERF_VEHICLE_SPEED, new int[]{0}, /* updateRateHz= */ 40.0f,
                                /* enableVur= */ true))));

        clearInvocations(mVehicleHal);
        // Simulate the error has been fixed.
        doNothing().when(mVehicleHal).subscribeProperty(any(), anyList());

        // Retry the operation.
        mPropertyHalService.subscribeProperty(List.of(
                createCarSubscriptionOption(
                        PERF_VEHICLE_SPEED, new int[]{0}, /* updateRateHz= */ 40.0f,
                        /* enableVur= */ true)));

        // The retry request must go to VehicleHal.
        verify(mVehicleHal).subscribeProperty(any(), anyList());

        mPropertyHalService.unsubscribeProperty(PERF_VEHICLE_SPEED);

        verify(mVehicleHal).unsubscribeProperty(any(), eq(PERF_VEHICLE_SPEED));

        verifyNoPendingRequest();
    }

    @Test
    public void testSubscribeProperty_enableVur() throws Exception {
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        mPropertyHalService.subscribeProperty(List.of(createCarSubscriptionOption(
                PERF_VEHICLE_SPEED, new int[]{0}, /* updateRateHz= */ 40.0f,
                /* enableVur= */ true)));

        // Subscription rate has to be updated according to client subscription.
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(speedHalSubscribeOption(40f));
    }

    @Test
    public void testSubscribeProperty_disablesVur() throws Exception {
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        mPropertyHalService.subscribeProperty(List.of(createCarSubscriptionOption(
                PERF_VEHICLE_SPEED, new int[]{0}, /* updateRateHz= */ 40.0f,
                /* enableVur= */ false)));

        // Subscription rate has to be updated according to client subscription.
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(new HalSubscribeOptions(
                PERF_VEHICLE_SPEED, new int[]{0}, 40.0f, /* enableVariableUpdateRate= */ false));
    }

    @Test
    public void testSubscribeProperty_withResolution() throws Exception {
        mListArgumentCaptor = ArgumentCaptor.forClass(List.class);

        mPropertyHalService.subscribeProperty(List.of(createCarSubscriptionOption(
                PERF_VEHICLE_SPEED, new int[]{0}, /* updateRateHz= */ 40.0f,
                /* enableVur= */ false, /* resolution */ 1.0f)));

        // Subscription rate has to be updated according to client subscription.
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(new HalSubscribeOptions(
                PERF_VEHICLE_SPEED, new int[]{0}, 40.0f, /* enableVariableUpdateRate= */ false,
                /* resolution */ 1.0f));
    }

    @Test
    public void testSubscribeProperty_setAsync_clientDisablesVur() throws Exception {
        doReturn(mSetAsyncPropertyResultBinder).when(mSetAsyncPropertyResultCallback).asBinder();
        AsyncPropertyServiceRequest request = copyRequest(SET_SPEED_REQUEST_ID_2);
        request.setUpdateRateHz(20.0f);

        mPropertyHalService.setCarPropertyValuesAsync(List.of(request),
                mSetAsyncPropertyResultCallback, /* timeoutInMs= */ 1000,
                /* asyncRequestStartTime= */ 0);

        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(speedHalSubscribeOption(20f));
        clearInvocations(mVehicleHal);

        // Client disables Vur.
        mPropertyHalService.subscribeProperty(List.of(createCarSubscriptionOption(
                PERF_VEHICLE_SPEED, new int[]{0}, /* updateRateHz= */ 10.0f,
                /* enableVur= */ false)));

        // Vur must be turned off.
        verify(mVehicleHal).subscribeProperty(any(), mListArgumentCaptor.capture());
        assertThat(mListArgumentCaptor.getValue()).containsExactly(new HalSubscribeOptions(
                PERF_VEHICLE_SPEED, new int[]{0}, 20.0f, /* enableVariableUpdateRate= */ false));

        mPropertyHalService.cancelRequests(new int[]{REQUEST_ID_2});
    }

    @Test
    public void testUnubscribeProperty_exceptionFromVhal() throws Exception {
        mPropertyHalService.subscribeProperty(List.of(
                createCarSubscriptionOption(
                        PERF_VEHICLE_SPEED, new int[]{0}, /* updateRateHz= */ 40.0f,
                        /* enableVur= */ true)));
        doThrow(new ServiceSpecificException(0)).when(mVehicleHal).unsubscribeProperty(
                any(), anyInt());

        assertThrows(ServiceSpecificException.class, () ->
                mPropertyHalService.unsubscribeProperty(PERF_VEHICLE_SPEED));
    }

    /**
     * Tests that if we receive exception from underlying layer, client must be able to retry the
     * operation and causes a retry to VHAL. If the error goes away in VHAL, the retry must succeed.
     */
    @Test
    public void tesUnsubscribeProperty_exceptionFromVhal_retryFixed() throws Exception {
        mPropertyHalService.subscribeProperty(List.of(
                createCarSubscriptionOption(
                        PERF_VEHICLE_SPEED, new int[]{0}, /* updateRateHz= */ 40.0f,
                        /* enableVur= */ true)));
        doThrow(new ServiceSpecificException(0)).when(mVehicleHal).unsubscribeProperty(
                any(), anyInt());

        assertThrows(ServiceSpecificException.class, () ->
                mPropertyHalService.unsubscribeProperty(PERF_VEHICLE_SPEED));

        // Simulate the error has been fixed.
        clearInvocations(mVehicleHal);
        doNothing().when(mVehicleHal).unsubscribeProperty(any(), anyInt());

        mPropertyHalService.unsubscribeProperty(PERF_VEHICLE_SPEED);

        // The retry request must go to VehicleHal.
        verify(mVehicleHal).unsubscribeProperty(any(), eq(PERF_VEHICLE_SPEED));
    }

    @Test
    public void testGetMinMaxSupportedValue_dynamicSupportedValuesNotSupported() {
        when(mVehicleHal.isSupportedValuesImplemented(
                newPropIdAreaId(PERF_VEHICLE_SPEED, 0))).thenReturn(false);
        int areaId = 0;
        float minValue = 1.23f;
        float maxValue = 3.21f;
        var areaIdConfig = new AreaIdConfig.Builder(VEHICLE_PROPERTY_ACCESS_READ, areaId)
                .setMinValue(minValue).setMaxValue(maxValue).build();

        var minMaxSupportedPropertyValue = mPropertyHalService.getMinMaxSupportedValue(
                PERF_VEHICLE_SPEED, 0, areaIdConfig);

        expectThat(minMaxSupportedPropertyValue.minValue.getParcelable(RawPropertyValue.class)
                .getTypedValue()).isEqualTo(minValue);
        expectThat(minMaxSupportedPropertyValue.maxValue.getParcelable(RawPropertyValue.class)
                .getTypedValue()).isEqualTo(maxValue);
    }

    @Test
    public void testGetMinMaxSupportedValue_dynamicSupportedValuesNotSupported_nullMinValue() {
        when(mVehicleHal.isSupportedValuesImplemented(
                newPropIdAreaId(PERF_VEHICLE_SPEED, 0))).thenReturn(false);
        int areaId = 0;
        float maxValue = 3.21f;
        var areaIdConfig = new AreaIdConfig.Builder(VEHICLE_PROPERTY_ACCESS_READ, areaId)
                .setMaxValue(maxValue).build();

        var minMaxSupportedPropertyValue = mPropertyHalService.getMinMaxSupportedValue(
                PERF_VEHICLE_SPEED, 0, areaIdConfig);

        expectThat(minMaxSupportedPropertyValue.minValue.getParcelable(RawPropertyValue.class))
                .isNull();
        expectThat(minMaxSupportedPropertyValue.maxValue.getParcelable(RawPropertyValue.class)
                .getTypedValue()).isEqualTo(maxValue);
    }

    @Test
    public void testGetMinMaxSupportedValue_dynamicSupportedValuesNotSupported_nullMaxValue() {
        when(mVehicleHal.isSupportedValuesImplemented(
                newPropIdAreaId(PERF_VEHICLE_SPEED, 0))).thenReturn(false);
        int areaId = 0;
        float minValue = 1.23f;
        var areaIdConfig = new AreaIdConfig.Builder(VEHICLE_PROPERTY_ACCESS_READ, areaId)
                .setMinValue(minValue).build();

        var minMaxSupportedPropertyValue = mPropertyHalService.getMinMaxSupportedValue(
                PERF_VEHICLE_SPEED, 0, areaIdConfig);

        expectThat(minMaxSupportedPropertyValue.minValue.getParcelable(RawPropertyValue.class)
                .getTypedValue()).isEqualTo(minValue);
        expectThat(minMaxSupportedPropertyValue.maxValue.getParcelable(RawPropertyValue.class))
                .isNull();
    }

    @Test
    public void testGetMinMaxSupportedValue() {
        when(mVehicleHal.isSupportedValuesImplemented(
                newPropIdAreaId(PERF_VEHICLE_SPEED, 0))).thenReturn(true);
        int areaId = 0;
        float minValue = 1.23f;
        float maxValue = 3.21f;
        var minRawPropValues = new RawPropValues();
        minRawPropValues.floatValues = new float[]{minValue};
        var maxRawPropValues = new RawPropValues();
        maxRawPropValues.floatValues = new float[]{maxValue};
        var minMaxSupportedRawPropValues = new MinMaxSupportedRawPropValues(minRawPropValues,
                maxRawPropValues);

        when(mVehicleHal.getMinMaxSupportedValue(PERF_VEHICLE_SPEED, areaId)).thenReturn(
                minMaxSupportedRawPropValues);

        var areaIdConfig = new AreaIdConfig.Builder(PERF_VEHICLE_SPEED, areaId)
                .setHasMinSupportedValue(true).setHasMaxSupportedValue(true).build();

        var minMaxSupportedPropertyValue = mPropertyHalService.getMinMaxSupportedValue(
                PERF_VEHICLE_SPEED, areaId, areaIdConfig);

        expectThat(minMaxSupportedPropertyValue.minValue.getParcelable(RawPropertyValue.class)
                .getTypedValue()).isEqualTo(minValue);
        expectThat(minMaxSupportedPropertyValue.maxValue.getParcelable(RawPropertyValue.class)
                .getTypedValue()).isEqualTo(maxValue);
    }

    @Test
    public void testGetMinMaxSupportedValue_halPropConfigNotFound() {
        when(mVehicleHal.isSupportedValuesImplemented(any())).thenReturn(true);
        int areaId = 0;
        int mgrPropId = -1;
        var areaIdConfig = new AreaIdConfig.Builder(mgrPropId, areaId).build();

        // In fact, CarPropertyService verifies the property ID before calling
        // PropertyHalService.getMinMaxSupportedValue, so this should never happen.
        assertThrows(IllegalArgumentException.class, () -> {
            mPropertyHalService.getMinMaxSupportedValue(mgrPropId, areaId, areaIdConfig);
        });
    }

    @Test
    public void testGetMinMaxSupportedValue_maxValueNull() {
        when(mVehicleHal.isSupportedValuesImplemented(
                newPropIdAreaId(PERF_VEHICLE_SPEED, 0))).thenReturn(true);
        int areaId = 0;
        float minValue = 1.23f;
        var minRawPropValues = new RawPropValues();
        minRawPropValues.floatValues = new float[]{minValue};
        var minMaxSupportedRawPropValues = new MinMaxSupportedRawPropValues(minRawPropValues,
                /* maxValue= */ null);

        when(mVehicleHal.getMinMaxSupportedValue(PERF_VEHICLE_SPEED, areaId)).thenReturn(
                minMaxSupportedRawPropValues);

        var areaIdConfig = new AreaIdConfig.Builder(PERF_VEHICLE_SPEED, areaId)
                .setHasMinSupportedValue(true).build();

        var minMaxSupportedPropertyValue = mPropertyHalService.getMinMaxSupportedValue(
                PERF_VEHICLE_SPEED, areaId, areaIdConfig);

        expectThat(minMaxSupportedPropertyValue.minValue.getParcelable(RawPropertyValue.class)
                .getTypedValue()).isEqualTo(minValue);
        expectThat(minMaxSupportedPropertyValue.maxValue.getParcelable(RawPropertyValue.class))
                .isNull();
    }

    @Test
    public void testGetMinMaxSupportedValue_minValueNull() {
        when(mVehicleHal.isSupportedValuesImplemented(
                newPropIdAreaId(PERF_VEHICLE_SPEED, 0))).thenReturn(true);
        int areaId = 0;
        float maxValue = 3.21f;
        var maxRawPropValues = new RawPropValues();
        maxRawPropValues.floatValues = new float[]{maxValue};
        var minMaxSupportedRawPropValues = new MinMaxSupportedRawPropValues(/* minValue= */ null,
                maxRawPropValues);

        when(mVehicleHal.getMinMaxSupportedValue(PERF_VEHICLE_SPEED, areaId)).thenReturn(
                minMaxSupportedRawPropValues);

        var areaIdConfig = new AreaIdConfig.Builder(PERF_VEHICLE_SPEED, areaId)
                .setHasMaxSupportedValue(true).build();

        var minMaxSupportedPropertyValue = mPropertyHalService.getMinMaxSupportedValue(
                PERF_VEHICLE_SPEED, areaId, areaIdConfig);

        expectThat(minMaxSupportedPropertyValue.minValue.getParcelable(RawPropertyValue.class))
                .isNull();
        expectThat(minMaxSupportedPropertyValue.maxValue.getParcelable(RawPropertyValue.class)
                .getTypedValue()).isEqualTo(maxValue);
    }

    @Test
    public void testGetMinMaxSupportedValue_notSpecified() {
        when(mVehicleHal.isSupportedValuesImplemented(
                newPropIdAreaId(PERF_VEHICLE_SPEED, 0))).thenReturn(true);
        int areaId = 0;

        var areaIdConfig = new AreaIdConfig.Builder(PERF_VEHICLE_SPEED, areaId).build();

        var minMaxSupportedPropertyValue = mPropertyHalService.getMinMaxSupportedValue(
                PERF_VEHICLE_SPEED, areaId, areaIdConfig);

        expectThat(minMaxSupportedPropertyValue.minValue.getParcelable(RawPropertyValue.class))
                .isNull();
        expectThat(minMaxSupportedPropertyValue.maxValue.getParcelable(RawPropertyValue.class))
                .isNull();
    }

    // Verifies that EV_CHARGE_CURRENT_DRAW_LIMIT max value comes from config array.
    @Test
    public void testGetMinMaxSupportedValue_EV_CHARGE_CURRENT_DRAW_LIMIT_fromConfig() {
        int mgrPropId = EV_CHARGE_CURRENT_DRAW_LIMIT;
        // This property does not implement the new APIs so it falls back to config array.
        when(mVehicleHal.isSupportedValuesImplemented(newPropIdAreaId(mgrPropId, 0)))
                .thenReturn(false);
        int areaId = 0;
        int maxValue = 123;

        var vehiclePropConfig = new VehiclePropConfig();
        vehiclePropConfig.prop = VehicleProperty.EV_CHARGE_CURRENT_DRAW_LIMIT;
        vehiclePropConfig.access = VehiclePropertyAccess.READ;
        vehiclePropConfig.changeMode = VehiclePropertyChangeMode.ON_CHANGE;
        vehiclePropConfig.configArray = new int[]{maxValue};
        HalPropConfig halPropConfig = new AidlHalPropConfig(vehiclePropConfig);
        CarPropertyConfig carPropertyConfig = halPropConfig.toCarPropertyConfig(
                mgrPropId, PropertyHalServiceConfigs.newConfigs(), /* isVhalPropId= */ false);

        mPropertyHalService.takeProperties(List.of(halPropConfig));

        var areaIdConfig = carPropertyConfig.getAreaIdConfig(areaId);

        expectThat(areaIdConfig.hasMinSupportedValue()).isTrue();
        expectThat(areaIdConfig.hasMaxSupportedValue()).isTrue();

        var minMaxSupportedPropertyValue = mPropertyHalService.getMinMaxSupportedValue(
                mgrPropId, areaId, areaIdConfig);

        expectThat(minMaxSupportedPropertyValue.minValue.getParcelable(RawPropertyValue.class)
                .getTypedValue()).isEqualTo(0.f);
        expectThat(minMaxSupportedPropertyValue.maxValue.getParcelable(RawPropertyValue.class)
                .getTypedValue()).isEqualTo((float) maxValue);
    }

    @Test
    public void testGetMinMaxSupportedValue_EV_CHARGE_CURRENT_DRAW_LIMIT_noConfig() {
        int mgrPropId = EV_CHARGE_CURRENT_DRAW_LIMIT;
        // This property does not implement the new APIs so it falls back to config array.
        when(mVehicleHal.isSupportedValuesImplemented(newPropIdAreaId(mgrPropId, 0)))
                .thenReturn(false);
        int areaId = 0;

        var vehiclePropConfig = new VehiclePropConfig();
        vehiclePropConfig.prop = VehicleProperty.EV_CHARGE_CURRENT_DRAW_LIMIT;
        vehiclePropConfig.access = VehiclePropertyAccess.READ;
        vehiclePropConfig.changeMode = VehiclePropertyChangeMode.ON_CHANGE;
        // No element in config array.
        vehiclePropConfig.configArray = new int[]{};
        HalPropConfig halPropConfig = new AidlHalPropConfig(vehiclePropConfig);
        CarPropertyConfig carPropertyConfig = halPropConfig.toCarPropertyConfig(
                mgrPropId, PropertyHalServiceConfigs.newConfigs(), /* isVhalPropId= */ false);

        mPropertyHalService.takeProperties(List.of(halPropConfig));

        var areaIdConfig = carPropertyConfig.getAreaIdConfig(areaId);

        expectThat(areaIdConfig.hasMinSupportedValue()).isFalse();
        expectThat(areaIdConfig.hasMaxSupportedValue()).isFalse();

        var minMaxSupportedPropertyValue = mPropertyHalService.getMinMaxSupportedValue(
                mgrPropId, areaId, areaIdConfig);

        expectThat(minMaxSupportedPropertyValue.minValue.getParcelable(RawPropertyValue.class))
                .isNull();
        expectThat(minMaxSupportedPropertyValue.maxValue.getParcelable(RawPropertyValue.class))
                .isNull();
    }

    @Test
    public void testGetSupportedValuesList_dynamicSupportedValuesNotSupported() {
        when(mVehicleHal.isSupportedValuesImplemented(
                newPropIdAreaId(PERF_VEHICLE_SPEED, 0))).thenReturn(false);
        int areaId = 0;
        var testSupportedValues = new ArrayList<Float>(Arrays.asList(1.1f, 2.2f));
        var areaIdConfig = new AreaIdConfig.Builder(VEHICLE_PROPERTY_ACCESS_READ, areaId)
                .setSupportedEnumValues(testSupportedValues).build();

        var supportedValuesList = mPropertyHalService.getSupportedValuesList(
                PERF_VEHICLE_SPEED, 0, areaIdConfig);

        assertThat(supportedValuesList).hasSize(2);
        expectThat(supportedValuesList.get(0).getTypedValue()).isEqualTo(1.1f);
        expectThat(supportedValuesList.get(1).getTypedValue()).isEqualTo(2.2f);
    }

    @Test
    public void testGetSupportedValuesList_dynamicSupportedValuesNotSupported_notSpecified() {
        when(mVehicleHal.isSupportedValuesImplemented(
                newPropIdAreaId(PERF_VEHICLE_SPEED, 0))).thenReturn(false);
        int areaId = 0;
        var testSupportedValues = new ArrayList<Float>(Arrays.asList(1.1f, 2.2f));
        var areaIdConfig = new AreaIdConfig.Builder(VEHICLE_PROPERTY_ACCESS_READ, areaId)
                .setHasSupportedValuesList(false).build();

        var supportedValuesList = mPropertyHalService.getSupportedValuesList(
                PERF_VEHICLE_SPEED, 0, areaIdConfig);

        assertThat(supportedValuesList).isNull();
    }

    @Test
    public void testGetSupportedValuesList() {
        when(mVehicleHal.isSupportedValuesImplemented(
                newPropIdAreaId(PERF_VEHICLE_SPEED, 0))).thenReturn(true);
        int areaId = 0;
        float value1 = 1.23f;
        float value2 = 3.21f;
        var rawPropValues1 = new RawPropValues();
        rawPropValues1.floatValues = new float[]{value1};
        var rawPropValues2 = new RawPropValues();
        rawPropValues2.floatValues = new float[]{value2};

        when(mVehicleHal.getSupportedValuesList(PERF_VEHICLE_SPEED, areaId)).thenReturn(
                List.of(rawPropValues1, rawPropValues2));

        var areaIdConfig = new AreaIdConfig.Builder(PERF_VEHICLE_SPEED, areaId)
                .setHasSupportedValuesList(true).build();

        var supportedValuesList = mPropertyHalService.getSupportedValuesList(
                PERF_VEHICLE_SPEED, areaId, areaIdConfig);

        assertThat(supportedValuesList).hasSize(2);
        expectThat(supportedValuesList.get(0).getTypedValue()).isEqualTo(value1);
        expectThat(supportedValuesList.get(1).getTypedValue()).isEqualTo(value2);
    }

    @Test
    public void testGetSupportedValuesList_floatSorted() {
        when(mVehicleHal.isSupportedValuesImplemented(
                newPropIdAreaId(PERF_VEHICLE_SPEED, 0))).thenReturn(true);
        int areaId = 0;
        float value1 = 1f;
        float value2 = 2f;
        float value3 = 3f;
        float value4 = 4f;
        var rawPropValues1 = new RawPropValues();
        rawPropValues1.floatValues = new float[]{value1};
        var rawPropValues2 = new RawPropValues();
        rawPropValues2.floatValues = new float[]{value2};
        var rawPropValues3 = new RawPropValues();
        rawPropValues3.floatValues = new float[]{value3};
        var rawPropValues4 = new RawPropValues();
        rawPropValues4.floatValues = new float[]{value4};

        // Randomize the order.
        when(mVehicleHal.getSupportedValuesList(PERF_VEHICLE_SPEED, areaId)).thenReturn(
                List.of(rawPropValues1, rawPropValues3, rawPropValues2, rawPropValues4));

        var areaIdConfig = new AreaIdConfig.Builder(PERF_VEHICLE_SPEED, areaId)
                .setHasSupportedValuesList(true).build();

        var supportedValuesList = mPropertyHalService.getSupportedValuesList(
                PERF_VEHICLE_SPEED, areaId, areaIdConfig);

        assertThat(supportedValuesList).hasSize(4);
        expectThat(supportedValuesList.get(0).getTypedValue()).isEqualTo(value1);
        expectThat(supportedValuesList.get(1).getTypedValue()).isEqualTo(value2);
        expectThat(supportedValuesList.get(2).getTypedValue()).isEqualTo(value3);
        expectThat(supportedValuesList.get(3).getTypedValue()).isEqualTo(value4);
    }

    @Test
    public void testGetSupportedValuesList_int32Sorted() {
        when(mVehicleHal.isSupportedValuesImplemented(
                newPropIdAreaId(INFO_FUEL_DOOR_LOCATION, 0))).thenReturn(true);
        int areaId = 0;
        int value1 = 1;
        int value2 = 2;
        int value3 = 3;
        int value4 = 4;
        var rawPropValues1 = new RawPropValues();
        rawPropValues1.int32Values = new int[]{value1};
        var rawPropValues2 = new RawPropValues();
        rawPropValues2.int32Values = new int[]{value2};
        var rawPropValues3 = new RawPropValues();
        rawPropValues3.int32Values = new int[]{value3};
        var rawPropValues4 = new RawPropValues();
        rawPropValues4.int32Values = new int[]{value4};

        // Randomize the order.
        when(mVehicleHal.getSupportedValuesList(INFO_FUEL_DOOR_LOCATION, areaId)).thenReturn(
                List.of(rawPropValues1, rawPropValues3, rawPropValues2, rawPropValues4));

        var areaIdConfig = new AreaIdConfig.Builder(INFO_FUEL_DOOR_LOCATION, areaId)
                .setHasSupportedValuesList(true).build();

        var supportedValuesList = mPropertyHalService.getSupportedValuesList(
                INFO_FUEL_DOOR_LOCATION, areaId, areaIdConfig);

        assertThat(supportedValuesList).hasSize(4);
        expectThat(supportedValuesList.get(0).getTypedValue()).isEqualTo(value1);
        expectThat(supportedValuesList.get(1).getTypedValue()).isEqualTo(value2);
        expectThat(supportedValuesList.get(2).getTypedValue()).isEqualTo(value3);
        expectThat(supportedValuesList.get(3).getTypedValue()).isEqualTo(value4);
    }

    @Test
    public void testGetSupportedValuesList_int64Sorted() {
        when(mVehicleHal.isSupportedValuesImplemented(
                newPropIdAreaId(EPOCH_TIME, 0))).thenReturn(true);
        int areaId = 0;
        long value1 = 1L;
        long value2 = 2L;
        long value3 = 3L;
        long value4 = 4L;
        var rawPropValues1 = new RawPropValues();
        rawPropValues1.int64Values = new long[]{value1};
        var rawPropValues2 = new RawPropValues();
        rawPropValues2.int64Values = new long[]{value2};
        var rawPropValues3 = new RawPropValues();
        rawPropValues3.int64Values = new long[]{value3};
        var rawPropValues4 = new RawPropValues();
        rawPropValues4.int64Values = new long[]{value4};

        // Randomize the order.
        when(mVehicleHal.getSupportedValuesList(EPOCH_TIME, areaId)).thenReturn(
                List.of(rawPropValues1, rawPropValues3, rawPropValues2, rawPropValues4));

        var areaIdConfig = new AreaIdConfig.Builder(EPOCH_TIME, areaId)
                .setHasSupportedValuesList(true).build();

        var supportedValuesList = mPropertyHalService.getSupportedValuesList(
                EPOCH_TIME, areaId, areaIdConfig);

        assertThat(supportedValuesList).hasSize(4);
        expectThat(supportedValuesList.get(0).getTypedValue()).isEqualTo(value1);
        expectThat(supportedValuesList.get(1).getTypedValue()).isEqualTo(value2);
        expectThat(supportedValuesList.get(2).getTypedValue()).isEqualTo(value3);
        expectThat(supportedValuesList.get(3).getTypedValue()).isEqualTo(value4);
    }

    @Test
    public void testGetSupportedValuesList_ignoreInvalidProp() {
        when(mVehicleHal.isSupportedValuesImplemented(
                newPropIdAreaId(PERF_VEHICLE_SPEED, 0))).thenReturn(true);
        int areaId = 0;
        float value2 = 2f;
        float value3 = 3f;
        float value4 = 4f;
        // rawPropValues1 does not contain a value.
        var rawPropValues1 = new RawPropValues();
        var rawPropValues2 = new RawPropValues();
        rawPropValues2.floatValues = new float[]{value2};
        var rawPropValues3 = new RawPropValues();
        rawPropValues3.floatValues = new float[]{value3};
        var rawPropValues4 = new RawPropValues();
        rawPropValues4.floatValues = new float[]{value4};

        // Randomize the order.
        when(mVehicleHal.getSupportedValuesList(PERF_VEHICLE_SPEED, areaId)).thenReturn(
                List.of(rawPropValues1, rawPropValues3, rawPropValues2, rawPropValues4));

        var areaIdConfig = new AreaIdConfig.Builder(PERF_VEHICLE_SPEED, areaId)
                .setHasSupportedValuesList(true).build();

        var supportedValuesList = mPropertyHalService.getSupportedValuesList(
                PERF_VEHICLE_SPEED, areaId, areaIdConfig);

        assertThat(supportedValuesList).hasSize(3);
        expectThat(supportedValuesList.get(0).getTypedValue()).isEqualTo(value2);
        expectThat(supportedValuesList.get(1).getTypedValue()).isEqualTo(value3);
        expectThat(supportedValuesList.get(2).getTypedValue()).isEqualTo(value4);
    }

    @Test
    public void testGetSupportedValuesList_returnNull() {
        when(mVehicleHal.isSupportedValuesImplemented(
                newPropIdAreaId(PERF_VEHICLE_SPEED, 0))).thenReturn(true);
        int areaId = 0;

        when(mVehicleHal.getSupportedValuesList(PERF_VEHICLE_SPEED, areaId)).thenReturn(null);

        var areaIdConfig = new AreaIdConfig.Builder(PERF_VEHICLE_SPEED, areaId).build();

        var supportedValuesList = mPropertyHalService.getSupportedValuesList(
                PERF_VEHICLE_SPEED, areaId, areaIdConfig);

        assertThat(supportedValuesList).isNull();
    }

    @Test
    public void testGetSupportedValuesList_halPropConfigNotFound() {
        when(mVehicleHal.isSupportedValuesImplemented(any())).thenReturn(true);
        int areaId = 0;
        int mgrPropId = -1;
        var areaIdConfig = new AreaIdConfig.Builder(mgrPropId, areaId).build();

        // In fact, CarPropertyService verifies the property ID before calling
        // PropertyHalService.GetSupportedValuesList, so this should never happen.
        assertThrows(IllegalArgumentException.class, () -> {
            mPropertyHalService.getSupportedValuesList(mgrPropId, areaId, areaIdConfig);
        });
    }

    // Verifies that properties that has annotation: legacy_supported_values_in_config gets
    // supported values from config.
    @Test
    public void testGetSupportedValuesList_withAnnotationfromConfig() {
        int mgrPropId = EV_CHARGE_PERCENT_LIMIT;
        // This property does not implement the new APIs so it falls back to config array.
        when(mVehicleHal.isSupportedValuesImplemented(newPropIdAreaId(mgrPropId, 0)))
                .thenReturn(false);
        int areaId = 0;

        var vehiclePropConfig = new VehiclePropConfig();
        vehiclePropConfig.prop = VehicleProperty.EV_CHARGE_PERCENT_LIMIT;
        vehiclePropConfig.access = VehiclePropertyAccess.READ;
        vehiclePropConfig.changeMode = VehiclePropertyChangeMode.ON_CHANGE;
        vehiclePropConfig.configArray = new int[]{1, 2, 3};
        HalPropConfig halPropConfig = new AidlHalPropConfig(vehiclePropConfig);
        CarPropertyConfig carPropertyConfig = halPropConfig.toCarPropertyConfig(
                mgrPropId, PropertyHalServiceConfigs.newConfigs(), /* isVhalPropId= */ false);

        mPropertyHalService.takeProperties(List.of(halPropConfig));

        var areaIdConfig = carPropertyConfig.getAreaIdConfig(areaId);

        expectThat(areaIdConfig.hasSupportedValuesList()).isTrue();

        var supportedValuesList = mPropertyHalService.getSupportedValuesList(
                mgrPropId, areaId, areaIdConfig);

        assertThat(supportedValuesList).hasSize(3);
        expectThat(supportedValuesList.get(0).getTypedValue()).isEqualTo(1.f);
        expectThat(supportedValuesList.get(1).getTypedValue()).isEqualTo(2.f);
        expectThat(supportedValuesList.get(2).getTypedValue()).isEqualTo(3.f);
    }

    @Test
    public void testGetSupportedValuesList_withAnnotationfromConfig_noConfig() {
        int mgrPropId = EV_CHARGE_PERCENT_LIMIT;
        // This property does not implement the new APIs so it falls back to config array.
        when(mVehicleHal.isSupportedValuesImplemented(newPropIdAreaId(mgrPropId, 0)))
                .thenReturn(false);
        int areaId = 0;

        var vehiclePropConfig = new VehiclePropConfig();
        vehiclePropConfig.prop = VehicleProperty.EV_CHARGE_PERCENT_LIMIT;
        vehiclePropConfig.access = VehiclePropertyAccess.READ;
        vehiclePropConfig.changeMode = VehiclePropertyChangeMode.ON_CHANGE;
        // No element in config array.
        vehiclePropConfig.configArray = new int[]{};
        HalPropConfig halPropConfig = new AidlHalPropConfig(vehiclePropConfig);
        CarPropertyConfig carPropertyConfig = halPropConfig.toCarPropertyConfig(
                mgrPropId, PropertyHalServiceConfigs.newConfigs(), /* isVhalPropId= */ false);

        mPropertyHalService.takeProperties(List.of(halPropConfig));

        var areaIdConfig = carPropertyConfig.getAreaIdConfig(areaId);

        expectThat(areaIdConfig.hasSupportedValuesList()).isFalse();

        var supportedValuesList = mPropertyHalService.getSupportedValuesList(
                mgrPropId, areaId, areaIdConfig);

        assertThat(supportedValuesList).isNull();
    }

    // Verifies that the supported values for HVAC_FAN_DIRECTION comes from
    // HVAC_FAN_DIRECTION_AVAILABLE
    @Test
    public void testGetSupportedValuesList_HVAC_FAN_DIRECTION() {
        int mgrPropId = HVAC_FAN_DIRECTION;
        int areaId = VehicleAreaSeat.ROW_1_LEFT;
        // This property does not implement the new APIs.
        when(mVehicleHal.isSupportedValuesImplemented(newPropIdAreaId(mgrPropId, areaId)))
                .thenReturn(false);

        var vehiclePropConfig = new VehiclePropConfig();
        vehiclePropConfig.prop = VehicleProperty.HVAC_FAN_DIRECTION;
        vehiclePropConfig.access = VehiclePropertyAccess.READ;
        vehiclePropConfig.changeMode = VehiclePropertyChangeMode.ON_CHANGE;
        var vehicleAreaConfig = new VehicleAreaConfig();
        vehicleAreaConfig.areaId = areaId;
        vehicleAreaConfig.access = VehiclePropertyAccess.READ;
        vehiclePropConfig.areaConfigs = new VehicleAreaConfig[] {vehicleAreaConfig};
        HalPropConfig halPropConfig = new AidlHalPropConfig(vehiclePropConfig);
        CarPropertyConfig carPropertyConfig = halPropConfig.toCarPropertyConfig(
                mgrPropId, PropertyHalServiceConfigs.newConfigs(), /* isVhalPropId= */ false);

        var hvacFanDirectionAvailablePropConfig = new VehiclePropConfig();
        hvacFanDirectionAvailablePropConfig.prop = VehicleProperty.HVAC_FAN_DIRECTION_AVAILABLE;
        hvacFanDirectionAvailablePropConfig.access = VehiclePropertyAccess.READ;
        hvacFanDirectionAvailablePropConfig.changeMode = VehiclePropertyChangeMode.ON_CHANGE;
        hvacFanDirectionAvailablePropConfig.areaConfigs = new VehicleAreaConfig[] {
                vehicleAreaConfig};
        HalPropConfig hvacFanDirectionAvailableHalPropConfig = new AidlHalPropConfig(
                hvacFanDirectionAvailablePropConfig);

        mPropertyHalService.takeProperties(List.of(halPropConfig,
                hvacFanDirectionAvailableHalPropConfig));

        when(mVehicleHal.get(VehicleProperty.HVAC_FAN_DIRECTION_AVAILABLE, areaId))
                .thenReturn(mPropValueBuilder.build(
                        VehicleProperty.HVAC_FAN_DIRECTION_AVAILABLE, areaId,
                        TEST_UPDATE_TIMESTAMP_NANOS, /* status= */ 0, new int[]{1, 2, 3}));

        var areaIdConfig = carPropertyConfig.getAreaIdConfig(areaId);

        var supportedValuesList = mPropertyHalService.getSupportedValuesList(
                mgrPropId, areaId, areaIdConfig);

        assertThat(supportedValuesList).hasSize(3);
        expectThat(supportedValuesList.get(0).getTypedValue()).isEqualTo(1);
        expectThat(supportedValuesList.get(1).getTypedValue()).isEqualTo(2);
        expectThat(supportedValuesList.get(2).getTypedValue()).isEqualTo(3);
    }

    @Test
    public void testGetSupportedValuesList_HVAC_FAN_DIRECTION_getHvacFanDirectAvailableFailed() {
        int mgrPropId = HVAC_FAN_DIRECTION;
        int areaId = VehicleAreaSeat.ROW_1_LEFT;
        // This property does not implement the new APIs.
        when(mVehicleHal.isSupportedValuesImplemented(newPropIdAreaId(mgrPropId, areaId)))
                .thenReturn(false);

        var vehiclePropConfig = new VehiclePropConfig();
        vehiclePropConfig.prop = VehicleProperty.HVAC_FAN_DIRECTION;
        vehiclePropConfig.access = VehiclePropertyAccess.READ;
        vehiclePropConfig.changeMode = VehiclePropertyChangeMode.ON_CHANGE;
        var vehicleAreaConfig = new VehicleAreaConfig();
        vehicleAreaConfig.areaId = areaId;
        vehicleAreaConfig.access = VehiclePropertyAccess.READ;
        vehiclePropConfig.areaConfigs = new VehicleAreaConfig[] {vehicleAreaConfig};
        HalPropConfig halPropConfig = new AidlHalPropConfig(vehiclePropConfig);
        CarPropertyConfig carPropertyConfig = halPropConfig.toCarPropertyConfig(
                mgrPropId, PropertyHalServiceConfigs.newConfigs(), /* isVhalPropId= */ false);

        var hvacFanDirectionAvailablePropConfig = new VehiclePropConfig();
        hvacFanDirectionAvailablePropConfig.prop = VehicleProperty.HVAC_FAN_DIRECTION_AVAILABLE;
        hvacFanDirectionAvailablePropConfig.access = VehiclePropertyAccess.READ;
        hvacFanDirectionAvailablePropConfig.changeMode = VehiclePropertyChangeMode.ON_CHANGE;
        hvacFanDirectionAvailablePropConfig.areaConfigs = new VehicleAreaConfig[] {
                vehicleAreaConfig};
        HalPropConfig hvacFanDirectionAvailableHalPropConfig = new AidlHalPropConfig(
                hvacFanDirectionAvailablePropConfig);

        mPropertyHalService.takeProperties(List.of(halPropConfig,
                hvacFanDirectionAvailableHalPropConfig));

        // Simulate some error from VHAL while getting the property.
        when(mVehicleHal.get(VehicleProperty.HVAC_FAN_DIRECTION_AVAILABLE, areaId))
                .thenThrow(new ServiceSpecificException(0));

        var areaIdConfig = carPropertyConfig.getAreaIdConfig(areaId);

        var supportedValuesList = mPropertyHalService.getSupportedValuesList(
                mgrPropId, areaId, areaIdConfig);

        assertThat(supportedValuesList).isNull();
    }

    // Verifies that the supported values for HVAC_TEMPERATURE_SET comes from config array.
    @Test
    public void testGetSupportedValuesList_HVAC_TEMPERATURE_SET() {
        int mgrPropId = HVAC_TEMPERATURE_SET;
        int areaId = VehicleAreaSeat.ROW_1_LEFT;
        // This property does not implement the new APIs.
        when(mVehicleHal.isSupportedValuesImplemented(newPropIdAreaId(mgrPropId, areaId)))
                .thenReturn(false);

        var vehiclePropConfig = new VehiclePropConfig();
        vehiclePropConfig.prop = VehicleProperty.HVAC_TEMPERATURE_SET;
        vehiclePropConfig.access = VehiclePropertyAccess.READ;
        vehiclePropConfig.changeMode = VehiclePropertyChangeMode.ON_CHANGE;
        vehiclePropConfig.configArray = new int[]{100, 110, 5, 20, 30, 5};
        var vehicleAreaConfig = new VehicleAreaConfig();
        vehicleAreaConfig.areaId = areaId;
        vehicleAreaConfig.access = VehiclePropertyAccess.READ;
        vehiclePropConfig.areaConfigs = new VehicleAreaConfig[] {vehicleAreaConfig};
        HalPropConfig halPropConfig = new AidlHalPropConfig(vehiclePropConfig);
        CarPropertyConfig carPropertyConfig = halPropConfig.toCarPropertyConfig(
                mgrPropId, PropertyHalServiceConfigs.newConfigs(), /* isVhalPropId= */ false);

        mPropertyHalService.takeProperties(List.of(halPropConfig));

        var areaIdConfig = carPropertyConfig.getAreaIdConfig(areaId);

        expectThat(areaIdConfig.hasSupportedValuesList()).isTrue();

        var supportedValuesList = mPropertyHalService.getSupportedValuesList(
                mgrPropId, areaId, areaIdConfig);

        assertThat(supportedValuesList).hasSize(3);
        expectThat(supportedValuesList.get(0).getTypedValue()).isEqualTo(10.f);
        expectThat(supportedValuesList.get(1).getTypedValue()).isEqualTo(10.5f);
        expectThat(supportedValuesList.get(2).getTypedValue()).isEqualTo(11.f);
    }

    @Test
    public void testGetSupportedValuesList_HVAC_TEMPERATURE_SET_invalidConfig() {
        int mgrPropId = HVAC_TEMPERATURE_SET;
        int areaId = VehicleAreaSeat.ROW_1_LEFT;
        // This property does not implement the new APIs.
        when(mVehicleHal.isSupportedValuesImplemented(newPropIdAreaId(mgrPropId, areaId)))
                .thenReturn(false);

        var vehiclePropConfig = new VehiclePropConfig();
        vehiclePropConfig.prop = VehicleProperty.HVAC_TEMPERATURE_SET;
        vehiclePropConfig.access = VehiclePropertyAccess.READ;
        vehiclePropConfig.changeMode = VehiclePropertyChangeMode.ON_CHANGE;
        // Invalid config, expect 6 elements.
        vehiclePropConfig.configArray = new int[]{100, 110, 5, 20, 30};
        var vehicleAreaConfig = new VehicleAreaConfig();
        vehicleAreaConfig.areaId = areaId;
        vehicleAreaConfig.access = VehiclePropertyAccess.READ;
        vehiclePropConfig.areaConfigs = new VehicleAreaConfig[] {vehicleAreaConfig};
        HalPropConfig halPropConfig = new AidlHalPropConfig(vehiclePropConfig);
        CarPropertyConfig carPropertyConfig = halPropConfig.toCarPropertyConfig(
                mgrPropId, PropertyHalServiceConfigs.newConfigs(), /* isVhalPropId= */ false);

        mPropertyHalService.takeProperties(List.of(halPropConfig));

        var areaIdConfig = carPropertyConfig.getAreaIdConfig(areaId);

        expectThat(areaIdConfig.hasSupportedValuesList()).isFalse();

        var supportedValuesList = mPropertyHalService.getSupportedValuesList(
                mgrPropId, areaId, areaIdConfig);

        assertThat(supportedValuesList).isNull();
    }

    @Test
    public void testRegisterSupportedValuesChangeCallback() throws Exception {
        when(mVehicleHal.isSupportedValuesImplemented(any())).thenReturn(true);
        var propIdAreaId1 = newPropIdAreaId(HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);
        var propIdAreaId2 = newPropIdAreaId(PERF_VEHICLE_SPEED, 0);
        var propIdAreaId3 = newPropIdAreaId(VEHICLE_SPEED_DISPLAY_UNITS, 0);

        mPropertyHalService.registerSupportedValuesChangeCallback(
                List.of(propIdAreaId1, propIdAreaId2, propIdAreaId3),
                mSupportedValuesChangeCallback);

        ArgumentCaptor<List> propIdAreaIdsCaptor = ArgumentCaptor.forClass(List.class);
        verify(mVehicleHal).registerSupportedValuesChange(any(), propIdAreaIdsCaptor.capture());

        var requestedPropIdAreaIds = (List<PropIdAreaId>) propIdAreaIdsCaptor.getValue();
        assertThat(requestedPropIdAreaIds).hasSize(3);
        expectThat(requestedPropIdAreaIds.get(0).propId).isEqualTo(VehicleProperty.HVAC_FAN_SPEED);
        expectThat(requestedPropIdAreaIds.get(0).areaId).isEqualTo(
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);
        expectThat(requestedPropIdAreaIds.get(1).propId).isEqualTo(
                VehicleProperty.PERF_VEHICLE_SPEED);
        expectThat(requestedPropIdAreaIds.get(1).areaId).isEqualTo(0);
        expectThat(requestedPropIdAreaIds.get(2).propId).isEqualTo(
                VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS);
        expectThat(requestedPropIdAreaIds.get(2).areaId).isEqualTo(0);
    }

    @Test
    public void testRegisterSupportedValuesChangeCallback_ignoreLegacyPropIdAreaIds()
            throws Exception {
        var propIdAreaId1 = newPropIdAreaId(HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);
        var propIdAreaId2 = newPropIdAreaId(PERF_VEHICLE_SPEED, 0);
        var propIdAreaId3 = newPropIdAreaId(VEHICLE_SPEED_DISPLAY_UNITS, 0);
        var halPropIdAreaId1 = newPropIdAreaId(VehicleProperty.HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);
        var halPropIdAreaId2 = newPropIdAreaId(VehicleProperty.PERF_VEHICLE_SPEED, 0);
        var halPropIdAreaId3 = newPropIdAreaId(VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS, 0);
        when(mVehicleHal.isSupportedValuesImplemented(halPropIdAreaId1)).thenReturn(true);
        when(mVehicleHal.isSupportedValuesImplemented(halPropIdAreaId2)).thenReturn(false);
        when(mVehicleHal.isSupportedValuesImplemented(halPropIdAreaId3)).thenReturn(true);

        mPropertyHalService.registerSupportedValuesChangeCallback(
                List.of(propIdAreaId1, propIdAreaId2, propIdAreaId3),
                mSupportedValuesChangeCallback);

        assertThat(mPropertyHalService.countSupportedValuesChangeClient()).isEqualTo(1);
        ArgumentCaptor<List> propIdAreaIdsCaptor = ArgumentCaptor.forClass(List.class);
        verify(mVehicleHal).registerSupportedValuesChange(any(), propIdAreaIdsCaptor.capture());

        var requestedPropIdAreaIds = (List<PropIdAreaId>) propIdAreaIdsCaptor.getValue();
        assertThat(requestedPropIdAreaIds).hasSize(2);
        expectThat(requestedPropIdAreaIds.get(0).propId).isEqualTo(VehicleProperty.HVAC_FAN_SPEED);
        expectThat(requestedPropIdAreaIds.get(0).areaId).isEqualTo(
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);
        expectThat(requestedPropIdAreaIds.get(1).propId).isEqualTo(
                VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS);
        expectThat(requestedPropIdAreaIds.get(1).areaId).isEqualTo(0);

        mPropertyHalService.unregisterSupportedValuesChangeCallback(
                List.of(propIdAreaId1, propIdAreaId2, propIdAreaId3),
                mSupportedValuesChangeCallback);

        assertThat(mPropertyHalService.countSupportedValuesChangeClient()).isEqualTo(0);
    }

    @Test
    public void testRegisterSupportedValuesChangeCallback_ignoreRegisteredPropIdAreaId()
            throws Exception {
        when(mVehicleHal.isSupportedValuesImplemented(any())).thenReturn(true);
        var propIdAreaId1 = newPropIdAreaId(HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);
        var propIdAreaId2 = newPropIdAreaId(PERF_VEHICLE_SPEED, 0);
        var propIdAreaId3 = newPropIdAreaId(VEHICLE_SPEED_DISPLAY_UNITS, 0);

        mPropertyHalService.registerSupportedValuesChangeCallback(
                List.of(propIdAreaId1, propIdAreaId2),
                mSupportedValuesChangeCallback);

        clearInvocations(mVehicleHal);

        // propIdAreaId1 is already registered before, must ignore.
        mPropertyHalService.registerSupportedValuesChangeCallback(
                List.of(propIdAreaId1, propIdAreaId3),
                mSupportedValuesChangeCallback);

        ArgumentCaptor<List> propIdAreaIdsCaptor = ArgumentCaptor.forClass(List.class);
        verify(mVehicleHal).registerSupportedValuesChange(any(), propIdAreaIdsCaptor.capture());

        var requestedPropIdAreaIds = (List<PropIdAreaId>) propIdAreaIdsCaptor.getValue();
        assertThat(requestedPropIdAreaIds).hasSize(1);
        expectThat(requestedPropIdAreaIds.get(0).propId).isEqualTo(
                VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS);
        expectThat(requestedPropIdAreaIds.get(0).areaId).isEqualTo(0);
    }

    @Test
    public void testRegisterSupportedValuesChangeCallback_binderAlreadyDead()
            throws Exception {
        when(mVehicleHal.isSupportedValuesImplemented(any())).thenReturn(true);
        var propIdAreaId = newPropIdAreaId(HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);
        doThrow(new RemoteException()).when(mSupportedValuesChangeCallbackBinder)
                .linkToDeath(any(), eq(0));

        assertThrows(IllegalStateException.class, () ->
                mPropertyHalService.registerSupportedValuesChangeCallback(
                        List.of(propIdAreaId), mSupportedValuesChangeCallback));
    }

    @Test
    public void testRegisterSupportedValuesChangeCallback_errorFromVhal() throws Exception {
        when(mVehicleHal.isSupportedValuesImplemented(any())).thenReturn(true);
        var propIdAreaId = newPropIdAreaId(HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);
        doThrow(new ServiceSpecificException(0)).when(mVehicleHal)
                .registerSupportedValuesChange(any(), any());

        assertThrows(ServiceSpecificException.class, () ->
                mPropertyHalService.registerSupportedValuesChangeCallback(
                        List.of(propIdAreaId), mSupportedValuesChangeCallback));

        // Simulate error goes away.
        doNothing().when(mVehicleHal).registerSupportedValuesChange(any(), any());

        mPropertyHalService.registerSupportedValuesChangeCallback(
                List.of(propIdAreaId), mSupportedValuesChangeCallback);

        verify(mVehicleHal, times(2)).registerSupportedValuesChange(any(), any());
    }

    @Test
    public void testRegisterSupportedValuesChangeCallback_binderDeath() throws Exception {
        when(mVehicleHal.isSupportedValuesImplemented(any())).thenReturn(true);
        var propIdAreaId = newPropIdAreaId(VEHICLE_SPEED_DISPLAY_UNITS, 0);

        mPropertyHalService.registerSupportedValuesChangeCallback(
                List.of(propIdAreaId), mSupportedValuesChangeCallback);

        verify(mSupportedValuesChangeCallbackBinder).linkToDeath(mDeathRecipientCaptor.capture(),
                eq(0));

        var binderDeathRecipient = mDeathRecipientCaptor.getValue();
        binderDeathRecipient.binderDied();

        verify(mVehicleHal).unregisterSupportedValuesChange(eq(mPropertyHalService),
                mListArgumentCaptor.capture());

        var updatedPropIdAreaIds = (List<PropIdAreaId>) mListArgumentCaptor.getValue();

        assertThat(updatedPropIdAreaIds).hasSize(1);
        // Must be converted from CarPropertyManager property ID to VHAL property ID.
        assertThat(updatedPropIdAreaIds.get(0)).isEqualTo(newPropIdAreaId(
                VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS, 0));
    }

    @Test
    public void testRegisterSupportedValuesChangeCallback_supportedValuesNotImplemented()
            throws Exception {
        when(mVehicleHal.isSupportedValuesImplemented(any())).thenReturn(false);
        var propIdAreaId = newPropIdAreaId(HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);

        mPropertyHalService.registerSupportedValuesChangeCallback(List.of(propIdAreaId),
                mSupportedValuesChangeCallback);

        verify(mVehicleHal, never()).registerSupportedValuesChange(any(), any());
    }

    @Test
    public void testRegisterSupportedValuesChangeCallback_twoClients()
            throws Exception {
        var propIdAreaId1 = newPropIdAreaId(HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);
        var propIdAreaId2 = newPropIdAreaId(VEHICLE_SPEED_DISPLAY_UNITS, 0);
        var halPropIdAreaId1 = newPropIdAreaId(VehicleProperty.HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);
        var halPropIdAreaId2 = newPropIdAreaId(VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS, 0);
        when(mVehicleHal.isSupportedValuesImplemented(halPropIdAreaId1)).thenReturn(true);
        when(mVehicleHal.isSupportedValuesImplemented(halPropIdAreaId2)).thenReturn(true);

        // We use two binders to simulate two different clients.
        ISupportedValuesChangeCallback callback1 = mock(ISupportedValuesChangeCallback.class);
        IBinder binder1 = mock(IBinder.class);
        ISupportedValuesChangeCallback callback2 = mock(ISupportedValuesChangeCallback.class);
        IBinder binder2 = mock(IBinder.class);
        when(callback1.asBinder()).thenReturn(binder1);
        when(callback2.asBinder()).thenReturn(binder2);

        mPropertyHalService.registerSupportedValuesChangeCallback(
                List.of(propIdAreaId1, propIdAreaId2), callback1);
        mPropertyHalService.registerSupportedValuesChangeCallback(
                List.of(propIdAreaId1), callback2);

        mPropertyHalService.unregisterSupportedValuesChangeCallback(
                List.of(propIdAreaId1), callback1);

        // propIdAreaId1 is still subscribed by callback2
        verify(mVehicleHal, never()).unregisterSupportedValuesChange(any(), any());

        // Trigger an on-change event to verify callback2 can still receive event.
        mPropertyHalService.onSupportedValuesChange(List.of(halPropIdAreaId1, halPropIdAreaId2));

        verify(callback2).onSupportedValuesChange(mListArgumentCaptor.capture());
        var updatedPropIdAreaIds = (List<PropIdAreaId>) mListArgumentCaptor.getValue();
        assertThat(updatedPropIdAreaIds).hasSize(1);
        expectThat(updatedPropIdAreaIds.get(0)).isEqualTo(propIdAreaId1);

        verify(callback1).onSupportedValuesChange(mListArgumentCaptor.capture());
        updatedPropIdAreaIds = (List<PropIdAreaId>) mListArgumentCaptor.getValue();
        assertThat(updatedPropIdAreaIds).hasSize(1);
        expectThat(updatedPropIdAreaIds.get(0)).isEqualTo(propIdAreaId2);

        // Unregister propIdAreaId1 for callback2 as well, after this, there is no client for
        // propIdAreaId1, so it should be unsubscribed from VehicleHal.
        mPropertyHalService.unregisterSupportedValuesChangeCallback(
                List.of(propIdAreaId1), callback2);

        verify(mVehicleHal).unregisterSupportedValuesChange(any(), mListArgumentCaptor.capture());
        updatedPropIdAreaIds = (List<PropIdAreaId>) mListArgumentCaptor.getValue();
        assertThat(updatedPropIdAreaIds).hasSize(1);
        // Must be converted from CarPropertyManager property ID to VHAL property ID.
        assertThat(updatedPropIdAreaIds.get(0)).isEqualTo(halPropIdAreaId1);

        clearInvocations(mVehicleHal);

        // Unregister propIdAreaId2 for callback1, after this, there is no client for propIdAreaId2.
        mPropertyHalService.unregisterSupportedValuesChangeCallback(
                List.of(propIdAreaId2), callback1);

        verify(mVehicleHal).unregisterSupportedValuesChange(any(), mListArgumentCaptor.capture());
        updatedPropIdAreaIds = (List<PropIdAreaId>) mListArgumentCaptor.getValue();
        assertThat(updatedPropIdAreaIds).hasSize(1);
        // Must be converted from CarPropertyManager property ID to VHAL property ID.
        assertThat(updatedPropIdAreaIds.get(0)).isEqualTo(halPropIdAreaId2);

        // Trigger an on-change event to verify both callback1 and callback2 cannot receive the
        // event.
        clearInvocations(callback1);
        clearInvocations(callback2);

        mPropertyHalService.onSupportedValuesChange(List.of(halPropIdAreaId1, halPropIdAreaId2));

        verify(callback1, never()).onSupportedValuesChange(any());
        verify(callback2, never()).onSupportedValuesChange(any());
    }

    @Test
    public void testRegisterSupportedValuesChangeCallback_sameClientDifferentCallback()
            throws Exception {
        // We should uniquely identify a client via IBinder, not the Callback interface
        // object.
        var propIdAreaId1 = newPropIdAreaId(HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);
        var propIdAreaId2 = newPropIdAreaId(VEHICLE_SPEED_DISPLAY_UNITS, 0);
        var halPropIdAreaId1 = newPropIdAreaId(VehicleProperty.HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);
        var halPropIdAreaId2 = newPropIdAreaId(VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS, 0);
        when(mVehicleHal.isSupportedValuesImplemented(halPropIdAreaId1)).thenReturn(true);
        when(mVehicleHal.isSupportedValuesImplemented(halPropIdAreaId2)).thenReturn(true);

        ISupportedValuesChangeCallback callback1 = mock(ISupportedValuesChangeCallback.class);
        ISupportedValuesChangeCallback callback2 = mock(ISupportedValuesChangeCallback.class);
        IBinder binder = mock(IBinder.class);

        // callback1 and callback2 is the same client.
        when(callback1.asBinder()).thenReturn(binder);
        when(callback2.asBinder()).thenReturn(binder);

        mPropertyHalService.registerSupportedValuesChangeCallback(
                List.of(propIdAreaId1, propIdAreaId2), callback1);
        mPropertyHalService.unregisterSupportedValuesChangeCallback(
                List.of(propIdAreaId1, propIdAreaId2), callback2);

        verify(mVehicleHal).unregisterSupportedValuesChange(any(), mListArgumentCaptor.capture());
        var updatedPropIdAreaIds = (List<PropIdAreaId>) mListArgumentCaptor.getValue();
        assertThat(updatedPropIdAreaIds).hasSize(2);
    }

    @Test
    public void testOnSupportedValuesChange() throws Exception {
        when(mVehicleHal.isSupportedValuesImplemented(any())).thenReturn(true);
        var propIdAreaId1 = newPropIdAreaId(HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);
        var propIdAreaId2 = newPropIdAreaId(PERF_VEHICLE_SPEED, 0);
        var propIdAreaId3 = newPropIdAreaId(VEHICLE_SPEED_DISPLAY_UNITS, 0);

        mPropertyHalService.registerSupportedValuesChangeCallback(
                List.of(propIdAreaId1, propIdAreaId2, propIdAreaId3),
                mSupportedValuesChangeCallback);

        var halPropIdAreaId1 = newPropIdAreaId(
                VehicleProperty.HVAC_FAN_SPEED, VehicleAreaSeat.ROW_1_LEFT);
        var halPropIdAreaId3 = newPropIdAreaId(VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS, 0);

        mPropertyHalService.onSupportedValuesChange(List.of(halPropIdAreaId1, halPropIdAreaId3));

        verify(mSupportedValuesChangeCallback).onSupportedValuesChange(
                mListArgumentCaptor.capture());

        var updatedPropIdAreaIds = (List<PropIdAreaId>) mListArgumentCaptor.getValue();

        assertThat(updatedPropIdAreaIds).hasSize(2);
        expectThat(updatedPropIdAreaIds.get(0)).isEqualTo(propIdAreaId1);
        expectThat(updatedPropIdAreaIds.get(1)).isEqualTo(propIdAreaId3);
    }

    @Test
    public void testOnSupportedValuesChange_noRegisteredClients() throws Exception {
        when(mVehicleHal.isSupportedValuesImplemented(any())).thenReturn(true);
        var halPropIdAreaId = newPropIdAreaId(VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS, 0);

        mPropertyHalService.onSupportedValuesChange(List.of(halPropIdAreaId));

        verify(mSupportedValuesChangeCallback, never()).onSupportedValuesChange(any());
    }

    @Test
    public void testUnregisterSupportedValuesChangeCallback() throws Exception {
        when(mVehicleHal.isSupportedValuesImplemented(any())).thenReturn(true);
        var propIdAreaId1 = newPropIdAreaId(HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);
        var propIdAreaId2 = newPropIdAreaId(PERF_VEHICLE_SPEED, 0);
        var propIdAreaId3 = newPropIdAreaId(VEHICLE_SPEED_DISPLAY_UNITS, 0);
        var halPropIdAreaId1 = newPropIdAreaId(VehicleProperty.HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);
        var halPropIdAreaId2 = newPropIdAreaId(VehicleProperty.PERF_VEHICLE_SPEED, 0);
        var halPropIdAreaId3 = newPropIdAreaId(VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS, 0);

        mPropertyHalService.registerSupportedValuesChangeCallback(
                List.of(propIdAreaId1, propIdAreaId2, propIdAreaId3),
                mSupportedValuesChangeCallback);

        // Now only propIdAreaId3 is registered.
        mPropertyHalService.unregisterSupportedValuesChangeCallback(
                List.of(propIdAreaId1, propIdAreaId2),
                mSupportedValuesChangeCallback);

        verify(mVehicleHal).unregisterSupportedValuesChange(eq(mPropertyHalService),
                mListArgumentCaptor.capture());
        var updatedPropIdAreaIds = (List<PropIdAreaId>) mListArgumentCaptor.getValue();
        assertThat(updatedPropIdAreaIds).hasSize(2);
        assertThat(updatedPropIdAreaIds.get(0)).isEqualTo(halPropIdAreaId1);
        assertThat(updatedPropIdAreaIds.get(1)).isEqualTo(halPropIdAreaId2);

        mPropertyHalService.onSupportedValuesChange(List.of(halPropIdAreaId1,
                halPropIdAreaId2, halPropIdAreaId3));

        verify(mSupportedValuesChangeCallback).onSupportedValuesChange(
                mListArgumentCaptor.capture());

        updatedPropIdAreaIds = (List<PropIdAreaId>) mListArgumentCaptor.getValue();
        assertThat(updatedPropIdAreaIds).hasSize(1);
        expectThat(updatedPropIdAreaIds.get(0)).isEqualTo(propIdAreaId3);
    }

    @Test
    public void testUnregisterSupportedValuesChangeCallback_ignoreUnregistered() throws Exception {
        when(mVehicleHal.isSupportedValuesImplemented(any())).thenReturn(true);
        var propIdAreaId1 = newPropIdAreaId(HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);

        // This does nothing.
        mPropertyHalService.unregisterSupportedValuesChangeCallback(
                List.of(propIdAreaId1), mSupportedValuesChangeCallback);

        verify(mVehicleHal, never()).unregisterSupportedValuesChange(any(), any());
    }

    @Test
    public void testUnregisterSupportedValuesChangeCallback_supportedValuesNotImplemented()
            throws Exception {
        when(mVehicleHal.isSupportedValuesImplemented(any())).thenReturn(false);
        var propIdAreaId1 = newPropIdAreaId(HVAC_FAN_SPEED,
                android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT);

        // This does nothing.
        mPropertyHalService.unregisterSupportedValuesChangeCallback(
                List.of(propIdAreaId1), mSupportedValuesChangeCallback);

        verify(mVehicleHal, never()).unregisterSupportedValuesChange(any(), any());
    }

    @Test
    public void testRegisterRecordingListener() {
        when(mVehicleHal.registerRecordingListener(mCallback)).thenReturn(List.of(mMockPropConfig1,
                mMockPropConfig2));
        when(mMockPropConfig1.toCarPropertyConfig(eq(VehicleProperty.HVAC_TEMPERATURE_SET),
                any(PropertyHalServiceConfigs.class), eq(true)))
                .thenReturn(mMockCarPropertyConfig1);
        when(mMockPropConfig2.toCarPropertyConfig(eq(VehicleProperty.PERF_VEHICLE_SPEED),
                any(PropertyHalServiceConfigs.class), eq(true)))
                .thenReturn(mMockCarPropertyConfig2);
        List<CarPropertyConfig> configs = mPropertyHalService.registerRecordingListener(mCallback);

        assertWithMessage("Successful register recording listener").that(configs)
                .containsExactlyElementsIn(List.of(mMockCarPropertyConfig1,
                        mMockCarPropertyConfig2));
    }

    @Test
    public void testIsRecordingVehicleProperties() {
        when(mVehicleHal.isRecordingVehicleProperties()).thenReturn(true);

        assertWithMessage("Vehicle hal recording").that(
                mPropertyHalService.isRecordingVehicleProperties()).isTrue();
    }

    @Test
    public void testIsRecordingVehicleProperties_false() {
        when(mVehicleHal.isRecordingVehicleProperties()).thenReturn(false);

        assertWithMessage("Vehicle hal recording").that(
                mPropertyHalService.isRecordingVehicleProperties()).isFalse();
    }

    @Test
    public void testStopRecordingVehicleProperties() {
        mPropertyHalService.stopRecordingVehicleProperties(mCallback);

        verify(mVehicleHal).stopRecordingVehicleProperties(eq(mCallback));
    }

    @Test
    public void testEnableInjectionModeReturnsValue() {
        when(mVehicleHal.enableInjectionMode(anyList())).thenReturn(5000L);

        assertThat(mPropertyHalService.enableInjectionMode(List.of())).isEqualTo(5000L);
    }

    @Test
    public void testEnableInjectionMode() {
        List<Integer> list = List.of(52);
        mPropertyHalService.enableInjectionMode(list);

        verify(mVehicleHal).enableInjectionMode(list);
    }

    @Test
    public void testDisableInjectionMode() {
        mPropertyHalService.disableInjectionMode();

        verify(mVehicleHal).disableInjectionMode();
    }

    @Test
    public void testIsVehiclePropertyInjectionModeEnabled() {
        mPropertyHalService.isVehiclePropertyInjectionModeEnabled();

        verify(mVehicleHal).isVehiclePropertyInjectionModeEnabled();
    }

    @Test
    public void testIsVehiclePropertyInjectionModeEnabledReturnsValue() {
        when(mVehicleHal.isVehiclePropertyInjectionModeEnabled()).thenReturn(true);

        assertThat(mPropertyHalService.isVehiclePropertyInjectionModeEnabled()).isTrue();
    }

    @Test
    public void testGetLastInjectedVehicleProperty() {
        CarPropertyValue carPropertyValue = new CarPropertyValue(52, 0, 0);
        when(mVehicleHal.getLastInjectedVehicleProperty(52)).thenReturn(carPropertyValue);

        assertWithMessage("Get last injected vehicle property")
                .that(mPropertyHalService.getLastInjectedVehicleProperty(52))
                .isEqualTo(carPropertyValue);
    }

    @Test
    @EnableFlags(FLAG_PROPERTY_VALUE_USE_DIRECT_EXECUTOR)
    public void testSetDirectExecutor() {
        clearInvocations(mVehicleHal);
        var propertyHalService = new PropertyHalService(mVehicleHal);
        propertyHalService.setPropertyHalServiceConfigs(PropertyHalServiceConfigs.newConfigs());

        propertyHalService.init();

        verify(mVehicleHal).setCallbackExecutor(eq(propertyHalService), any());
        propertyHalService.destroy();
    }

    /** Creates a {@code CarSubscription} with Vur off. */
    @VisibleForTesting
    public static CarSubscription createCarSubscriptionOption(int propertyId,
            int[] areaId, float updateRateHz) {
        return createCarSubscriptionOption(propertyId, areaId, updateRateHz,
                /* enableVur= */ false, /*resolution*/ 0.0f);
    }

    /** Creates a {@code CarSubscription}. */
    @VisibleForTesting
    public static CarSubscription createCarSubscriptionOption(int propertyId,
            int[] areaId, float updateRateHz, boolean enableVur) {
        return createCarSubscriptionOption(propertyId, areaId, updateRateHz,
                enableVur, /*resolution*/ 0.0f);
    }

    /** Creates a {@code CarSubscription}. */
    @VisibleForTesting
    public static CarSubscription createCarSubscriptionOption(int propertyId,
            int[] areaId, float updateRateHz, boolean enableVur, float resolution) {
        CarSubscription options = new CarSubscription();
        options.propertyId = propertyId;
        options.areaIds = areaId;
        options.updateRateHz = updateRateHz;
        options.enableVariableUpdateRate = enableVur;
        options.resolution = resolution;
        return options;
    }

    private static HalSubscribeOptions hvacHalSubscribeOption() {
        return new HalSubscribeOptions(HVAC_TEMPERATURE_SET, new int[]{0}, 0.0f,
                /* enableVariableUpdateRate= */ false);
    }

    private static HalSubscribeOptions speedHalSubscribeOption(float speed) {
        return new HalSubscribeOptions(
                PERF_VEHICLE_SPEED, new int[]{0}, speed, /* enableVariableUpdateRate= */ true);
    }
}
