/*
 * Copyright (C) 2018 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.VehiclePropertyIds.HVAC_FAN_DIRECTION_AVAILABLE;
import static android.car.hardware.CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_STATIC;
import static android.car.hardware.CarPropertyValue.STATUS_AVAILABLE;
import static android.car.hardware.property.CarPropertyManager.CAR_SET_PROPERTY_ERROR_CODE_ACCESS_DENIED;
import static android.car.hardware.property.CarPropertyManager.CAR_SET_PROPERTY_ERROR_CODE_INVALID_ARG;
import static android.car.hardware.property.CarPropertyManager.CAR_SET_PROPERTY_ERROR_CODE_PROPERTY_NOT_AVAILABLE;
import static android.car.hardware.property.CarPropertyManager.CAR_SET_PROPERTY_ERROR_CODE_TRY_AGAIN;
import static android.car.hardware.property.CarPropertyManager.CAR_SET_PROPERTY_ERROR_CODE_UNKNOWN;
import static android.car.hardware.property.VehicleHalStatusCode.STATUS_ACCESS_DENIED;
import static android.car.hardware.property.VehicleHalStatusCode.STATUS_INTERNAL_ERROR;
import static android.car.hardware.property.VehicleHalStatusCode.STATUS_INVALID_ARG;
import static android.car.hardware.property.VehicleHalStatusCode.STATUS_NOT_AVAILABLE;
import static android.car.hardware.property.VehicleHalStatusCode.STATUS_NOT_AVAILABLE_DISABLED;
import static android.car.hardware.property.VehicleHalStatusCode.STATUS_NOT_AVAILABLE_POOR_VISIBILITY;
import static android.car.hardware.property.VehicleHalStatusCode.STATUS_NOT_AVAILABLE_SAFETY;
import static android.car.hardware.property.VehicleHalStatusCode.STATUS_NOT_AVAILABLE_SPEED_HIGH;
import static android.car.hardware.property.VehicleHalStatusCode.STATUS_NOT_AVAILABLE_SPEED_LOW;
import static android.car.hardware.property.VehicleHalStatusCode.STATUS_TRY_AGAIN;

import static com.android.car.hal.property.HalPropertyDebugUtils.toAreaIdString;
import static com.android.car.hal.property.HalPropertyDebugUtils.toHalPropIdAreaIdString;
import static com.android.car.hal.property.HalPropertyDebugUtils.toHalPropIdAreaIdsString;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DEBUGGING_CODE;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
import static com.android.car.internal.common.CommonConstants.EMPTY_INT_ARRAY;
import static com.android.car.internal.property.CarPropertyErrorCodes.createFromVhalStatusCode;
import static com.android.car.internal.property.CarPropertyHelper.isSystemProperty;
import static com.android.car.internal.property.CarPropertyHelper.newPropIdAreaId;
import static com.android.car.internal.property.GetSetValueResult.newGetValueResult;
import static com.android.car.internal.property.InputSanitizationUtils.sanitizeUpdateRateHz;
import static com.android.car.internal.util.DebugUtils.toDebugString;

import android.annotation.IntDef;
import android.annotation.Nullable;
import android.car.VehiclePropertyIds;
import android.car.builtin.os.BuildHelper;
import android.car.builtin.os.TraceHelper;
import android.car.builtin.util.Slogf;
import android.car.hardware.CarPropertyConfig;
import android.car.hardware.CarPropertyValue;
import android.car.hardware.property.AreaIdConfig;
import android.car.hardware.property.CarPropertyEvent;
import android.car.hardware.property.CarPropertyManager.CarSetPropertyErrorCode;
import android.car.hardware.property.ICarPropertyEventListener;
import android.car.hardware.property.VehicleHalStatusCode.VehicleHalStatusCodeInt;
import android.content.Context;
import android.hardware.automotive.vehicle.AnnotationsForVehicleProperty;
import android.hardware.automotive.vehicle.RawPropValues;
import android.hardware.automotive.vehicle.VehiclePropError;
import android.hardware.automotive.vehicle.VehicleProperty;
import android.hardware.automotive.vehicle.VehiclePropertyStatus;
import android.hardware.automotive.vehicle.VehiclePropertyType;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.os.SystemClock;
import android.os.Trace;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;

import com.android.car.CarLog;
import com.android.car.CarServiceUtils;
import com.android.car.VehicleStub;
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.property.PropertyPermissionInfo.PermissionCondition;
import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.internal.LongPendingRequestPool;
import com.android.car.internal.LongPendingRequestPool.TimeoutCallback;
import com.android.car.internal.LongRequestIdWithTimeout;
import com.android.car.internal.common.DispatchList;
import com.android.car.internal.property.AsyncPropertyServiceRequest;
import com.android.car.internal.property.CarPropertyErrorCodes;
import com.android.car.internal.property.CarPropertyHelper;
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.MinMaxSupportedPropertyValue;
import com.android.car.internal.property.PropIdAreaId;
import com.android.car.internal.property.RawPropertyValue;
import com.android.car.internal.property.SubscriptionManager;
import com.android.car.internal.util.IndentingPrintWriter;
import com.android.car.internal.util.PairSparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.expresslog.Histogram;

import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;

/**
 * Common interface for HAL services that send Vehicle Properties back and forth via ICarProperty.
 * Services that communicate by passing vehicle properties back and forth via ICarProperty should
 * extend this class.
 */
public class PropertyHalService extends HalServiceBase {
    private static final String TAG = CarLog.tagFor(PropertyHalService.class);
    private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG);
    private static final int ASYNC_RETRY_SLEEP_IN_MS = 100;

    // Async get request from user.
    private static final int GET = 0;
    // Async set request from user.
    private static final int SET = 1;
    // Async get request for getting initial value when user issues async set property request.
    // The reason we need to get initial value is that if the value to be set is the same as
    // the current value, there might not be a property update event generated. In this case,
    // it should be considered a success. If we get the initial value successfully and the
    // initial value is the same as the target value, we treat the async set as success.
    private static final int GET_INITIAL_VALUE_FOR_SET = 2;

    // A fake pending request ID for car property service.
    private static final int CAR_PROP_SVC_REQUEST_ID = -1;

    private static final long TRACE_TAG = TraceHelper.TRACE_TAG_CAR_SERVICE;

    // Only changed in testing.
    private PropertyHalServiceConfigs mPropertyHalServiceConfigs =
            PropertyHalServiceConfigs.getInstance();

    @GuardedBy("mLock")
    private final PairSparseArray<CarPropertyValue> mStaticPropertyIdAreaIdCache =
            new PairSparseArray<>();

    private final Histogram mGetAsyncEndToEndLatencyHistogram = new Histogram(
            "automotive_os.value_get_async_end_to_end_latency",
            new Histogram.ScaledRangeOptions(/* binCount= */ 20, /* minValue= */ 0,
                    /* firstBinWidth= */ 2, /* scaleFactor= */ 1.5f));

    private final Histogram mSetAsyncEndToEndLatencyHistogram = new Histogram(
            "automotive_os.value_set_async_end_to_end_latency",
            new Histogram.ScaledRangeOptions(/* binCount= */ 20, /* minValue= */ 0,
                    /* firstBinWidth= */ 2, /* scaleFactor= */ 1.5f));

    // Different type of async get/set property requests.
    @IntDef({GET, SET, GET_INITIAL_VALUE_FOR_SET})
    @Retention(RetentionPolicy.SOURCE)
    private @interface AsyncRequestType {}

    public record ClientType(Integer requestId) {
        @Override
        @ExcludeFromCodeCoverageGeneratedReport(reason = DEBUGGING_CODE)
        public String toString() {
            if (requestId == CAR_PROP_SVC_REQUEST_ID) {
                return "PropertyHalService.subscribeProperty";
            }
            return "PropertyHalService.setCarPropertyValuesAsync(requestId="
                    + requestId.toString() + ")";
        }
    }

    // This is a wrapper for death recipient that will unlink itself upon binder death.
    private static final class DeathRecipientWrapper implements DeathRecipient {
        private DeathRecipient mInnerRecipient;
        private final IBinder mClientBinder;

        DeathRecipientWrapper(IBinder clientBinder, DeathRecipient innerRecipient) {
            mInnerRecipient = innerRecipient;
            mClientBinder = clientBinder;
        }

        @Override
        public void binderDied() {
            mInnerRecipient.binderDied();
            mClientBinder.unlinkToDeath(this, /* flags= */ 0);
        }
    }

    private static final class GetSetValueResultWrapper {
        private GetSetValueResult mGetSetValueResult;
        private long mAsyncRequestStartTime;
        private final int mRetryCount;
        private final int mPropertyId;

        private GetSetValueResultWrapper(GetSetValueResult getSetValueResult,
                long asyncRequestStartTime, int retryCount, int propertyId) {
            mGetSetValueResult = getSetValueResult;
            mAsyncRequestStartTime = asyncRequestStartTime;
            mRetryCount = retryCount;
            mPropertyId = propertyId;
        }

        private GetSetValueResult getGetSetValueResult() {
            return mGetSetValueResult;
        }

        private long getAsyncRequestStartTime() {
            return mAsyncRequestStartTime;
        }

        private int getRetryCount() {
            return mRetryCount;
        }

        private int getPropertyId() {
            return mPropertyId;
        }
    }

    private static final class AsyncPropRequestInfo implements LongRequestIdWithTimeout {
        private final AsyncPropertyServiceRequest mPropMgrRequest;
        // The uptimeMillis when this request time out.
        private final long mTimeoutUptimeMs;
        private final @AsyncRequestType int mRequestType;
        private final VehicleStubCallback mVehicleStubCallback;
        private final long mAsyncRequestStartTime;
        private boolean mSetRequestSent;
        private long mUpdateTimestampNanos;
        private boolean mValueUpdated;
        private int mServiceRequestId;
        private float mUpdateRateHz;
        private int mRetryCount;
        // The associated async set request for get_initial_value request.
        private @Nullable AsyncPropRequestInfo mAssocSetValueRequestInfo;
        // The associated get initial value request for async set request.
        private @Nullable AsyncPropRequestInfo mAssocGetInitValueRequestInfo;

        AsyncPropRequestInfo(@AsyncRequestType int requestType,
                AsyncPropertyServiceRequest propMgrRequest,
                long timeoutUptimeMs, VehicleStubCallback vehicleStubCallback,
                long asyncRequestStartTime) {
            mPropMgrRequest = propMgrRequest;
            mTimeoutUptimeMs = timeoutUptimeMs;
            mRequestType = requestType;
            mVehicleStubCallback = vehicleStubCallback;
            mAsyncRequestStartTime = asyncRequestStartTime;
        }

        private @AsyncRequestType int getRequestType() {
            return mRequestType;
        }

        private int getManagerRequestId() {
            return mPropMgrRequest.getRequestId();
        }


        private String getPropertyName() {
            return VehiclePropertyIds.toString(getPropertyId());
        }

        int getPropertyId() {
            return mPropMgrRequest.getPropertyId();
        }

        int getAreaId() {
            return mPropMgrRequest.getAreaId();
        }

        public long getUpdateTimestampNanos() {
            return mUpdateTimestampNanos;
        }

        AsyncPropertyServiceRequest getPropSvcRequest() {
            return mPropMgrRequest;
        }

        GetSetValueResult toErrorResult(CarPropertyErrorCodes errorCodes) {
            return GetSetValueResult.newErrorResult(getManagerRequestId(), errorCodes);
        }

        GetSetValueResult toGetValueResult(CarPropertyValue value) {
            return newGetValueResult(getManagerRequestId(), value);
        }

        GetSetValueResult toSetValueResult(long updateTimestampNanos) {
            return GetSetValueResult.newSetValueResult(getManagerRequestId(),
                    updateTimestampNanos);
        }

        void setSetRequestSent() {
            mSetRequestSent = true;
        }

        void setValueUpdated(long updateTimestampNanos) {
            mValueUpdated = true;
            mUpdateTimestampNanos = updateTimestampNanos;
        }

        boolean isWaitForPropertyUpdate() {
            return mPropMgrRequest.isWaitForPropertyUpdate();
        }

        boolean success() {
            // If the set request is sent and either we don't wait for property update or the
            // property update happened (which includes the initial value is already the target
            // value)
            return mSetRequestSent && (!isWaitForPropertyUpdate() || mValueUpdated);
        }

        void setAssocSetValueRequestInfo(AsyncPropRequestInfo requestInfo) {
            mAssocSetValueRequestInfo = requestInfo;
        }

        @Nullable AsyncPropRequestInfo getAssocSetValueRequestInfo() {
            return mAssocSetValueRequestInfo;
        }

        void setAssocGetInitValueRequestInfo(AsyncPropRequestInfo requestInfo) {
            mAssocGetInitValueRequestInfo = requestInfo;
        }

        @Nullable AsyncPropRequestInfo getAssocGetInitValueRequestInfo() {
            return mAssocGetInitValueRequestInfo;
        }

        void setServiceRequestId(int serviceRequestId) {
            mServiceRequestId = serviceRequestId;
        }

        int getServiceRequestId() {
            return mServiceRequestId;
        }

        VehicleStubCallback getVehicleStubCallback() {
            return mVehicleStubCallback;
        }

        float getUpdateRateHz() {
            return mUpdateRateHz;
        }

        void incrementRetryCount() {
            mRetryCount++;
        }

        int getRetryCount() {
            return mRetryCount;
        }

        /**
         * Parses the updateRateHz from client and sanitize it.
         */
        void parseClientUpdateRateHz(CarPropertyConfig carPropertyConfig) {
            float clientUpdateRateHz = mPropMgrRequest.getUpdateRateHz();
            if (clientUpdateRateHz == 0.0f) {
                // If client does not specify a update rate for async set, subscribe at the max
                // update rate so that we can get the property update as soon as possible.
                clientUpdateRateHz = carPropertyConfig.getMaxSampleRate();
            }
            mUpdateRateHz = sanitizeUpdateRateHz(carPropertyConfig, clientUpdateRateHz);
        }

        @Override
        public long getTimeoutUptimeMs() {
            return mTimeoutUptimeMs;
        }

        @Override
        public long getRequestId() {
            return getServiceRequestId();
        }

        public long getAsyncRequestStartTime() {
            return mAsyncRequestStartTime;
        }

        @Override
        @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
        public String toString() {
            return new StringBuilder()
                    .append("AsyncPropRequestInfo{type: ")
                    .append(requestTypeToString(mRequestType))
                    .append(", mgrRequestId: ")
                    .append(getManagerRequestId())
                    .append(", property: ")
                    .append(getPropertyName())
                    .append(", areaId: ")
                    .append(getAreaId())
                    .append(", timeout at uptime: ")
                    .append(getTimeoutUptimeMs()).append("ms")
                    .append(", serviceRequestId: ")
                    .append(getServiceRequestId())
                    .append(", update rate: ")
                    .append(getUpdateRateHz()).append("hz")
                    .append(", value updated for set: ")
                    .append(mValueUpdated)
                    .append(", request sent for set: ")
                    .append(mSetRequestSent)
                    .append("}").toString();
        }
    };

    // A class to represent one ISupportedValuesChangeCallback client.
    // One SupportedValuesChangeClient is equal to another if they have the same binder.
    private static final class SupportedValuesChangeClient {
        private final ISupportedValuesChangeCallback mCallback;
        private final IBinder mBinder;

        SupportedValuesChangeClient(ISupportedValuesChangeCallback callback) {
            mCallback = callback;
            mBinder = callback.asBinder();
        }

        @Override
        public boolean equals(Object other) {
            if (this == other) {
                return true;
            }
            if (!(other instanceof SupportedValuesChangeClient)) {
                return false;
            }
            return mBinder.equals(((SupportedValuesChangeClient) other).mBinder);
        }

        @Override
        public int hashCode() {
            return mBinder.hashCode();
        }

        public ISupportedValuesChangeCallback getCallback() {
            return mCallback;
        }
    }

    // The request ID passed by CarPropertyService (ManagerRequestId) is directly passed from
    // CarPropertyManager. Multiple CarPropertyManagers use the same car service instance, thus,
    // the ManagerRequestId is not unique. We have to create another unique ID called
    // ServiceRequestId and pass it to underlying layer (VehicleHal and VehicleStub).
    // Internally, we will map ManagerRequestId to ServiceRequestId.
    private final AtomicInteger mServiceRequestIdCounter = new AtomicInteger(0);
    private final VehicleHal mVehicleHal;
    private final HalPropValueBuilder mPropValueBuilder;
    private final HandlerThread mHandlerThread =
            CarServiceUtils.getHandlerThread(getClass().getSimpleName());
    private final Handler mHandler = new Handler(mHandlerThread.getLooper());
    private final TimeoutCallback mTimeoutCallback = new AsyncRequestTimeoutCallback();

    private final Object mLock = new Object();
    @GuardedBy("mLock")
    private final Map<IBinder, VehicleStubCallback>
            mResultBinderToVehicleStubCallback = new ArrayMap<>();
    @GuardedBy("mLock")
    private final SparseArray<HalPropConfig> mHalPropIdToPropConfig =
            new SparseArray<>();
    // A pending request pool to store all pending async get/set property request info.
    // Service request ID is int, not long, but we only have one version of PendingRequestPool.
    @GuardedBy("mLock")
    private final LongPendingRequestPool<AsyncPropRequestInfo> mPendingAsyncRequests =
            new LongPendingRequestPool<>(mHandler.getLooper(), mTimeoutCallback);
    @GuardedBy("mLock")
    private PropertyHalListener mPropertyHalListener;
    // A map to store pending async set request info that are currently waiting for property update
    // events.
    @GuardedBy("mLock")
    private final SparseArray<List<AsyncPropRequestInfo>> mHalPropIdToWaitingUpdateRequestInfo =
            new SparseArray<>();
    // A map to store registered SupportedValuesChangeClient for each [propId, areaId].
    @GuardedBy("mLock")
    private final ArrayMap<PropIdAreaId, ArraySet<SupportedValuesChangeClient>>
            mSupportedValuesChangeClientByPropIdAreaId = new ArrayMap<>();

    // CarPropertyService subscribes to properties through PropertyHalService. Meanwhile,
    // PropertyHalService internally also subscribes to some property for async set operations.
    // We need to merge both of these subscription rate to VHAL.
    //
    // This manager uses async property request ID as key to store subscription caused by async
    // set operations. It uses CAR_PROP_SVC_REQUEST_ID as a fake key to store subscription caused
    // by car property service.
    //
    // For example, if we internally subscribed to [propA, areaA] at 10hz, client requests at 20hz,
    // then we need to tell VHAL to update the rate to 20hz. If we internally subscribed at 20hz,
    // client requests at 10hz, then we should do nothing, however, if we internally unsubscribe,
    // then the [propA, areaA] should be subscribed at 10hz.
    @GuardedBy("mLock")
    private final SubscriptionManager<ClientType> mSubManager = new SubscriptionManager<>();

    private class AsyncRequestTimeoutCallback implements TimeoutCallback {
        @Override
        public void onRequestsTimeout(List<Long> serviceRequestIds) {
            ArrayMap<VehicleStubCallback, List<Integer>> callbackToRequestIds = new ArrayMap<>();
            synchronized (mLock) {
                // Get the callback for the pending requests.
                for (int i = 0; i < serviceRequestIds.size(); i++) {
                    // Service ID is always a valid int.
                    int serviceRequestId = serviceRequestIds.get(i).intValue();
                    AsyncPropRequestInfo requestInfo =
                            getPendingAsyncPropRequestInfoLocked(serviceRequestId);
                    if (requestInfo == null) {
                        Slogf.i(TAG, "The pending request: %d finished before timeout handler",
                                serviceRequestId);
                        continue;
                    }
                    VehicleStubCallback callback = requestInfo.getVehicleStubCallback();
                    if (callbackToRequestIds.get(callback) == null) {
                        callbackToRequestIds.put(callback, new ArrayList<>());
                    }
                    callbackToRequestIds.get(callback).add(serviceRequestId);
                }
            }
            for (int i = 0; i < callbackToRequestIds.size(); i++) {
                callbackToRequestIds.keyAt(i).onRequestsTimeout(
                        callbackToRequestIds.valueAt(i), /* fromVhal= */ false);
            }
        }
    }

    private class VehicleStubCallback extends VehicleStubCallbackInterface {
        private final IAsyncPropertyResultCallback mAsyncPropertyResultCallback;
        private final IBinder mClientBinder;

        VehicleStubCallback(
                IAsyncPropertyResultCallback asyncPropertyResultCallback) {
            mAsyncPropertyResultCallback = asyncPropertyResultCallback;
            mClientBinder = asyncPropertyResultCallback.asBinder();
        }

        private static List<GetSetValueResult> logAndReturnResults(Histogram histogram,
                List<GetSetValueResultWrapper> getSetValueResultWrapperList,
                @AsyncRequestType int asyncRequestType) {
            try {
                List<GetSetValueResult> getSetValueResults = new ArrayList<>();
                long systemCurrentTimeMillis = System.currentTimeMillis();
                for (int i = 0; i < getSetValueResultWrapperList.size(); i++) {
                    GetSetValueResultWrapper getSetValueResultWrapper =
                            getSetValueResultWrapperList.get(i);
                    GetSetValueResult getSetValueResult = getSetValueResultWrapper
                            .getGetSetValueResult();
                    long durationInMillis = systemCurrentTimeMillis
                            - getSetValueResultWrapper.getAsyncRequestStartTime();
                    histogram.logSample((float)durationInMillis);
                    getSetValueResults.add(getSetValueResult);
                    if (DBG) {
                        Slogf.d(TAG, "E2E latency for %sPropertiesAsync for requestId: %d is %d ms",
                                requestTypeToString(asyncRequestType),
                                getSetValueResult.getRequestId(), durationInMillis);
                    }
                    if (getSetValueResultWrapper.getRetryCount() != 0) {
                        Slogf.i(TAG, "Async %s request finished after retry, requestID: %d,"
                                        + " CarPropertyValue: %s , retry count: %d",
                                requestTypeToString(asyncRequestType),
                                getSetValueResult.getRequestId(),
                                getSetValueResult.getCarPropertyValue(),
                                getSetValueResultWrapper.getRetryCount());
                    }
                }
                return getSetValueResults;
            } finally {
                if (asyncRequestType == SET) {
                    for (int i = 0; i < getSetValueResultWrapperList.size(); i++) {
                        GetSetValueResultWrapper getSetValueResultWrapper =
                                getSetValueResultWrapperList.get(i);
                        Trace.asyncTraceEnd(TRACE_TAG, "setCarPropertyValuesAsync-propertyId: "
                            + getSetValueResultWrapper.getPropertyId(),
                            /* cookie= */ getSetValueResultWrapper
                                .getGetSetValueResult()
                                .getRequestId());
                    }
                }
            }
        }

        private void sendGetValueResults(List<GetSetValueResultWrapper> results) {
            if (results.isEmpty()) {
                return;
            }
            List<GetSetValueResult> getSetValueResults = logAndReturnResults(
                    mGetAsyncEndToEndLatencyHistogram, results, GET);
            try {
                mAsyncPropertyResultCallback.onGetValueResults(
                        new GetSetValueResultList(getSetValueResults));
            } catch (RemoteException e) {
                Slogf.w(TAG, "sendGetValueResults: Client might have died already", e);
            }
        }

        void sendSetValueResults(List<GetSetValueResultWrapper> results) {
            if (results.isEmpty()) {
                return;
            }
            List<GetSetValueResult> getSetValueResults = logAndReturnResults(
                    mSetAsyncEndToEndLatencyHistogram, results, SET);
            try {
                mAsyncPropertyResultCallback.onSetValueResults(
                        new GetSetValueResultList(getSetValueResults));
            } catch (RemoteException e) {
                Slogf.w(TAG, "sendSetValueResults: Client might have died already", e);
            }
        }

        private void retryIfNotExpired(List<AsyncPropRequestInfo> retryRequests) {
            List<AsyncGetSetRequest> vehicleStubAsyncGetRequests = new ArrayList<>();
            List<GetSetValueResultWrapper> timeoutGetResults = new ArrayList<>();
            List<AsyncGetSetRequest> vehicleStubAsyncSetRequests = new ArrayList<>();
            List<GetSetValueResultWrapper> timeoutSetResults = new ArrayList<>();
            List<AsyncPropRequestInfo> pendingRetryRequests = new ArrayList<>();
            synchronized (mLock) {
                // Get the current time after obtaining lock since it might take some time to get
                // the lock.
                long currentUptimeMs = SystemClock.uptimeMillis();
                for (int i = 0; i < retryRequests.size(); i++) {
                    AsyncPropRequestInfo requestInfo = retryRequests.get(i);
                    requestInfo.incrementRetryCount();
                    long timeoutUptimeMs = requestInfo.getTimeoutUptimeMs();
                    if (timeoutUptimeMs <= currentUptimeMs) {
                        // The request already expired.
                        generateTimeoutResult(requestInfo, timeoutGetResults, timeoutSetResults);
                        continue;
                    }

                    // Generate a new service request ID and async request object for the retry.
                    AsyncGetSetRequest vehicleStubAsyncRequest =
                            generateVehicleStubAsyncRequestLocked(requestInfo);
                    pendingRetryRequests.add(requestInfo);

                    switch (requestInfo.getRequestType()) {
                        case GET: // fallthrough
                        case GET_INITIAL_VALUE_FOR_SET:
                            vehicleStubAsyncGetRequests.add(vehicleStubAsyncRequest);
                            break;
                        case SET:
                            vehicleStubAsyncSetRequests.add(vehicleStubAsyncRequest);
                            break;
                    }
                }

                // We already marked all the input requests as finished. Now for the new retry
                // requests, we need to put them back into the pending request pool.
                mPendingAsyncRequests.addPendingRequests(pendingRetryRequests);
            }

            sendGetValueResults(timeoutGetResults);
            if (!vehicleStubAsyncGetRequests.isEmpty()) {
                mVehicleHal.getAsync(vehicleStubAsyncGetRequests, this);
            }
            sendSetValueResults(timeoutSetResults);
            if (!vehicleStubAsyncSetRequests.isEmpty()) {
                mVehicleHal.setAsync(vehicleStubAsyncSetRequests, this);
            }
        }

        IBinder getClientBinder() {
            return mClientBinder;
        }

        @Override
        public void linkToDeath(DeathRecipient recipient) throws RemoteException {
            mClientBinder.linkToDeath(new DeathRecipientWrapper(mClientBinder, recipient),
                    /* flags= */ 0);
        }

        // Parses an async getProperty result and convert it to an okay/error result.
        private GetSetValueResult parseGetAsyncResults(
                GetVehicleStubAsyncResult getVehicleStubAsyncResult,
                AsyncPropRequestInfo clientRequestInfo) {
            var errorCodes = getVehicleStubAsyncResult.getCarPropertyErrorCodes();
            if (!errorCodes.isOkay()) {
                // All other error results will be delivered back through callback.
                return clientRequestInfo.toErrorResult(errorCodes);
            }

            // For okay status, convert the property value to the type the client expects.
            int mgrPropId = clientRequestInfo.getPropertyId();
            int halPropId = managerToHalPropId(mgrPropId);
            HalPropConfig halPropConfig;
            synchronized (mLock) {
                halPropConfig = mHalPropIdToPropConfig.get(halPropId);
            }
            if (halPropConfig == null) {
                Slogf.e(TAG, "No configuration found for property: %s, must not happen",
                        clientRequestInfo.getPropertyName());
                return clientRequestInfo.toErrorResult(
                        CarPropertyErrorCodes.ERROR_CODES_INTERNAL);
            }
            HalPropValue halPropValue = getVehicleStubAsyncResult.getHalPropValue();
            if (halPropValue.getStatus() == VehiclePropertyStatus.UNAVAILABLE) {
                return clientRequestInfo.toErrorResult(
                        CarPropertyErrorCodes.ERROR_CODES_NOT_AVAILABLE);
            }
            if (halPropValue.getStatus() != VehiclePropertyStatus.AVAILABLE) {
                return clientRequestInfo.toErrorResult(
                        CarPropertyErrorCodes.ERROR_CODES_INTERNAL);
            }

            try {
                return clientRequestInfo.toGetValueResult(
                        halPropValue.toCarPropertyValue(mgrPropId, halPropConfig));
            } catch (IllegalStateException e) {
                Slogf.e(TAG, e,
                        "Cannot convert halPropValue to carPropertyValue, property: %s, areaId: %d",
                        halPropIdToName(halPropValue.getPropId()), halPropValue.getAreaId());
                return clientRequestInfo.toErrorResult(
                        CarPropertyErrorCodes.ERROR_CODES_INTERNAL);
            }
        }

        @Override
        public void onGetAsyncResults(
                List<GetVehicleStubAsyncResult> getVehicleStubAsyncResults) {
            List<GetSetValueResultWrapper> getValueResults = new ArrayList<>();
            // If we receive get value result for initial value request and the result is the
            // same as the target value, we might finish the associated async set value request.
            // So we need potential set value results here.
            List<GetSetValueResultWrapper> setValueResults = new ArrayList<>();
            List<AsyncPropRequestInfo> retryRequests = new ArrayList<>();
            synchronized (mLock) {
                for (int i = 0; i < getVehicleStubAsyncResults.size(); i++) {
                    GetVehicleStubAsyncResult getVehicleStubAsyncResult =
                            getVehicleStubAsyncResults.get(i);
                    int serviceRequestId = getVehicleStubAsyncResult.getServiceRequestId();
                    AsyncPropRequestInfo clientRequestInfo =
                            getAndRemovePendingAsyncPropRequestInfoLocked(serviceRequestId);
                    if (clientRequestInfo == null) {
                        Slogf.w(TAG, "async request for ID: %d not found, ignore the result",
                                serviceRequestId);
                        continue;
                    }

                    var errorCodes = getVehicleStubAsyncResult.getCarPropertyErrorCodes();
                    if (errorCodes.isTryAgain()) {
                        // The request might need to be retried.
                        if (DBG) {
                            Slogf.d(TAG, "request: %s try again", clientRequestInfo);
                        }
                        retryRequests.add(clientRequestInfo);
                        continue;
                    }

                    GetSetValueResult result = parseGetAsyncResults(getVehicleStubAsyncResult,
                            clientRequestInfo);
                    CarPropertyValue carPropertyValue = result.getCarPropertyValue();
                    if (clientRequestInfo.getRequestType() != GET_INITIAL_VALUE_FOR_SET) {
                        getValueResults.add(new GetSetValueResultWrapper(result,
                                clientRequestInfo.getAsyncRequestStartTime(),
                                clientRequestInfo.getRetryCount(),
                                clientRequestInfo.getPropertyId()));
                        if (carPropertyValue != null) {
                            int propertyId = carPropertyValue.getPropertyId();
                            int areaId = carPropertyValue.getAreaId();
                            if (isStaticAndSystemPropertyLocked(propertyId)) {
                                mStaticPropertyIdAreaIdCache.put(propertyId, areaId,
                                        carPropertyValue);
                            }
                        }
                        continue;
                    }

                    if (DBG) {
                        Slogf.d(TAG, "handling init value result for request: %s",
                                clientRequestInfo);
                    }
                    // Handle GET_INITIAL_VALUE_FOR_SET result.
                    errorCodes = result.getCarPropertyErrorCodes();
                    if (!errorCodes.isOkay()) {
                        Slogf.w(TAG, "the init value get request: %s failed, ignore the result, "
                                + "error: %s", clientRequestInfo, errorCodes);
                        continue;
                    }
                    // If the initial value result is the target value and the async set
                    // request returned, we finish the pending async set result.
                    AsyncPropRequestInfo assocSetValueRequestInfo =
                            clientRequestInfo.getAssocSetValueRequestInfo();
                    if (assocSetValueRequestInfo == null) {
                        Slogf.e(TAG, "received get initial value result, but no associated set "
                                + "value request is defined");
                        continue;
                    }
                    GetSetValueResult maybeSetResult = maybeFinishPendingSetValueRequestLocked(
                            assocSetValueRequestInfo, carPropertyValue);
                    if (maybeSetResult != null) {
                        if (DBG) {
                            Slogf.d(TAG, "The initial value is the same as target value for "
                                    + "request: %s, sending success set result",
                                    assocSetValueRequestInfo);
                        }
                        setValueResults.add(new GetSetValueResultWrapper(maybeSetResult,
                                assocSetValueRequestInfo.getAsyncRequestStartTime(),
                                assocSetValueRequestInfo.getRetryCount(),
                                assocSetValueRequestInfo.getPropertyId()));
                        removePendingAsyncPropRequestInfoLocked(assocSetValueRequestInfo);
                    }
                }
                updateSubscriptionRateForAsyncSetRequestLocked();
            }

            sendGetValueResults(getValueResults);
            sendSetValueResults(setValueResults);

            if (!retryRequests.isEmpty()) {
                mHandler.postDelayed(() -> {
                    retryIfNotExpired(retryRequests);
                }, ASYNC_RETRY_SLEEP_IN_MS);
            }
        }

        @Override
        public void onSetAsyncResults(
                List<SetVehicleStubAsyncResult> setVehicleStubAsyncResults) {
            List<GetSetValueResultWrapper> setValueResults = new ArrayList<>();
            List<AsyncPropRequestInfo> retryRequests = new ArrayList<>();
            synchronized (mLock) {
                for (int i = 0; i < setVehicleStubAsyncResults.size(); i++) {
                    SetVehicleStubAsyncResult setVehicleStubAsyncResult =
                            setVehicleStubAsyncResults.get(i);
                    int serviceRequestId = setVehicleStubAsyncResult.getServiceRequestId();
                    AsyncPropRequestInfo clientRequestInfo =
                            getPendingAsyncPropRequestInfoLocked(serviceRequestId);
                    if (clientRequestInfo == null) {
                        Slogf.w(TAG, "async request for ID:  %d not found, ignore the result",
                                serviceRequestId);
                        continue;
                    }

                    var errorCodes = setVehicleStubAsyncResult.getCarPropertyErrorCodes();
                    if (errorCodes.isTryAgain()) {
                        // The request might need to be retried.
                        retryRequests.add(clientRequestInfo);
                        removePendingAsyncPropRequestInfoLocked(clientRequestInfo);
                        continue;
                    }

                    if (!errorCodes.isOkay()) {
                        // All other error results will be delivered back through callback.
                        setValueResults.add(new GetSetValueResultWrapper(clientRequestInfo
                                .toErrorResult(errorCodes),
                                clientRequestInfo.getAsyncRequestStartTime(),
                                clientRequestInfo.getRetryCount(),
                                clientRequestInfo.getPropertyId()));
                        removePendingAsyncPropRequestInfoLocked(clientRequestInfo);
                        mSetAsyncEndToEndLatencyHistogram
                                .logSample((float) System.currentTimeMillis()
                                - clientRequestInfo.getAsyncRequestStartTime());
                        continue;
                    }

                    clientRequestInfo.setSetRequestSent();
                    if (clientRequestInfo.success()) {
                        // If we have already received event for the target value or the initial
                        // value is already the target value. Mark the request as complete.
                        removePendingAsyncPropRequestInfoLocked(clientRequestInfo);
                        // If we don't wait for property update event, then we don't know when
                        // the property is updated to the target value. We set it to the
                        // current timestamp.
                        long updateTimestampNanos = clientRequestInfo.isWaitForPropertyUpdate()
                                ? clientRequestInfo.getUpdateTimestampNanos() :
                                SystemClock.elapsedRealtimeNanos();
                        setValueResults.add(new GetSetValueResultWrapper(clientRequestInfo
                                .toSetValueResult(updateTimestampNanos),
                                clientRequestInfo.getAsyncRequestStartTime(),
                                clientRequestInfo.getRetryCount(),
                                clientRequestInfo.getPropertyId()));
                    }
                }
                updateSubscriptionRateForAsyncSetRequestLocked();
            }

            sendSetValueResults(setValueResults);

            if (!retryRequests.isEmpty()) {
                mHandler.postDelayed(() -> {
                    retryIfNotExpired(retryRequests);
                }, ASYNC_RETRY_SLEEP_IN_MS);
            }
        }

        private void generateTimeoutResult(AsyncPropRequestInfo requestInfo,
                List<GetSetValueResultWrapper> timeoutGetResults,
                List<GetSetValueResultWrapper> timeoutSetResults) {
            GetSetValueResult timeoutResult =  requestInfo.toErrorResult(
                    CarPropertyErrorCodes.ERROR_CODES_TIMEOUT);
            Slogf.w(TAG, "the %s request for request ID: %d time out, request time: %d ms, current"
                    + " time: %d ms", requestTypeToString(requestInfo.getRequestType()),
                    requestInfo.getRequestId(), requestInfo.getAsyncRequestStartTime(),
                    System.currentTimeMillis());

            switch (requestInfo.getRequestType()) {
                case GET:
                    timeoutGetResults.add(new GetSetValueResultWrapper(timeoutResult,
                            requestInfo.getAsyncRequestStartTime(), requestInfo.getRetryCount(),
                            requestInfo.getPropertyId()));
                    break;
                case GET_INITIAL_VALUE_FOR_SET:
                    // Do not send the timeout requests back to the user because the original
                    // request is not originated from the user.
                    Slogf.e(TAG, "the initial value request: %s timeout", requestInfo);
                    break;
                case SET:
                    timeoutSetResults.add(new GetSetValueResultWrapper(timeoutResult,
                            requestInfo.getAsyncRequestStartTime(), requestInfo.getRetryCount(),
                            requestInfo.getPropertyId()));
                    break;
            }
        }

        @Override
        public void onRequestsTimeout(List<Integer> serviceRequestIds) {
            onRequestsTimeout(serviceRequestIds, /* fromVhal= */ true);
        }

        private void onRequestsTimeout(List<Integer> serviceRequestIds, boolean fromVhal) {
            if (fromVhal) {
                Slogf.w(TAG, "Requests timeout from VHAL, service Request IDs: %s",
                        serviceRequestIds);
            } else {
                Slogf.w(TAG, "Requests timeout internally, service Request IDs: %s",
                        serviceRequestIds);
            }
            List<GetSetValueResultWrapper> timeoutGetResults = new ArrayList<>();
            List<GetSetValueResultWrapper> timeoutSetResults = new ArrayList<>();
            synchronized (mLock) {
                for (int i = 0; i < serviceRequestIds.size(); i++) {
                    int serviceRequestId = serviceRequestIds.get(i);
                    AsyncPropRequestInfo requestInfo =
                            getAndRemovePendingAsyncPropRequestInfoLocked(serviceRequestId,
                                    /* alreadyTimedOut= */ true);
                    if (requestInfo == null) {
                        Slogf.i(TAG, "Service request ID %d time out but no "
                                + "pending request is found. The request may have already been "
                                + "cancelled, timeout or finished", serviceRequestId);
                        continue;
                    }
                    if (DBG) {
                        Slogf.d(TAG, "Request: %s time out", requestInfo);
                    }
                    generateTimeoutResult(requestInfo, timeoutGetResults, timeoutSetResults);
                }
                updateSubscriptionRateForAsyncSetRequestLocked();
            }
            sendGetValueResults(timeoutGetResults);
            sendSetValueResults(timeoutSetResults);
        }
    }

    /**
     * Maybe finish the pending set value request depending on the updated value.
     *
     * Check whether the updated property value is the same as the target value for pending
     * set value requests. If so, finish those requests.
     *
     * @return A success set value result for the finished request or {@code null}.
     */
    @GuardedBy("mLock")
    @Nullable
    private GetSetValueResult maybeFinishPendingSetValueRequestLocked(
            AsyncPropRequestInfo pendingSetValueRequest, CarPropertyValue updatedValue) {
        Object targetValue = pendingSetValueRequest.getPropSvcRequest()
                .getCarPropertyValue().getValue();
        Object currentValue = updatedValue.getValue();
        if (!targetValue.equals(currentValue)) {
            if (DBG) {
                Slogf.d(TAG, "Async set value request: %s receive different updated value: %s"
                        + " than target value: %s", pendingSetValueRequest, currentValue,
                        targetValue);
            }
            return null;
        }
        long updateTimestampNanos = updatedValue.getTimestamp();
        pendingSetValueRequest.setValueUpdated(updateTimestampNanos);
        if (!pendingSetValueRequest.success()) {
            return null;
        }

        return pendingSetValueRequest.toSetValueResult(updateTimestampNanos);
    }

    /**
     * Generates a {@link AsyncGetSetRequest} according to a {@link AsyncPropRequestInfo}.
     *
     * <p>Generates a new PropertyHalService Request ID. Associate the ID with the request and
     * returns a {@link AsyncGetSetRequest} that could be sent to {@link VehicleStub}.
     */
    @GuardedBy("mLock")
    private AsyncGetSetRequest generateVehicleStubAsyncRequestLocked(
            AsyncPropRequestInfo asyncPropRequestInfo) {
        int serviceRequestId = mServiceRequestIdCounter.getAndIncrement();
        asyncPropRequestInfo.setServiceRequestId(serviceRequestId);

        HalPropValue halPropValue;
        CarPropertyValue requestCarPropertyValue = asyncPropRequestInfo.getPropSvcRequest()
                .getCarPropertyValue();
        if (requestCarPropertyValue != null) {
            // If this is a set request, the car property value stores the value to be set.
            halPropValue = carPropertyValueToHalPropValueLocked(requestCarPropertyValue);
        } else {
            // Otherwise this is a get request, we only need the property ID and area ID.
            int halPropertyId = managerToHalPropId(asyncPropRequestInfo.getPropertyId());
            int areaId = asyncPropRequestInfo.getAreaId();
            halPropValue = mPropValueBuilder.build(halPropertyId, areaId);
        }
        return new AsyncGetSetRequest(serviceRequestId, halPropValue,
                asyncPropRequestInfo.getTimeoutUptimeMs());
    }

    @GuardedBy("mLock")
    @Nullable private AsyncPropRequestInfo getPendingAsyncPropRequestInfoLocked(
            int serviceRequestId) {
        AsyncPropRequestInfo requestInfo =
                mPendingAsyncRequests.getRequestIfFound(serviceRequestId);
        if (requestInfo == null) {
            Slogf.i(TAG, "the request for propertyHalService request "
                    + "ID: %d already timed out or already completed", serviceRequestId);
        }
        return requestInfo;
    }

    @GuardedBy("mLock")
    @Nullable private AsyncPropRequestInfo getAndRemovePendingAsyncPropRequestInfoLocked(
            int serviceRequestId) {
        return getAndRemovePendingAsyncPropRequestInfoLocked(serviceRequestId,
                /* alreadyTimedOut= */ false);
    }

    @GuardedBy("mLock")
    @Nullable private AsyncPropRequestInfo getAndRemovePendingAsyncPropRequestInfoLocked(
            int serviceRequestId, boolean alreadyTimedOut) {
        AsyncPropRequestInfo requestInfo = getPendingAsyncPropRequestInfoLocked(serviceRequestId);
        if (requestInfo == null) {
            return null;
        }
        removePendingAsyncPropRequestInfoLocked(requestInfo, alreadyTimedOut);
        return requestInfo;
    }

    /**
     * Remove the pending async request from the pool.
     *
     * If the request to remove is an async set request, also remove it from the
     * {@code mHalPropIdToWaitingUpdateRequestInfo} map. This will cause the subscription rate to
     * be updated for the specific property because we no longer need to monitor this property
     * any more internally.
     *
     * The {@code updatedAreaIdsByHalPropIds} will store the affected area Ids and property IDs if
     * their subscription rate need to be recalculated.
     */
    @GuardedBy("mLock")
    private void removePendingAsyncPropRequestInfoLocked(AsyncPropRequestInfo pendingRequest) {
        removePendingAsyncPropRequestInfoLocked(pendingRequest, /* alreadyTimedOut= */ false);
    }

    /**
     * Remove the pending async request from the pool.
     *
     * If the request to remove is an async set request, also remove it from the
     * {@code mHalPropIdToWaitingUpdateRequestInfo} map. This will cause the subscription rate to
     * be updated for the specific property because we no longer need to monitor this property
     * any more internally.
     *
     * The {@code updatedAreaIdsByHalPropIds} will store the affected area Ids and property IDs if
     * their subscription rate need to be recalculated.
     */
    @GuardedBy("mLock")
    private void removePendingAsyncPropRequestInfoLocked(AsyncPropRequestInfo pendingRequest,
            boolean alreadyTimedOut) {
        int serviceRequestId = pendingRequest.getServiceRequestId();
        mPendingAsyncRequests.removeRequest(serviceRequestId, alreadyTimedOut);
        if (pendingRequest.getRequestType() == SET) {
            cleanupPendingAsyncSetRequestLocked(pendingRequest);
        }
    }

    @GuardedBy("mLock")
    private void cleanupPendingAsyncSetRequestLocked(AsyncPropRequestInfo pendingRequest) {
        int halPropId = managerToHalPropId(pendingRequest.getPropertyId());
        if (!pendingRequest.isWaitForPropertyUpdate()) {
            return;
        }
        if (pendingRequest.getAssocGetInitValueRequestInfo() == null) {
            Slogf.e(TAG, "The pending async set value request: %s"
                    + " does not have an associated get initial value request, must not happen",
                    pendingRequest);
            return;
        }
        // If we are removing an async set property request, then we should remove its associated
        // get initial value request as well if it has not been finished.
        AsyncPropRequestInfo assocGetInitValueRequestInfo =
                pendingRequest.getAssocGetInitValueRequestInfo();
        int assocInitValueRequestId = assocGetInitValueRequestInfo.getServiceRequestId();
        assocGetInitValueRequestInfo = mPendingAsyncRequests.getRequestIfFound(
                assocInitValueRequestId);
        if (assocGetInitValueRequestInfo != null) {
            mPendingAsyncRequests.removeRequest(assocInitValueRequestId);
            // Use a separate runnable to do this outside lock.
            mHandler.post(() -> mVehicleHal.cancelRequests(List.of(assocInitValueRequestId)));
        }
        if (!mHalPropIdToWaitingUpdateRequestInfo.contains(halPropId)) {
            return;
        }
        if (!mHalPropIdToWaitingUpdateRequestInfo.get(halPropId).remove(pendingRequest)) {
            return;
        }
        if (mHalPropIdToWaitingUpdateRequestInfo.get(halPropId).isEmpty()) {
            mHalPropIdToWaitingUpdateRequestInfo.remove(halPropId);
        }
        // We no longer need to subscribe to the property.
        mSubManager.stageUnregister(new ClientType(pendingRequest.getServiceRequestId()),
                new ArraySet<Integer>(Set.of(halPropId)));
    }

    /**
     * PropertyHalListener used to send events to CarPropertyService
     */
    public interface PropertyHalListener {
        /**
         * This event is sent whenever the property value is updated
         */
        void onPropertyChange(List<CarPropertyEvent> events);

        /**
         * This event is sent when the set property call fails
         */
        void onPropertySetError(int property, int area,
                @CarSetPropertyErrorCode int errorCode);

    }

    public PropertyHalService(VehicleHal vehicleHal) {
        mVehicleHal = vehicleHal;
        if (DBG) {
            Slogf.d(TAG, "started PropertyHalService");
        }
        mPropValueBuilder = vehicleHal.getHalPropValueBuilder();
    }

    /**
     * Set the listener for the HAL service
     */
    public void setPropertyHalListener(PropertyHalListener propertyHalListener) {
        synchronized (mLock) {
            mPropertyHalListener = propertyHalListener;
        }
    }

    /**
     * Used for resetting the configs state during unit testing. The real implementation uses a
     * static instance of configs so one test will affect the state of another.
     */
    @VisibleForTesting
    void setPropertyHalServiceConfigs(PropertyHalServiceConfigs configs) {
        mPropertyHalServiceConfigs = configs;
    }

    /**
     * @return SparseArray<CarPropertyConfig> List of configs available.
     */
    public SparseArray<CarPropertyConfig<?>> getPropertyList() {
        if (DBG) {
            Slogf.d(TAG, "getPropertyList");
        }
        synchronized (mLock) {
            SparseArray<CarPropertyConfig<?>> mgrPropIdToCarPropConfig = new SparseArray<>();
            for (int i = 0; i < mHalPropIdToPropConfig.size(); i++) {
                HalPropConfig halPropConfig = mHalPropIdToPropConfig.valueAt(i);
                int mgrPropId = halToManagerPropId(halPropConfig.getPropId());
                CarPropertyConfig<?> carPropertyConfig = halPropConfig.toCarPropertyConfig(
                        mgrPropId, mPropertyHalServiceConfigs);
                mgrPropIdToCarPropConfig.put(mgrPropId, carPropertyConfig);
            }
            return mgrPropIdToCarPropConfig;
        }
    }

    /**
     * Returns property value.
     *
     * @param mgrPropId property id in {@link VehiclePropertyIds}
     * @throws IllegalArgumentException if argument is not valid.
     * @throws ServiceSpecificException if there is an exception in HAL or the property status is
     *                                  not available.
     */
    public CarPropertyValue getProperty(int mgrPropId, int areaId)
            throws IllegalArgumentException, ServiceSpecificException {
        int halPropId = managerToHalPropId(mgrPropId);
        // CarPropertyManager catches and rethrows exception, no need to handle here.
        HalPropValue halPropValue;
        HalPropConfig halPropConfig;
        synchronized (mLock) {
            halPropConfig = mHalPropIdToPropConfig.get(halPropId);
            if (isStaticAndSystemPropertyLocked(mgrPropId)) {
                CarPropertyValue carPropertyValue = mStaticPropertyIdAreaIdCache.get(mgrPropId,
                        areaId);
                if (carPropertyValue != null) {
                    if (DBG) {
                        Slogf.d(TAG, "Get Sync Property: %s retrieved from cache",
                                VehiclePropertyIds.toString(mgrPropId));
                    }
                    return carPropertyValue;
                }
            }
        }
        halPropValue = mVehicleHal.get(halPropId, areaId);
        try {
            CarPropertyValue result = halPropValue.toCarPropertyValue(mgrPropId, halPropConfig);
            synchronized (mLock) {
                if (!isStaticAndSystemPropertyLocked(mgrPropId)) {
                    return result;
                }
                mStaticPropertyIdAreaIdCache.put(mgrPropId, areaId, result);
                return result;
            }
        } catch (IllegalStateException e) {
            throw new ServiceSpecificException(STATUS_INTERNAL_ERROR,
                    "Cannot convert halPropValue to carPropertyValue, property: "
                    + VehiclePropertyIds.toString(mgrPropId) + " areaId: "
                    + toAreaIdString(halPropId, areaId)
                    + ", exception: " + e);
        }
    }

    /**
     * Get the read permission string for the property. The format of the return value of this
     * function has changed over time and thus should not be relied on.
     */
    @Nullable
    public String getReadPermission(int mgrPropId) {
        PermissionCondition readPermission =
                mPropertyHalServiceConfigs.getReadPermission(managerToHalPropId(mgrPropId));
        if (readPermission == null) {
            Slogf.w(TAG, "readPermission is null for mgrPropId: "
                    + VehiclePropertyIds.toString(mgrPropId));
            return null;
        }
        return readPermission.toString();
    }

    /**
     * Get the write permission string for the property. The format of the return value of this
     * function has changed over time and thus should not be relied on.
     */
    @Nullable
    public String getWritePermission(int mgrPropId) {
        PermissionCondition writePermission =
                mPropertyHalServiceConfigs.getWritePermission(managerToHalPropId(mgrPropId));
        if (writePermission == null) {
            Slogf.w(TAG, "writePermission is null for mgrPropId: "
                    + VehiclePropertyIds.toString(mgrPropId));
            return null;
        }
        return writePermission.toString();
    }

    /**
     * Checks whether the property is readable.
     */
    public boolean isReadable(Context context, int mgrPropId) {
        return mPropertyHalServiceConfigs.isReadable(context, managerToHalPropId(mgrPropId));
    }

    /**
     * Checks whether the property is writable.
     */
    public boolean isWritable(Context context, int mgrPropId) {
        return mPropertyHalServiceConfigs.isWritable(context, managerToHalPropId(mgrPropId));
    }

    /**
     * Set the property value.
     *
     * @throws IllegalArgumentException if argument is invalid.
     * @throws ServiceSpecificException if there is an exception in HAL.
     */
    public void setProperty(CarPropertyValue carPropertyValue)
            throws IllegalArgumentException, ServiceSpecificException {
        HalPropValue valueToSet;
        synchronized (mLock) {
            valueToSet = carPropertyValueToHalPropValueLocked(carPropertyValue);
        }

        // CarPropertyManager catches and rethrows exception, no need to handle here.
        mVehicleHal.set(valueToSet);
    }

    /**
     * Subscribe to this property at the specified updateRateHz and areaId. The list of
     * carSubscriptions should never be empty since it is checked at CarPropertyService.
     *
     * @throws ServiceSpecificException If VHAL returns error.
     */
    public void subscribeProperty(List<CarSubscription> carSubscriptions)
            throws ServiceSpecificException {
        synchronized (mLock) {
            // Even though this involves binder call, this must be done inside the lock so that
            // the state in {@code mSubManager} is consistent with the state in VHAL.
            for (int i = 0; i < carSubscriptions.size(); i++) {
                CarSubscription carSubscription = carSubscriptions.get(i);
                int mgrPropId = carSubscription.propertyId;
                int[] areaIds = carSubscription.areaIds;
                float updateRateHz = carSubscription.updateRateHz;
                if (DBG) {
                    Slogf.d(TAG, "subscribeProperty propertyId: %s, updateRateHz=%f",
                            VehiclePropertyIds.toString(mgrPropId), updateRateHz);
                }
                int halPropId = managerToHalPropId(mgrPropId);
                // Note that we use halPropId instead of mgrPropId in mSubManager.
                mSubManager.stageNewOptions(new ClientType(CAR_PROP_SVC_REQUEST_ID),
                        List.of(newCarSubscription(halPropId, areaIds, updateRateHz,
                        carSubscription.enableVariableUpdateRate, carSubscription.resolution)));
            }
            try {
                updateSubscriptionRateLocked();
            } catch (ServiceSpecificException e) {
                Slogf.e(TAG, "Failed to update subscription rate for subscribe", e);
                throw e;
            }
        }
    }

    /**
     * Unsubscribe the property and turn off update events for it.
     *
     * @throws ServiceSpecificException If VHAL returns error.
     */
    public void unsubscribeProperty(int mgrPropId) throws ServiceSpecificException {
        if (DBG) {
            Slogf.d(TAG, "unsubscribeProperty mgrPropId=%s",
                    VehiclePropertyIds.toString(mgrPropId));
        }
        int halPropId = managerToHalPropId(mgrPropId);
        synchronized (mLock) {
            // Even though this involves binder call, this must be done inside the lock so that
            // the state in {@code mSubManager} is consistent with the state in VHAL.
            mSubManager.stageUnregister(new ClientType(CAR_PROP_SVC_REQUEST_ID),
                    new ArraySet<Integer>(Set.of(halPropId)));
            try {
                updateSubscriptionRateLocked();
            } catch (ServiceSpecificException e) {
                Slogf.e(TAG, "Failed to update subscription rate for unsubscribe, "
                        + "restoring previous state", e);
                throw e;
            }
        }
    }

    @Override
    public void init() {
        if (DBG) {
            Slogf.d(TAG, "init()");
        }
    }

    @Override
    public void release() {
        if (DBG) {
            Slogf.d(TAG, "release()");
        }
        synchronized (mLock) {
            ArraySet<Integer> halPropIds = mSubManager.getCurrentSubscribedPropIds();
            for (int i = 0; i < halPropIds.size(); i++) {
                int halPropId = halPropIds.valueAt(i);
                mVehicleHal.unsubscribePropertySafe(this, halPropId);
            }
            mSubManager.clear();
            mHalPropIdToPropConfig.clear();
            mPropertyHalListener = null;
        }
        mHandlerThread.quitSafely();
    }

    @Override
    public boolean isSupportedProperty(int halPropId) {
        return mPropertyHalServiceConfigs.isSupportedProperty(halPropId)
                && CarPropertyHelper.isSupported(halToManagerPropId(halPropId));
    }

    @Override
    public int[] getAllSupportedProperties() {
        return EMPTY_INT_ARRAY;
    }

    // The method is called in HAL init(). Avoid handling complex things in here.
    @Override
    public void takeProperties(Collection<HalPropConfig> halPropConfigs) {
        for (HalPropConfig halPropConfig : halPropConfigs) {
            int halPropId = halPropConfig.getPropId();
            if (isSupportedProperty(halPropId)) {
                synchronized (mLock) {
                    mHalPropIdToPropConfig.put(halPropId, halPropConfig);
                }
                if (DBG) {
                    Slogf.d(TAG, "takeSupportedProperties: %s", halPropIdToName(halPropId));
                }
            } else {
                if (DBG) {
                    Slogf.d(TAG, "takeProperties: Property: %s is not supported, ignore",
                            halPropIdToName(halPropId));
                }
            }
        }
        if (DBG) {
            Slogf.d(TAG, "takeSupportedProperties() took %d properties", halPropConfigs.size());
        }
        // If vehicle hal support to select permission for vendor properties.
        HalPropConfig customizePermission = mVehicleHal.getPropConfig(
                VehicleProperty.SUPPORT_CUSTOMIZE_VENDOR_PERMISSION);
        if (customizePermission != null) {
            mPropertyHalServiceConfigs.customizeVendorPermission(
                    customizePermission.getConfigArray());
        } else {
            if (DBG) {
                Slogf.d(TAG, "No custom vendor permission defined in VHAL");
            }
        }
    }

    /**
     * Gets the currently min/max supported value.
     *
     * The passed in [mgrPropId, areaId] is already checked and must be supported.
     *
     * @return The currently supported min/max value.
     * @throws ServiceSpecificException If VHAL returns error.
     * @throws IllegalArgumentException If the property ID is not valid or supported.
     */
    public MinMaxSupportedPropertyValue getMinMaxSupportedValue(int mgrPropId, int areaId,
            AreaIdConfig<?> areaIdConfig) {
        int halPropId = managerToHalPropId(mgrPropId);
        HalPropConfig halPropConfig;
        synchronized (mLock) {
            halPropConfig = mHalPropIdToPropConfig.get(halPropId);
            if (halPropConfig == null) {
                throw new IllegalArgumentException("Property ID: "
                        + VehiclePropertyIds.toString(mgrPropId) + " is not supported");
            }
        }

        if (!areaIdConfig.hasMinSupportedValue() && !areaIdConfig.hasMaxSupportedValue()) {
            Slogf.d(TAG, "No specified supported min/max for property: "
                    + VehiclePropertyIds.toString(mgrPropId) + ", areaId: "
                    + toAreaIdString(halPropId, areaId));
            return new MinMaxSupportedPropertyValue();
        }

        var returnValue = new MinMaxSupportedPropertyValue();
        if (mVehicleHal.isSupportedValuesImplemented(newPropIdAreaId(halPropId, areaId))) {
            MinMaxSupportedRawPropValues minMaxRawPropValues =
                    mVehicleHal.getMinMaxSupportedValue(halPropId, areaId);
            if (areaIdConfig.hasMinSupportedValue() && minMaxRawPropValues.minValue() != null) {
                returnValue.minValue.setParcelable(HalPropValue.toRawPropertyValue(
                        halPropId, areaId, mgrPropId, minMaxRawPropValues.minValue(),
                        halPropConfig));
            } else {
                Slogf.d(TAG, "No specified min supported value for property: "
                        + VehiclePropertyIds.toString(mgrPropId) + ", areaId: "
                        + toAreaIdString(halPropId, areaId));
            }
            if (areaIdConfig.hasMaxSupportedValue() && minMaxRawPropValues.maxValue() != null) {
                returnValue.maxValue.setParcelable(HalPropValue.toRawPropertyValue(
                        halPropId, areaId, mgrPropId, minMaxRawPropValues.maxValue(),
                        halPropConfig));
            } else {
                Slogf.d(TAG, "No specified max supported value for property: "
                        + VehiclePropertyIds.toString(mgrPropId) + ", areaId: "
                        + toAreaIdString(halPropId, areaId));
            }
            return returnValue;
        } else {
            if (halPropId == VehicleProperty.EV_CHARGE_CURRENT_DRAW_LIMIT) {
                // We use configArray[0] as the max value for EV_CHARGE_CURRENT_DRAW_LIMIT.
                var configArray = halPropConfig.getConfigArray();
                if (configArray.length > 0) {
                    // Note that EV_CHARGE_CURRENT_DRAW_LIMIT is float type.
                    returnValue.minValue.setParcelable(new RawPropertyValue(0.f));
                    returnValue.maxValue.setParcelable(new RawPropertyValue(
                            (float) configArray[0]));
                }
                return returnValue;
            }

            // If VHAL does not support value range, we use areaIdConfig.
            if (areaIdConfig.hasMinSupportedValue() && areaIdConfig.getMinValue() != null) {
                returnValue.minValue.setParcelable(new RawPropertyValue(
                        areaIdConfig.getMinValue()));
            } else {
                Slogf.d(TAG, "No specified min supported value for property: "
                        + VehiclePropertyIds.toString(mgrPropId) + ", areaId: "
                        + toAreaIdString(halPropId, areaId));
            }
            if (areaIdConfig.hasMaxSupportedValue() && areaIdConfig.getMaxValue() != null) {
                returnValue.maxValue.setParcelable(new RawPropertyValue(
                        areaIdConfig.getMaxValue()));
            } else {
                Slogf.d(TAG, "No specified max supported value for property: "
                        + VehiclePropertyIds.toString(mgrPropId) + ", areaId: "
                        + toAreaIdString(halPropId, areaId));
            }
            return returnValue;
        }
    }

    /**
     * Gets the currently supported values list.
     *
     * The passed in [mgrPropId, areaId] is already checked and must be supported.
     *
     * @return The currently supported values list or {@code null} if not specified.
     * @throws ServiceSpecificException If VHAL returns error.
     * @throws IllegalArgumentException If the property ID is not valid or supported.
     */
    @Nullable
    public List<RawPropertyValue> getSupportedValuesList(int mgrPropId, int areaId,
            AreaIdConfig<?> areaIdConfig) {
        int halPropId = managerToHalPropId(mgrPropId);
        HalPropConfig halPropConfig;
        synchronized (mLock) {
            halPropConfig = mHalPropIdToPropConfig.get(halPropId);
        }

        if (halPropConfig == null) {
            throw new IllegalArgumentException("Property ID: "
                    + VehiclePropertyIds.toString(mgrPropId) + " is not supported");
        }

        if (!areaIdConfig.hasSupportedValuesList()) {
            Slogf.d(TAG, "No specified supported values list for property: "
                    + VehiclePropertyIds.toString(mgrPropId) + ", areaId: "
                    + toAreaIdString(halPropId, areaId));
            return null;
        }

        if (mVehicleHal.isSupportedValuesImplemented(newPropIdAreaId(halPropId, areaId))) {
            List<RawPropValues> supportedRawPropValues = mVehicleHal.getSupportedValuesList(
                    halPropId, areaId);
            if (supportedRawPropValues == null) {
                Slogf.d(TAG, "No specified supported values list for property: "
                        + VehiclePropertyIds.toString(mgrPropId) + ", areaId: "
                        + toAreaIdString(halPropId, areaId));
                return null;
            }

            List<RawPropertyValue> supportedValuesList = new ArrayList<>();
            for (int i = 0; i < supportedRawPropValues.size(); i++) {
                var rawPropValues = supportedRawPropValues.get(i);
                var rawPropertyValue = HalPropValue.toRawPropertyValue(halPropId, areaId, mgrPropId,
                        rawPropValues, halPropConfig);
                if (rawPropertyValue != null) {
                    supportedValuesList.add(rawPropertyValue);
                }
            }
            return sortRawPropertyValueList(halPropId, supportedValuesList);
        } else {
            // If VHAL does not support value range, we try to get the supported values from
            // CarPropertyConfig or AreaIdConfig.
            List<RawPropertyValue> returnValues = new ArrayList<>();
            if (halPropId == VehicleProperty.HVAC_FAN_DIRECTION) {
                return getHvacFanDirectionSupportedValues(areaId);
            } else if (halPropId == VehicleProperty.HVAC_TEMPERATURE_SET) {
                // The config array for HVAC_TEMPERATURE_SET is defined as:
                // configArray[0] = [the lower bound of the supported temperature in Celsius] * 10.
                // configArray[1] = [the upper bound of the supported temperature in Celsius] * 10.
                // configArray[2] = [the increment in Celsius] * 10.
                // configArray[3] = [the lower bound of the supported temperature in Fahrenheit]
                // * 10.
                // configArray[4] = [the upper bound of the supported temperature in Fahrenheit]
                // * 10.
                // configArray[5] = [the increment in Fahrenheit] * 10.
                int[] configArray = halPropConfig.getConfigArray();
                if (configArray.length == HalPropConfig.HVAC_CONFIG_ARRAY_LENGTH) {
                    int celsiusLowerBound = configArray[0];
                    int celsiusUpperBound = configArray[1];
                    int step = configArray[2];
                    for (int temp = celsiusLowerBound; temp <= celsiusUpperBound; temp += step) {
                        // The celsiusUpperBound, celsiusUpperBound and step is Celsius temp * 10.
                        returnValues.add(new RawPropertyValue(temp / 10.f));
                    }
                } else {
                    return null;
                }
                // This is already sorted.
                return returnValues;
            }

            var annotations = AnnotationsForVehicleProperty.values.get(halPropId);
            if (annotations != null && annotations.contains(
                    HalPropConfig.ANNOTATION_SUPPORTED_VALUES_IN_CONFIG)) {
                // For certain properties, we use config array to represent supported values.
                int propertyType = halPropId & VehiclePropertyType.MASK;
                int[] configArray = halPropConfig.getConfigArray();
                if (configArray.length == 0) {
                    return null;
                }
                for (int i = 0; i < configArray.length; i++) {
                    int value = configArray[i];
                    if (propertyType == VehiclePropertyType.INT32) {
                        returnValues.add(new RawPropertyValue<Integer>(value));
                    } else if (propertyType == VehiclePropertyType.INT64) {
                        returnValues.add(new RawPropertyValue<Long>((long) value));
                    } else if (propertyType == VehiclePropertyType.FLOAT) {
                        returnValues.add(new RawPropertyValue<Float>((float) value));
                    } else {
                        Slogf.wtf(TAG,
                                "annotation: %s must only be used for INT32, INT64 or FLOAT "
                                + "property",
                                HalPropConfig.ANNOTATION_SUPPORTED_VALUES_IN_CONFIG);
                        break;
                    }
                }
                return sortRawPropertyValueList(halPropId, returnValues);
            }

            var supportedEnumValues = areaIdConfig.getSupportedEnumValues();
            if (supportedEnumValues.size() == 0) {
                return null;
            }
            for (int i = 0; i < supportedEnumValues.size(); i++) {
                returnValues.add(new RawPropertyValue(supportedEnumValues.get(i)));
            }
            return sortRawPropertyValueList(halPropId, returnValues);
        }
    }

    /**
     * Sorts the list of RawPropertyValue for int32/int64/float type in ascending order.
     */
    private static List<RawPropertyValue> sortRawPropertyValueList(int halPropId,
            List<RawPropertyValue> input) {
        int propertyType = halPropId & VehiclePropertyType.MASK;
        if (propertyType != VehiclePropertyType.INT32
                && propertyType != VehiclePropertyType.INT64
                && propertyType != VehiclePropertyType.FLOAT) {
            return input;
        }
        List<RawPropertyValue> output = new ArrayList<RawPropertyValue>(input);
        Collections.sort(output, (RawPropertyValue x, RawPropertyValue y) -> {
            if (x.getTypedValue() == null) {
                Slogf.e(TAG, "Invalid RawPropertyValue: " + x + ", no value");
                return -1;
            }
            if (y.getTypedValue() == null) {
                Slogf.e(TAG, "Invalid RawPropertyValue: " + y + ", no value");
                return 1;
            }

            switch (propertyType) {
                case VehiclePropertyType.INT32:
                    // Convert from int to float should not change whether it is positive.
                    return Integer.compare((Integer) x.getTypedValue(),
                            (Integer) y.getTypedValue());
                case VehiclePropertyType.INT64:
                    // Convert from int64 to float should not change whether it is positive.
                    return Long.compare((Long) x.getTypedValue(),
                            (Long) y.getTypedValue());
                case VehiclePropertyType.FLOAT:
                    return Float.compare((Float) x.getTypedValue(),
                            (Float) y.getTypedValue());
            }
            return 0;
        });
        return output;
    }

    /**
     * Registers the callback to be called when the min/max supported value or supported values
     * list change.
     *
     * @throws ServiceSpecificException If VHAL returns error.
     */
    public void registerSupportedValuesChangeCallback(List<PropIdAreaId> mgrPropIdAreaIds,
            ISupportedValuesChangeCallback callback) {
        List<PropIdAreaId> halPropIdAreaIds = new ArrayList<>();
        synchronized (mLock) {
            // This must be called within the lock so that unregisterSupportedValuesChangeCallback
            // is never called before this critical section returns.
            try {
                callback.asBinder().linkToDeath(new DeathRecipientWrapper(
                        callback.asBinder(), () -> {
                            unregisterSupportedValuesChangeCallback(callback);
                        }), /* flags= */ 0);
            } catch (RemoteException e) {
                // This will be returned back to the client (if possible).
                throw new IllegalStateException("Linking to binder death recipient failed, "
                        + "the client might already died", e);
            }

            for (int i = 0; i < mgrPropIdAreaIds.size(); i++) {
                var halPropIdAreaId = managerToHalPropIdAreaId(mgrPropIdAreaIds.get(i));
                if (!mVehicleHal.isSupportedValuesImplemented(halPropIdAreaId)) {
                    Slogf.i(TAG, "Do nothing for registerSupportedValuesChangeCallback for %s "
                            + "because VHAL does not implement dynamic supported values API for it",
                            toHalPropIdAreaIdString(halPropIdAreaId));
                    continue;
                }

                var registeredClients = mSupportedValuesChangeClientByPropIdAreaId.get(
                        mgrPropIdAreaIds.get(i));
                if (registeredClients == null) {
                    // [propId, areaId] was never registered before. Need to register to VHAL.
                    halPropIdAreaIds.add(halPropIdAreaId);
                }
            }

            if (!halPropIdAreaIds.isEmpty()) {
                // This is a binder call but we call this inside the lock to achieve state
                // consistency between car service and VHAL.
                mVehicleHal.registerSupportedValuesChange(this, halPropIdAreaIds);
            }

            for (int i = 0; i < mgrPropIdAreaIds.size(); i++) {
                var halPropIdAreaId = managerToHalPropIdAreaId(mgrPropIdAreaIds.get(i));
                if (!mVehicleHal.isSupportedValuesImplemented(halPropIdAreaId)) {
                    continue;
                }
                var registeredClients = mSupportedValuesChangeClientByPropIdAreaId.get(
                        mgrPropIdAreaIds.get(i));
                if (registeredClients == null) {
                    registeredClients = new ArraySet<SupportedValuesChangeClient>();
                }
                registeredClients.add(new SupportedValuesChangeClient(callback));
                mSupportedValuesChangeClientByPropIdAreaId.put(mgrPropIdAreaIds.get(i),
                        registeredClients);
            }
        }
    }

    /**
     * Unregisters the callback previously registered with registerSupportedValuesChangeCallback.
     *
     * Do nothing if the [propertyId, areaId]s were not previously registered.
     */
    public void unregisterSupportedValuesChangeCallback(List<PropIdAreaId> propIdAreaIds,
            ISupportedValuesChangeCallback callback) {
        List<PropIdAreaId> halPropIdAreaIdsToUnregister = new ArrayList<>();
        synchronized (mLock) {
            for (int i = 0; i < propIdAreaIds.size(); i++) {
                var propIdAreaId = propIdAreaIds.get(i);
                var halPropIdAreaId = managerToHalPropIdAreaId(propIdAreaId);
                if (!mVehicleHal.isSupportedValuesImplemented(halPropIdAreaId)) {
                    Slogf.i(TAG, "Do nothing for unregisterSupportedValuesChangeCallback for %s "
                            + "because VHAL does not implement dynamic supported values API for it",
                            toHalPropIdAreaIdString(halPropIdAreaId));
                    continue;
                }

                var registeredClients = mSupportedValuesChangeClientByPropIdAreaId.get(
                        propIdAreaId);
                if (registeredClients == null) {
                    continue;
                }
                registeredClients.remove(new SupportedValuesChangeClient(callback));
                if (!registeredClients.isEmpty()) {
                    // There are still callbacks registered for propIdAreaId, do not unregister
                    // from VehicleHal.
                    continue;
                }
                halPropIdAreaIdsToUnregister.add(managerToHalPropIdAreaId(propIdAreaId));
                mSupportedValuesChangeClientByPropIdAreaId.remove(propIdAreaId);
            }
            if (halPropIdAreaIdsToUnregister.isEmpty()) {
                return;
            }
            mVehicleHal.unregisterSupportedValuesChange(this, halPropIdAreaIdsToUnregister);
        }
    }

    private static class SupportedValuesChangeDispatchList extends
            DispatchList<ISupportedValuesChangeCallback, PropIdAreaId> {
        @Override
        protected void dispatchToClient(ISupportedValuesChangeCallback client,
                List<PropIdAreaId> events) {
            try {
                client.onSupportedValuesChange(events);
            } catch (RemoteException e) {
                Slogf.w(TAG, "onSupportedValuesChange: Client might have died already"
                        + ", ignore delivering supported values change event", e);
            }
        }
    }

    // Inherits from HalServiceBase
    @Override
    public void onSupportedValuesChange(List<PropIdAreaId> halPropIdAreaIds) {
        if (DBG) {
            Slogf.d(TAG, "onSupportedValuesChange called for: %s", toHalPropIdAreaIdsString(
                    halPropIdAreaIds));
        }
        var dispatchList = new SupportedValuesChangeDispatchList();

        synchronized (mLock) {
            for (int i = 0; i < halPropIdAreaIds.size(); i++) {
                PropIdAreaId propIdAreaId = halToManagerPropIdAreaId(halPropIdAreaIds.get(i));
                var clients = mSupportedValuesChangeClientByPropIdAreaId.get(propIdAreaId);
                if (clients == null) {
                    Slogf.w(TAG, "No registered clients for supported values change event for "
                            + toDebugString(propIdAreaId) + ", ignore");
                    continue;
                }
                for (int j = 0; j < clients.size(); j++) {
                    dispatchList.addEvent(clients.valueAt(j).getCallback(), propIdAreaId);
                }
            }
        }

        dispatchList.dispatchToClients();
    }

    private void unregisterSupportedValuesChangeCallback(ISupportedValuesChangeCallback callback) {
        synchronized (mLock) {
            List<PropIdAreaId> halPropIdAreaIdsToUnregister = new ArrayList<>();
            for (int i = 0; i < mSupportedValuesChangeClientByPropIdAreaId.size(); i++) {
                var clients = mSupportedValuesChangeClientByPropIdAreaId.valueAt(i);
                var propIdAreaId = mSupportedValuesChangeClientByPropIdAreaId.keyAt(i);
                clients.remove(new SupportedValuesChangeClient(callback));
                if (clients.size() == 0) {
                    halPropIdAreaIdsToUnregister.add(managerToHalPropIdAreaId(propIdAreaId));
                    mSupportedValuesChangeClientByPropIdAreaId.remove(propIdAreaId);
                }
            }
            if (halPropIdAreaIdsToUnregister.isEmpty()) {
                return;
            }
            mVehicleHal.unregisterSupportedValuesChange(this, halPropIdAreaIdsToUnregister);
        }
    }

    private PropIdAreaId managerToHalPropIdAreaId(PropIdAreaId managerPropIdAreaId) {
        return newPropIdAreaId(managerToHalPropId(managerPropIdAreaId.propId),
                managerPropIdAreaId.areaId);
    }

    private PropIdAreaId halToManagerPropIdAreaId(PropIdAreaId halPropIdAreaId) {
        return newPropIdAreaId(halToManagerPropId(halPropIdAreaId.propId),
                halPropIdAreaId.areaId);
    }

    private static void storeResultForRequest(GetSetValueResult result,
            AsyncPropRequestInfo request,
            Map<VehicleStubCallback, List<GetSetValueResultWrapper>> callbackToResults) {
        VehicleStubCallback clientCallback = request.getVehicleStubCallback();
        if (callbackToResults.get(clientCallback) == null) {
            callbackToResults.put(clientCallback, new ArrayList<>());
        }
        callbackToResults.get(clientCallback).add(new GetSetValueResultWrapper(result,
                request.getAsyncRequestStartTime(), request.getRetryCount(), request.getAreaId()));
    }

    /**
     * Check whether there is pending async set value request for the property.
     *
     * If there are pending async set value request, check whether the updated property value is
     * the target value. If so, store the success set value result into callbackToSetValueResults.
     */
    @GuardedBy("mLock")
    private void checkPendingWaitForUpdateRequestsLocked(int halPropId,
            CarPropertyValue<?> updatedValue,
            Map<VehicleStubCallback, List<GetSetValueResultWrapper>> callbackToSetValueResults) {
        List<AsyncPropRequestInfo> pendingSetRequests = mHalPropIdToWaitingUpdateRequestInfo.get(
                halPropId);
        if (pendingSetRequests == null) {
            return;
        }
        List<AsyncPropRequestInfo> finishedPendingSetRequests = new ArrayList<>();
        for (AsyncPropRequestInfo pendingSetRequest : pendingSetRequests) {
            GetSetValueResult maybeSetResult = maybeFinishPendingSetValueRequestLocked(
                    pendingSetRequest, updatedValue);
            if (pendingSetRequest.getAreaId() != updatedValue.getAreaId()) {
                continue;
            }
            // Don't remove the finished pending request info during the loop since it will
            // modify pendingSetRequests array.
            if (maybeSetResult == null) {
                if (DBG) {
                    Slogf.d(TAG, "received property update event for request: %s, but the value is "
                            + "different than target value", pendingSetRequest);
                }
                continue;
            }
            if (DBG) {
                Slogf.d(TAG, "received property update to target value event for request: %s"
                        + ", sending success async set value result", pendingSetRequest);
            }
            storeResultForRequest(maybeSetResult, pendingSetRequest, callbackToSetValueResults);
            finishedPendingSetRequests.add(pendingSetRequest);
        }

        for (AsyncPropRequestInfo finishedRequest : finishedPendingSetRequests) {
            // Pending set value request is now succeeded. Remove all record to the pending request.
            removePendingAsyncPropRequestInfoLocked(finishedRequest);
        }
    }

    private static ArrayList<HalSubscribeOptions> toHalSubscribeOptions(
            ArrayList<CarSubscription> carSubscriptions) {
        ArrayList<HalSubscribeOptions> halOptions = new ArrayList<>();
        for (int i = 0; i < carSubscriptions.size(); i++) {
            CarSubscription carOption = carSubscriptions.get(i);
            halOptions.add(new HalSubscribeOptions(carOption.propertyId, carOption.areaIds,
                    carOption.updateRateHz, carOption.enableVariableUpdateRate,
                    carOption.resolution));
        }
        return halOptions;
    }

    /**
     * Apply the staged subscription rate in {@code mSubManager} to VHAL.
     *
     * Use {@code subscribeProperty} to update its subscription rate or {@code unsubscribeProperty}
     * if it is no longer subscribed.
     *
     * This functions involves binder call to VHAL, but we intentionally keep this inside the
     * lock because we need to keep the subscription status consistent. If we do not use lock
     * here, the following situation might happen:
     *
     * <ol>
     * <li>Lock is obtained by thread 1.
     * <li>mSubManager is updated by one thread to state 1.
     * <li>New update rate (local variable) is calculated based on state 1.
     * <li>Lock is released by thread 1.
     * <li>Lock is obtained by thread 2.
     * <li>mSubManager is updated by thread 2 to state 2.
     * <li>New update rate (local variable) is calculated based on state 2.
     * <li>Lock is released by thread 2.
     * <li>Thread 2 calls subscribeProperty to VHAL based on state 2.
     * <li>Thread 1 calls subscribeProperty to VHAL based on state 1.
     * <li>Now internally, the state is in state 2, but from VHAL side, it is in state 1.
     */
    @GuardedBy("mLock")
    private void updateSubscriptionRateLocked() throws ServiceSpecificException {
        ArrayList<CarSubscription> diffSubscribeOptions = new ArrayList<>();
        List<Integer> propIdsToUnsubscribe = new ArrayList<>();
        mSubManager.diffBetweenCurrentAndStage(diffSubscribeOptions, propIdsToUnsubscribe);
        try {
            if (!diffSubscribeOptions.isEmpty()) {
                if (DBG) {
                    Slogf.d(TAG, "subscribeProperty, options: %s", diffSubscribeOptions);
                }
                // This may throw ServiceSpecificException.
                mVehicleHal.subscribeProperty(this, toHalSubscribeOptions(diffSubscribeOptions));
            }
            for (int halPropId : propIdsToUnsubscribe) {
                if (DBG) {
                    Slogf.d(TAG, "unsubscribeProperty for property ID: %s",
                            halPropIdToName(halPropId));
                }
                // This may throw ServiceSpecificException.
                mVehicleHal.unsubscribeProperty(this, halPropId);
            }
            mSubManager.commit();
        } catch (IllegalArgumentException e) {
            Slogf.e(TAG, "Failed to subscribe/unsubscribe, property is not supported, this should "
                    + "not happen, caller must make sure the property is supported", e);
            mSubManager.dropCommit();
            return;
        } catch (Exception e) {
            mSubManager.dropCommit();
            throw e;
        }
    }

    @GuardedBy("mLock")
    private void updateSubscriptionRateForAsyncSetRequestLocked() {
        try {
            updateSubscriptionRateLocked();
        } catch (ServiceSpecificException e) {
            Slogf.e(TAG, "failed to update subscription rate after we finish async set request", e);
            return;
        }
    }

    @Override
    public void onHalEvents(List<HalPropValue> halPropValues) {

        List<CarPropertyEvent> eventsToDispatch = new ArrayList<>();

        // A map to store potential succeeded set value results which is caused by the values
        // updated to the target values.
        Map<VehicleStubCallback, List<GetSetValueResultWrapper>> callbackToSetValueResults =
                new ArrayMap<>();

        synchronized (mLock) {
            for (HalPropValue halPropValue : halPropValues) {
                if (halPropValue == null) {
                    continue;
                }
                int halPropId = halPropValue.getPropId();
                HalPropConfig halPropConfig = mHalPropIdToPropConfig.get(halPropId);
                if (halPropConfig == null) {
                    Slogf.w(TAG, "onHalEvents - received HalPropValue for unsupported property: %s",
                            halPropIdToName(halPropId));
                    continue;
                }
                // Check payload if it is an userdebug build.
                if (BuildHelper.isDebuggableBuild()
                        && !mPropertyHalServiceConfigs.checkPayload(halPropValue)) {
                    Slogf.wtf(TAG,
                            "Drop event for property: %s because it is failed "
                                    + "in payload checking.", halPropValue);
                }
                int mgrPropId = halToManagerPropId(halPropId);
                if (DBG && halPropValue.getStatus() != VehiclePropertyStatus.AVAILABLE) {
                    Slogf.d(TAG, "Received event %s with status that is not AVAILABLE",
                            halPropValue);
                }
                try {
                    CarPropertyValue<?> carPropertyValue = halPropValue.toCarPropertyValue(
                            mgrPropId, halPropConfig);
                    CarPropertyEvent carPropertyEvent = new CarPropertyEvent(
                            CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, carPropertyValue);
                    eventsToDispatch.add(carPropertyEvent);

                    checkPendingWaitForUpdateRequestsLocked(halPropId, carPropertyValue,
                            callbackToSetValueResults);
                } catch (IllegalStateException e) {
                    Slogf.w(TAG, "Drop event %s that does not have valid value", halPropValue);
                    continue;
                }
            }
            updateSubscriptionRateForAsyncSetRequestLocked();
        }

        PropertyHalListener propertyHalListener;
        synchronized (mLock) {
            propertyHalListener = mPropertyHalListener;
        }
        if (propertyHalListener != null) {
            propertyHalListener.onPropertyChange(eventsToDispatch);
        }

        for (VehicleStubCallback callback : callbackToSetValueResults.keySet()) {
            callback.sendSetValueResults(callbackToSetValueResults.get(callback));
        }
    }

    /**
     * Registers a recording listener.
     *
     * @param callback The callback to register
     * @return A list of CarPropertyConfigs that are being recorded
     */
    public List<CarPropertyConfig> registerRecordingListener(ICarPropertyEventListener callback) {
        List<HalPropConfig> allHalPropConfigs = mVehicleHal.registerRecordingListener(callback);
        List<CarPropertyConfig> allCarPropertyConfigs = new ArrayList<>();
        for (int i = 0; i < allHalPropConfigs.size(); i++) {
            HalPropConfig halPropConfig = allHalPropConfigs.get(i);
            allCarPropertyConfigs.add(halPropConfig.toCarPropertyConfig(halPropConfig.getPropId(),
                    mPropertyHalServiceConfigs, /* isVhalPropId= */ true));
        }
        return allCarPropertyConfigs;
    }

    /**
     * @return True If currently recording vehicle properties
     */
    public boolean isRecordingVehicleProperties() {
        return mVehicleHal.isRecordingVehicleProperties();
    }

    /**
     * Stops the recording.
     * @param callback The callback to stop recording.
     */
    public void stopRecordingVehicleProperties(ICarPropertyEventListener callback) {
        mVehicleHal.stopRecordingVehicleProperties(callback);
    }

    /**
     * Enables Injection mode with the list of properties to allow to come from the real VHAL.
     * @param propertyIdsFromRealHardware THe list of properties to allow to come from real VHAL.
     */
    public long enableInjectionMode(List<Integer> propertyIdsFromRealHardware) {
        return mVehicleHal.enableInjectionMode(propertyIdsFromRealHardware);
    }

    /**
     * Disables injeciton mode.
     */
    public void disableInjectionMode() {
        mVehicleHal.disableInjectionMode();
    }

    /**
     * @return True if Vehicle property injection mode is enabled, false otherwise.
     */
    public boolean isVehiclePropertyInjectionModeEnabled() {
        return mVehicleHal.isVehiclePropertyInjectionModeEnabled();
    }

    /**
     * Gets the last injected vehicle property for the propertyId.
     *
     * @param propertyId The propertyId that was last injected.
     * @return The {@link CarPropertyValue} that was last injected.
     */
    @Nullable
    public CarPropertyValue getLastInjectedVehicleProperty(int propertyId) {
        return mVehicleHal.getLastInjectedVehicleProperty(propertyId);
    }

    /**
     * Injects the CarPropertyValues.
     * @param carPropertyValues The carPropertyValues to inject.
     */
    public void injectVehicleProperties(List<CarPropertyValue> carPropertyValues) {
        mVehicleHal.injectVehicleProperties(carPropertyValues);
    }

    private static @CarSetPropertyErrorCode int convertStatusCodeToCarSetPropertyErrorCode(
            @VehicleHalStatusCodeInt int vhalStatusCode) {
        switch (vhalStatusCode) {
            case STATUS_TRY_AGAIN:
                return CAR_SET_PROPERTY_ERROR_CODE_TRY_AGAIN;
            case STATUS_INVALID_ARG:
                return CAR_SET_PROPERTY_ERROR_CODE_INVALID_ARG;
            case STATUS_NOT_AVAILABLE: // fallthrough
            case STATUS_NOT_AVAILABLE_DISABLED: // fallthrough
            case STATUS_NOT_AVAILABLE_SPEED_LOW: // fallthrough
            case STATUS_NOT_AVAILABLE_SPEED_HIGH: // fallthrough
            case STATUS_NOT_AVAILABLE_POOR_VISIBILITY: // fallthrough
            case STATUS_NOT_AVAILABLE_SAFETY:
                return CAR_SET_PROPERTY_ERROR_CODE_PROPERTY_NOT_AVAILABLE;
            case STATUS_ACCESS_DENIED:
                return CAR_SET_PROPERTY_ERROR_CODE_ACCESS_DENIED;
            default:
                return CAR_SET_PROPERTY_ERROR_CODE_UNKNOWN;
        }
    }

    @Override
    public void onPropertySetError(ArrayList<VehiclePropError> vehiclePropErrors) {
        PropertyHalListener propertyHalListener;
        synchronized (mLock) {
            propertyHalListener = mPropertyHalListener;
        }
        if (propertyHalListener != null) {
            for (int i = 0; i < vehiclePropErrors.size(); i++) {
                VehiclePropError vehiclePropError = vehiclePropErrors.get(i);
                int mgrPropId = halToManagerPropId(vehiclePropError.propId);
                int vhalErrorCode = CarPropertyErrorCodes.getVhalSystemErrorCode(
                        vehiclePropError.errorCode);
                Slogf.w(TAG,
                        "onPropertySetError for property: %s, area ID: %d, vhal error code: %d",
                        VehiclePropertyIds.toString(mgrPropId), vehiclePropError.areaId,
                        vhalErrorCode);
                @CarSetPropertyErrorCode int carPropErrorCode =
                        convertStatusCodeToCarSetPropertyErrorCode(vhalErrorCode);
                propertyHalListener.onPropertySetError(mgrPropId, vehiclePropError.areaId,
                        carPropErrorCode);
            }
        }
        Map<VehicleStubCallback, List<GetSetValueResultWrapper>> callbackToSetValueResults =
                new ArrayMap<>();
        synchronized (mLock) {
            for (int i = 0; i < vehiclePropErrors.size(); i++) {
                VehiclePropError vehiclePropError = vehiclePropErrors.get(i);
                // Fail all pending async set requests that are currently waiting for property
                // update which has the same property ID and same area ID.
                int halPropId = vehiclePropError.propId;
                List<AsyncPropRequestInfo> pendingSetRequests =
                        mHalPropIdToWaitingUpdateRequestInfo.get(halPropId);
                if (pendingSetRequests == null) {
                    continue;
                }
                for (int j = 0; j < pendingSetRequests.size(); j++) {
                    AsyncPropRequestInfo pendingRequest = pendingSetRequests.get(j);
                    if (pendingRequest.getAreaId() != vehiclePropError.areaId) {
                        continue;
                    }
                    removePendingAsyncPropRequestInfoLocked(pendingRequest);
                    CarPropertyErrorCodes carPropertyErrorCodes =
                            createFromVhalStatusCode(vehiclePropError.errorCode);
                    GetSetValueResult errorResult = pendingRequest.toErrorResult(
                            carPropertyErrorCodes);
                    Slogf.w(TAG, "Pending async set request received property set error with "
                            + "error: %s, fail the pending request: %s",
                            carPropertyErrorCodes, pendingRequest);
                    storeResultForRequest(errorResult, pendingRequest, callbackToSetValueResults);
                }
            }
            updateSubscriptionRateForAsyncSetRequestLocked();
        }
        for (VehicleStubCallback callback : callbackToSetValueResults.keySet()) {
            callback.sendSetValueResults(callbackToSetValueResults.get(callback));
        }
    }

    @Override
    @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
    public void dump(PrintWriter writer) {
        writer.println(TAG);
        writer.println("  Properties available:");
        synchronized (mLock) {
            for (int i = 0; i < mHalPropIdToPropConfig.size(); i++) {
                HalPropConfig halPropConfig = mHalPropIdToPropConfig.valueAt(i);
                writer.println("    " + halPropConfig);
            }
            mSubManager.dump(new IndentingPrintWriter(writer));
        }
    }

    private List<AsyncGetSetRequest> prepareVehicleStubRequests(@AsyncRequestType int requestType,
            List<AsyncPropertyServiceRequest> serviceRequests,
            long timeoutInMs,
            VehicleStubCallback vehicleStubCallback,
            @Nullable List<AsyncPropRequestInfo> assocSetValueRequestInfo,
            @Nullable List<AsyncPropRequestInfo> outRequestInfo, long asyncRequestStartTime) {
        // TODO(b/242326085): Change local variables into memory pool to reduce memory
        //  allocation/release cycle
        List<AsyncGetSetRequest> vehicleStubRequests = new ArrayList<>();
        List<AsyncPropRequestInfo> pendingRequestInfo = new ArrayList<>();
        List<GetSetValueResultWrapper> staticGetValueResults = new ArrayList<>();
        long nowUptimeMs = SystemClock.uptimeMillis();
        synchronized (mLock) {
            for (int i = 0; i < serviceRequests.size(); i++) {
                AsyncPropertyServiceRequest serviceRequest = serviceRequests.get(i);
                int propertyId = serviceRequest.getPropertyId();
                int areaId = serviceRequest.getAreaId();
                if (requestType == GET
                        && mStaticPropertyIdAreaIdCache.get(propertyId, areaId) != null) {
                    if (DBG) {
                        Slogf.d(TAG, "Get Async property: %s retrieved from cache",
                                VehiclePropertyIds.toString(propertyId));
                    }
                    staticGetValueResults.add(new GetSetValueResultWrapper(newGetValueResult(
                            serviceRequest.getRequestId(), mStaticPropertyIdAreaIdCache.get(
                                    propertyId, areaId)), asyncRequestStartTime,
                            /* retryCount= */ 0, propertyId));
                    continue;
                }
                AsyncPropRequestInfo pendingRequest = new AsyncPropRequestInfo(requestType,
                        serviceRequest, nowUptimeMs + timeoutInMs, vehicleStubCallback,
                        asyncRequestStartTime);
                if (assocSetValueRequestInfo != null) {
                    // Link the async set value request and the get init value request together.
                    pendingRequest.setAssocSetValueRequestInfo(assocSetValueRequestInfo.get(i));
                    assocSetValueRequestInfo.get(i).setAssocGetInitValueRequestInfo(pendingRequest);
                }
                AsyncGetSetRequest vehicleStubRequest = generateVehicleStubAsyncRequestLocked(
                        pendingRequest);
                vehicleStubRequests.add(vehicleStubRequest);
                pendingRequestInfo.add(pendingRequest);
                if (outRequestInfo != null) {
                    outRequestInfo.add(pendingRequest);
                }
            }
            mPendingAsyncRequests.addPendingRequests(pendingRequestInfo);
        }
        if (!staticGetValueResults.isEmpty()) {
            vehicleStubCallback.sendGetValueResults(staticGetValueResults);
        }
        return vehicleStubRequests;
    }

    VehicleStubCallback createVehicleStubCallback(
            IAsyncPropertyResultCallback asyncPropertyResultCallback) {
        IBinder asyncPropertyResultBinder = asyncPropertyResultCallback.asBinder();
        VehicleStubCallback callback;
        synchronized (mLock) {
            if (mResultBinderToVehicleStubCallback.get(asyncPropertyResultBinder) == null) {
                callback = new VehicleStubCallback(asyncPropertyResultCallback);
                try {
                    callback.linkToDeath(() -> onBinderDied(asyncPropertyResultBinder));
                } catch (RemoteException e) {
                    throw new IllegalStateException("Linking to binder death recipient failed, "
                            + "the client might already died", e);
                }
                mResultBinderToVehicleStubCallback.put(asyncPropertyResultBinder, callback);
            } else {
                callback = mResultBinderToVehicleStubCallback.get(asyncPropertyResultBinder);
            }
        }
        return callback;
    }

    private void sendVehicleStubRequests(@AsyncRequestType int requestType,
            List<AsyncGetSetRequest> vehicleStubRequests, VehicleStubCallback callback) {
        switch (requestType) {
            case GET: // fallthrough
            case GET_INITIAL_VALUE_FOR_SET:
                mVehicleHal.getAsync(vehicleStubRequests, callback);
                break;
            case SET:
                mVehicleHal.setAsync(vehicleStubRequests, callback);
                break;
        }
    }

    /**
     * Queries CarPropertyValue with list of AsyncPropertyServiceRequest objects.
     *
     * <p>This method gets the CarPropertyValue using async methods. </p>
     */
    public void getCarPropertyValuesAsync(
            List<AsyncPropertyServiceRequest> serviceRequests,
            IAsyncPropertyResultCallback asyncPropertyResultCallback,
            long timeoutInMs, long asyncRequestStartTime) {
        VehicleStubCallback vehicleStubCallback = createVehicleStubCallback(
                asyncPropertyResultCallback);
        List<AsyncGetSetRequest> vehicleStubRequests = prepareVehicleStubRequests(
                GET, serviceRequests, timeoutInMs, vehicleStubCallback,
                /* assocSetValueRequestInfo= */ null, /* outRequestInfo= */ null,
                asyncRequestStartTime);
        if (vehicleStubRequests.isEmpty()) {
            return;
        }
        sendVehicleStubRequests(GET, vehicleStubRequests, vehicleStubCallback);
    }

    private <T> List<T> filterWaitForUpdateRequests(List<T> requests,
            Function<T, Boolean> isWaitForPropertyUpdate) {
        List<T> waitForUpdateSetRequests = new ArrayList<>();
        for (int i = 0; i < requests.size(); i++) {
            if (isWaitForPropertyUpdate.apply(requests.get(i))) {
                waitForUpdateSetRequests.add(requests.get(i));
            }
        }
        return waitForUpdateSetRequests;
    }

    private static CarSubscription newCarSubscription(int propertyId, int[] areaIds,
                                                      float updateRateHz, boolean enableVur) {
        return newCarSubscription(propertyId, areaIds, updateRateHz, enableVur,
                /*resolution*/ 0.0f);
    }

    private static CarSubscription newCarSubscription(int propertyId, int[] areaIds,
            float updateRateHz, boolean enableVur, float resolution) {
        CarSubscription option = new CarSubscription();
        option.propertyId = propertyId;
        option.areaIds = areaIds;
        option.updateRateHz = updateRateHz;
        option.enableVariableUpdateRate = enableVur;
        option.resolution = resolution;
        return option;
    }

    /**
     * For every pending async set request that needs to wait for property update, generates an
     * async get initial value request and subscribes to the property update event.
     */
    private void sendGetInitialValueAndSubscribeUpdateEvent(
            List<AsyncPropertyServiceRequest> serviceRequests,
            VehicleStubCallback vehicleStubCallback, long timeoutInMs,
            List<AsyncPropRequestInfo> waitForUpdateSetRequestInfo, long asyncRequestStartTime) {
        // Stores a list of async GET_INITIAL_VALUE request to be sent.
        List<AsyncGetSetRequest> getInitValueRequests = prepareVehicleStubRequests(
                GET_INITIAL_VALUE_FOR_SET, serviceRequests, timeoutInMs,
                vehicleStubCallback, /* assocSetValueRequestInfo= */ waitForUpdateSetRequestInfo,
                /* outRequestInfo= */ null, asyncRequestStartTime);

        // Subscribe to the property's change events before setting the property.
        synchronized (mLock) {
            for (AsyncPropRequestInfo setRequestInfo : waitForUpdateSetRequestInfo) {
                int halPropId = managerToHalPropId(setRequestInfo.getPropertyId());
                // We already checked in {@code carPropertyValueToHalPropValueLocked} inside
                // {@code prepareVehicleStubRequests}, this is guaranteed not to be null.
                HalPropConfig halPropConfig = mHalPropIdToPropConfig.get(halPropId);

                setRequestInfo.parseClientUpdateRateHz(halPropConfig.toCarPropertyConfig(
                        setRequestInfo.getPropertyId(), mPropertyHalServiceConfigs));

                if (mHalPropIdToWaitingUpdateRequestInfo.get(halPropId) == null) {
                    mHalPropIdToWaitingUpdateRequestInfo.put(halPropId, new ArrayList<>());
                }
                mHalPropIdToWaitingUpdateRequestInfo.get(halPropId).add(setRequestInfo);
                // Internally subscribe to the propId, areaId for property update events.
                // We use the pending async service request ID as client key.
                // Enable VUR for continuous since we only want to know when the value is updated.
                boolean enableVur = (halPropConfig.getChangeMode()
                        == CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_CONTINUOUS);
                mSubManager.stageNewOptions(new ClientType(setRequestInfo.getServiceRequestId()),
                        // Note that we use halPropId instead of mgrPropId in mSubManager.
                        List.of(newCarSubscription(halPropId,
                                new int[]{setRequestInfo.getAreaId()},
                                setRequestInfo.getUpdateRateHz(), enableVur)));
            }
            try {
                updateSubscriptionRateLocked();
            } catch (ServiceSpecificException e) {
                Slogf.e(TAG, "failed to update subscription rate after we start a new async set "
                        + "request, the request will likely time-out", e);
            }
        }

        sendVehicleStubRequests(GET_INITIAL_VALUE_FOR_SET, getInitValueRequests,
                vehicleStubCallback);
    }

    /**
     * Sets car property values asynchronously.
     */
    public void setCarPropertyValuesAsync(
            List<AsyncPropertyServiceRequest> serviceRequests,
            IAsyncPropertyResultCallback asyncPropertyResultCallback,
            long timeoutInMs, long asyncRequestStartTime) {
        for (int i = 0; i < serviceRequests.size(); i++) {
            Trace.asyncTraceBegin(TRACE_TAG, "setCarPropertyValuesAsync-propertyId: "
                    + serviceRequests.get(i).getPropertyId(),
                    /* cookie= */ serviceRequests.get(i).getRequestId());
        }
        List<AsyncPropRequestInfo> pendingSetRequestInfo = new ArrayList<>();
        VehicleStubCallback vehicleStubCallback = createVehicleStubCallback(
                asyncPropertyResultCallback);
        List<AsyncGetSetRequest> setValueRequests = prepareVehicleStubRequests(
                SET, serviceRequests, timeoutInMs, vehicleStubCallback,
                 /* assocSetValueRequestInfo= */ null, /* outRequestInfo= */ pendingSetRequestInfo,
                asyncRequestStartTime);
        List<AsyncPropRequestInfo> waitForUpdateSetRequestInfo = filterWaitForUpdateRequests(
                pendingSetRequestInfo, (request) -> request.isWaitForPropertyUpdate());

        if (waitForUpdateSetRequestInfo.size() != 0) {
            List<AsyncPropertyServiceRequest> waitForUpdateServiceRequests =
                    filterWaitForUpdateRequests(serviceRequests,
                            (request) -> request.isWaitForPropertyUpdate());
            sendGetInitialValueAndSubscribeUpdateEvent(waitForUpdateServiceRequests,
                    vehicleStubCallback, timeoutInMs, waitForUpdateSetRequestInfo,
                    asyncRequestStartTime);
        }

        sendVehicleStubRequests(SET, setValueRequests, vehicleStubCallback);
    }

    /**
     * Maps managerRequestIds to serviceRequestIds and remove them from the pending request map.
     */
    public void cancelRequests(int[] managerRequestIds) {
        List<Integer> serviceRequestIdsToCancel = new ArrayList<>();
        Set<Integer> managerRequestIdsSet = CarServiceUtils.toIntArraySet(managerRequestIds);
        synchronized (mLock) {
            for (int i = 0; i < mPendingAsyncRequests.size(); i++) {
                // For GET_INITIAL_VALUE request, they have the same manager request ID as their
                // associated async set request. While cancelling the async set request, they will
                // be cancelled as well, see {@link cleanupPendingAsyncSetRequestLocked}, so no need
                // to cancel them here.
                AsyncPropRequestInfo propRequestInfo = mPendingAsyncRequests.valueAt(i);
                if (managerRequestIdsSet.contains(propRequestInfo.getManagerRequestId())
                        && propRequestInfo.getRequestType() != GET_INITIAL_VALUE_FOR_SET) {
                    serviceRequestIdsToCancel.add((int) mPendingAsyncRequests.keyAt(i));
                }
            }
            cancelRequestsByServiceRequestIdsLocked(serviceRequestIdsToCancel);
        }
        if (!serviceRequestIdsToCancel.isEmpty()) {
            mVehicleHal.cancelRequests(serviceRequestIdsToCancel);
        }
    }

    private void onBinderDied(IBinder binder) {
        List<Integer> serviceRequestIdsToCancel = new ArrayList<>();
        synchronized (mLock) {
            mResultBinderToVehicleStubCallback.remove(binder);
            for (int i = 0; i < mPendingAsyncRequests.size(); i++) {
                AsyncPropRequestInfo clientRequestInfo = mPendingAsyncRequests.valueAt(i);
                if (clientRequestInfo.getVehicleStubCallback().getClientBinder() != binder) {
                    continue;
                }
                serviceRequestIdsToCancel.add((int) mPendingAsyncRequests.keyAt(i));
            }
            cancelRequestsByServiceRequestIdsLocked(serviceRequestIdsToCancel);
        }
        if (!serviceRequestIdsToCancel.isEmpty()) {
            mVehicleHal.cancelRequests(serviceRequestIdsToCancel);
        }
    }

    @GuardedBy("mLock")
    private void cancelRequestsByServiceRequestIdsLocked(List<Integer> serviceRequestIdsToCancel) {
        if (serviceRequestIdsToCancel.isEmpty()) {
            return;
        }
        for (int i = 0; i < serviceRequestIdsToCancel.size(); i++) {
            Slogf.w(TAG, "the request for propertyHalService request ID: %d is cancelled",
                    serviceRequestIdsToCancel.get(i));
            getAndRemovePendingAsyncPropRequestInfoLocked(serviceRequestIdsToCancel.get(i));
        }
        try {
            updateSubscriptionRateLocked();
        } catch (ServiceSpecificException e) {
            Slogf.e(TAG, " failed to update subscription rate when an async set request is "
                    + "cancelled", e);
        }
    }

    @GuardedBy("mLock")
    private HalPropValue carPropertyValueToHalPropValueLocked(CarPropertyValue carPropertyValue) {
        int mgrPropId = carPropertyValue.getPropertyId();
        int halPropId = managerToHalPropId(mgrPropId);
        HalPropConfig halPropConfig = mHalPropIdToPropConfig.get(halPropId);
        if (halPropConfig == null) {
            throw new IllegalArgumentException("Property ID: " + mgrPropId + " is not supported");
        }
        return mPropValueBuilder.build(carPropertyValue, halPropId, halPropConfig);
    }

    /**
     * Get the pending async requests size.
     *
     * For test only.
     */
    public int countPendingAsyncRequests() {
        synchronized (mLock) {
            return mPendingAsyncRequests.size();
        }
    }

    /**
     * Get the size of the map from hal prop ID to pending async set value requests.
     *
     * For test only.
     */
    public int countHalPropIdToWaitForUpdateRequests() {
        synchronized (mLock) {
            return mHalPropIdToWaitingUpdateRequestInfo.size();
        }
    }

    /**
     * Counts the number of supported values change clients.
     *
     * For test only.
     */
    @VisibleForTesting
    public int countSupportedValuesChangeClient() {
        ArraySet<SupportedValuesChangeClient> clients = new ArraySet<>();
        synchronized (mLock) {
            for (int i = 0; i < mSupportedValuesChangeClientByPropIdAreaId.size(); i++) {
                clients.addAll(mSupportedValuesChangeClientByPropIdAreaId.valueAt(i));
            }
        }
        return clients.size();
    }

    private static String requestTypeToString(@AsyncRequestType int requestType) {
        switch (requestType) {
            case GET:
                return "GET";
            case SET:
                return "SET";
            case GET_INITIAL_VALUE_FOR_SET:
                return "GET_INITIAL_VALUE_FOR_SET";
            default:
                return "UNKNOWN";
        }
    }

    @GuardedBy("mLock")
    private boolean isStaticAndSystemPropertyLocked(int propertyId) {
        return mHalPropIdToPropConfig.get(managerToHalPropId(propertyId))
                .getChangeMode() == VEHICLE_PROPERTY_CHANGE_MODE_STATIC
                && isSystemProperty(propertyId);
    }

    private int managerToHalPropId(int mgrPropId) {
        return mPropertyHalServiceConfigs.managerToHalPropId(mgrPropId);
    }

    private int halToManagerPropId(int mgrPropId) {
        return mPropertyHalServiceConfigs.halToManagerPropId(mgrPropId);
    }

    private String halPropIdToName(int halPropId) {
        return mPropertyHalServiceConfigs.halPropIdToName(halPropId);
    }

    private @Nullable List<RawPropertyValue> getHvacFanDirectionSupportedValues(int areaId) {
        List<RawPropertyValue> supportedValues = new ArrayList<>();
        try {
            // Since HVAC_FAN_DIRECTION_AVAILABLE, the value should be cached here.
            var hvacFanDirectionAvailable =
                    (CarPropertyValue<Integer[]>) getProperty(
                            HVAC_FAN_DIRECTION_AVAILABLE, areaId);
            var status = hvacFanDirectionAvailable.getPropertyStatus();
            if (status != STATUS_AVAILABLE) {
                throw new IllegalStateException(
                        "HVAC_FAN_DIRECTION_AVAILABLE property status is not available, status: "
                        + status);
            }
            Integer[] availableDirections = hvacFanDirectionAvailable.getValue();
            for (int availableDirection : availableDirections) {
                supportedValues.add(new RawPropertyValue(availableDirection));
            }
        } catch (Exception e) {
            Slogf.e(TAG, "Failed to get property: "
                    + VehiclePropertyIds.toString(HVAC_FAN_DIRECTION_AVAILABLE)
                    + ", areaId: "
                    + toAreaIdString(VehicleProperty.HVAC_FAN_DIRECTION_AVAILABLE, areaId),
                    e);
            return null;
        }
        return sortRawPropertyValueList(VehicleProperty.HVAC_FAN_DIRECTION,
                supportedValues);
    }
}
