/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.car;

import static com.android.car.internal.property.CarPropertyErrorCodes.convertVhalStatusCodeToCarPropertyManagerErrorCodes;

import android.annotation.Nullable;
import android.car.builtin.os.ServiceManagerHelper;
import android.car.builtin.os.TraceHelper;
import android.car.builtin.util.Slogf;
import android.car.hardware.property.CarPropertyManager;
import android.car.util.concurrent.AndroidFuture;
import android.hardware.automotive.vehicle.GetValueRequest;
import android.hardware.automotive.vehicle.GetValueRequests;
import android.hardware.automotive.vehicle.GetValueResult;
import android.hardware.automotive.vehicle.GetValueResults;
import android.hardware.automotive.vehicle.IVehicle;
import android.hardware.automotive.vehicle.IVehicleCallback;
import android.hardware.automotive.vehicle.SetValueRequest;
import android.hardware.automotive.vehicle.SetValueRequests;
import android.hardware.automotive.vehicle.SetValueResult;
import android.hardware.automotive.vehicle.SetValueResults;
import android.hardware.automotive.vehicle.StatusCode;
import android.hardware.automotive.vehicle.SubscribeOptions;
import android.hardware.automotive.vehicle.VehiclePropConfig;
import android.hardware.automotive.vehicle.VehiclePropConfigs;
import android.hardware.automotive.vehicle.VehiclePropError;
import android.hardware.automotive.vehicle.VehiclePropErrors;
import android.hardware.automotive.vehicle.VehiclePropValue;
import android.hardware.automotive.vehicle.VehiclePropValues;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.os.Trace;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.LongSparseArray;

import com.android.car.hal.AidlHalPropConfig;
import com.android.car.hal.HalPropConfig;
import com.android.car.hal.HalPropValue;
import com.android.car.hal.HalPropValueBuilder;
import com.android.car.hal.VehicleHalCallback;
import com.android.car.internal.LargeParcelable;
import com.android.car.internal.LongPendingRequestPool;
import com.android.car.internal.LongPendingRequestPool.TimeoutCallback;
import com.android.car.internal.LongRequestIdWithTimeout;
import com.android.car.internal.property.CarPropertyErrorCodes;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.expresslog.Histogram;

import java.io.FileDescriptor;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;

final class AidlVehicleStub extends VehicleStub {
    private static final Histogram sVehicleHalGetSyncLatencyHistogram = new Histogram(
            "automotive_os.value_sync_hal_get_property_latency",
            new Histogram.ScaledRangeOptions(/* binCount= */ 20, /* minValue= */ 0,
                    /* firstBinWidth= */ 2, /* scaleFactor= */ 1.5f));

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

    private static final String AIDL_VHAL_SERVICE =
            "android.hardware.automotive.vehicle.IVehicle/default";
    // default timeout: 10s
    private static final long DEFAULT_TIMEOUT_MS = 10_000;

    private static final String TAG = CarLog.tagFor(AidlVehicleStub.class);
    private static final long TRACE_TAG = TraceHelper.TRACE_TAG_CAR_SERVICE;

    private final IVehicle mAidlVehicle;
    private final HalPropValueBuilder mPropValueBuilder;
    private final GetSetValuesCallback mGetSetValuesCallback;
    private final HandlerThread mHandlerThread;
    private final Handler mHandler;
    private final AtomicLong mRequestId = new AtomicLong(0);
    private final Object mLock = new Object();
    // PendingSyncRequestPool is thread-safe.
    private final PendingSyncRequestPool<GetValueResult> mPendingSyncGetValueRequestPool =
            new PendingSyncRequestPool<>();
    private final PendingSyncRequestPool<SetValueResult> mPendingSyncSetValueRequestPool =
            new PendingSyncRequestPool<>();
    // PendingAsyncRequestPool is thread-safe.
    private final PendingAsyncRequestPool mPendingAsyncRequestPool;

    // This might be modifed during tests.
    private long mSyncOpTimeoutInMs = DEFAULT_TIMEOUT_MS;

    private static class AsyncRequestInfo implements LongRequestIdWithTimeout {
        private final int mServiceRequestId;
        private final VehicleStubCallbackInterface mClientCallback;
        private final long mTimeoutUptimeMs;
        private final long mVhalRequestId;

        private AsyncRequestInfo(
                long vhalRequestId,
                int serviceRequestId,
                VehicleStubCallbackInterface clientCallback,
                long timeoutUptimeMs) {
            mVhalRequestId = vhalRequestId;
            mServiceRequestId = serviceRequestId;
            mClientCallback = clientCallback;
            mTimeoutUptimeMs = timeoutUptimeMs;
        }

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

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

        public int getServiceRequestId() {
            return mServiceRequestId;
        }

        public VehicleStubCallbackInterface getClientCallback() {
            return mClientCallback;
        }
    }

    AidlVehicleStub() {
        this(getAidlVehicle());
    }

    @VisibleForTesting
    AidlVehicleStub(IVehicle aidlVehicle) {
        this(aidlVehicle,
                CarServiceUtils.getHandlerThread(AidlVehicleStub.class.getSimpleName()));
    }

    @VisibleForTesting
    AidlVehicleStub(IVehicle aidlVehicle, HandlerThread handlerThread) {
        mAidlVehicle = aidlVehicle;
        mPropValueBuilder = new HalPropValueBuilder(/*isAidl=*/true);
        mHandlerThread = handlerThread;
        mHandler = new Handler(mHandlerThread.getLooper());
        mGetSetValuesCallback = new GetSetValuesCallback();
        mPendingAsyncRequestPool = new PendingAsyncRequestPool(mHandler.getLooper());
    }

    /**
     * Sets the timeout for getValue/setValue requests in milliseconds.
     */
    @VisibleForTesting
    void setSyncOpTimeoutInMs(long timeoutMs) {
        mSyncOpTimeoutInMs = timeoutMs;
    }

    @VisibleForTesting
    int countPendingRequests() {
        synchronized (mLock) {
            return mPendingAsyncRequestPool.size()
                    + mPendingSyncGetValueRequestPool.size()
                    + mPendingSyncSetValueRequestPool.size();
        }
    }

    /**
     * Checks whether we are connected to AIDL VHAL: {@code true} or HIDL VHAL: {@code false}.
     */
    @Override
    public boolean isAidlVhal() {
        return true;
    }

    /**
     * Gets a HalPropValueBuilder that could be used to build a HalPropValue.
     *
     * @return a builder to build HalPropValue.
     */
    @Override
    public HalPropValueBuilder getHalPropValueBuilder() {
        return mPropValueBuilder;
    }

    /**
     * Returns whether this vehicle stub is connecting to a valid vehicle HAL.
     *
     * @return Whether this vehicle stub is connecting to a valid vehicle HAL.
     */
    @Override
    public boolean isValid() {
        return mAidlVehicle != null;
    }

    /**
     * Gets the interface descriptor for the connecting vehicle HAL.
     *
     * @return the interface descriptor.
     * @throws IllegalStateException If unable to get the descriptor.
     */
    @Override
    public String getInterfaceDescriptor() throws IllegalStateException {
        try {
            return mAidlVehicle.asBinder().getInterfaceDescriptor();
        } catch (RemoteException e) {
            throw new IllegalStateException("Unable to get Vehicle HAL interface descriptor", e);
        }
    }

    /**
     * Registers a death recipient that would be called when vehicle HAL died.
     *
     * @param recipient A death recipient.
     * @throws IllegalStateException If unable to register the death recipient.
     */
    @Override
    public void linkToDeath(IVehicleDeathRecipient recipient) throws IllegalStateException {
        try {
            mAidlVehicle.asBinder().linkToDeath(recipient, /*flag=*/ 0);
        } catch (RemoteException e) {
            throw new IllegalStateException("Failed to linkToDeath Vehicle HAL");
        }
    }

    /**
     * Unlinks a previously linked death recipient.
     *
     * @param recipient A previously linked death recipient.
     */
    @Override
    public void unlinkToDeath(IVehicleDeathRecipient recipient) {
        mAidlVehicle.asBinder().unlinkToDeath(recipient, /*flag=*/ 0);
    }

    /**
     * Gets all property configs.
     *
     * @return All the property configs.
     * @throws RemoteException if the remote operation fails.
     * @throws ServiceSpecificException if VHAL returns service specific error.
     */
    @Override
    public HalPropConfig[] getAllPropConfigs()
            throws RemoteException, ServiceSpecificException {
        VehiclePropConfigs propConfigs = (VehiclePropConfigs)
                LargeParcelable.reconstructStableAIDLParcelable(
                        mAidlVehicle.getAllPropConfigs(), /* keepSharedMemory= */ false);
        VehiclePropConfig[] payloads = propConfigs.payloads;
        int size = payloads.length;
        HalPropConfig[] configs = new HalPropConfig[size];
        for (int i = 0; i < size; i++) {
            configs[i] = new AidlHalPropConfig(payloads[i]);
        }
        return configs;
    }

    /**
     * Gets a new {@code SubscriptionClient} that could be used to subscribe/unsubscribe.
     *
     * @param callback A callback that could be used to receive events.
     * @return a {@code SubscriptionClient} that could be used to subscribe/unsubscribe.
     */
    @Override
    public SubscriptionClient newSubscriptionClient(VehicleHalCallback callback) {
        return new AidlSubscriptionClient(callback, mPropValueBuilder);
    }

    /**
     * Gets a property.
     *
     * @param requestedPropValue The property to get.
     * @return The vehicle property value.
     * @throws RemoteException if the remote operation fails.
     * @throws ServiceSpecificException if VHAL returns service specific error.
     */
    @Override
    @Nullable
    public HalPropValue get(HalPropValue requestedPropValue)
            throws RemoteException, ServiceSpecificException {
        long currentTime = System.currentTimeMillis();
        HalPropValue halPropValue = getOrSetSync(requestedPropValue,
                mPendingSyncGetValueRequestPool, new AsyncGetRequestsHandler(),
                (result) -> {
                    if (result.status != StatusCode.OK) {
                        throw new ServiceSpecificException(result.status,
                                "failed to get value for " + printPropIdAreaId(requestedPropValue));
                    }
                    if (result.prop == null) {
                        return null;
                    }
                    return mPropValueBuilder.build(result.prop);
                });
        sVehicleHalGetSyncLatencyHistogram.logSample((float)
                (System.currentTimeMillis() - currentTime));
        return halPropValue;
    }

    /**
     * Sets a property.
     *
     * @param requestedPropValue The property to set.
     * @throws RemoteException if the remote operation fails.
     * @throws ServiceSpecificException if VHAL returns service specific error.
     */
    @Override
    public void set(HalPropValue requestedPropValue) throws RemoteException,
            ServiceSpecificException {
        long currentTime = System.currentTimeMillis();
        getOrSetSync(requestedPropValue, mPendingSyncSetValueRequestPool,
                new AsyncSetRequestsHandler(),
                (result) -> {
                    if (result.status != StatusCode.OK) {
                        throw new ServiceSpecificException(result.status,
                                "failed to set value for " + printPropIdAreaId(requestedPropValue));
                    }
                    return null;
                });
        sVehicleHalSetSyncLatencyHistogram.logSample((float)
                (System.currentTimeMillis() - currentTime));
    }

    @Override
    public void getAsync(List<AsyncGetSetRequest> getVehicleStubAsyncRequests,
            VehicleStubCallbackInterface getCallback) {
        getOrSetAsync(getVehicleStubAsyncRequests, getCallback, new AsyncGetRequestsHandler(),
                new AsyncGetResultsHandler(mPropValueBuilder));
    }

    @Override
    public void setAsync(List<AsyncGetSetRequest> setVehicleStubAsyncRequests,
            VehicleStubCallbackInterface setCallback) {
        getOrSetAsync(setVehicleStubAsyncRequests, setCallback, new AsyncSetRequestsHandler(),
                new AsyncSetResultsHandler());
    }

    @Override
    public void dump(FileDescriptor fd, List<String> args) throws RemoteException {
        mAidlVehicle.asBinder().dump(fd, args.toArray(new String[args.size()]));
    }

    // Get all the VHAL request IDs according to the service request IDs and remove them from
    // pending requests map.
    @Override
    public void cancelRequests(List<Integer> serviceRequestIds) {
        mPendingAsyncRequestPool.cancelRequests(serviceRequestIds);
    }

    /**
     * A thread-safe pending sync request pool.
     */
    private static final class PendingSyncRequestPool<VhalResultType> {
        private final Object mSyncRequestPoolLock = new Object();
        @GuardedBy("mSyncRequestPoolLock")
        private final LongSparseArray<AndroidFuture<VhalResultType>>
                mPendingRequestsByVhalRequestId = new LongSparseArray();

        AndroidFuture<VhalResultType> addRequest(long vhalRequestId) {
            synchronized (mSyncRequestPoolLock) {
                AndroidFuture<VhalResultType> resultFuture = new AndroidFuture();
                mPendingRequestsByVhalRequestId.put(vhalRequestId, resultFuture);
                return resultFuture;
            }
        }

        @Nullable AndroidFuture<VhalResultType> finishRequestIfFound(long vhalRequestId) {
            synchronized (mSyncRequestPoolLock) {
                AndroidFuture<VhalResultType> pendingRequest =
                        mPendingRequestsByVhalRequestId.get(vhalRequestId);
                mPendingRequestsByVhalRequestId.remove(vhalRequestId);
                return pendingRequest;
            }
        }

        int size() {
            synchronized (mSyncRequestPoolLock) {
                return mPendingRequestsByVhalRequestId.size();
            }
        }
    }

    /**
     * A thread-safe pending async request pool.
     */
    private static final class PendingAsyncRequestPool {
        private final Object mAsyncRequestPoolLock = new Object();
        private final TimeoutCallback mTimeoutCallback = new AsyncRequestTimeoutCallback();
        private final Looper mLooper;
        @GuardedBy("mAsyncRequestPoolLock")
        private final LongPendingRequestPool<AsyncRequestInfo> mPendingRequestPool;

        PendingAsyncRequestPool(Looper looper) {
            mLooper = looper;
            mPendingRequestPool = new LongPendingRequestPool<>(mLooper, mTimeoutCallback);
        }

        private class AsyncRequestTimeoutCallback implements TimeoutCallback {
            @Override
            public void onRequestsTimeout(List<Long> vhalRequestIds) {
                ArrayMap<VehicleStubCallbackInterface, List<Integer>> serviceRequestIdsByCallback =
                        new ArrayMap<>();
                for (int i = 0; i < vhalRequestIds.size(); i++) {
                    long vhalRequestId = vhalRequestIds.get(i);
                    AsyncRequestInfo requestInfo = finishRequestIfFound(vhalRequestId);
                    if (requestInfo == null) {
                        // We already finished the request or the callback is already dead, ignore.
                        Slogf.w(TAG, "onRequestsTimeout: the request for VHAL request ID: %d is "
                                + "already finished or the callback is already dead, ignore",
                                vhalRequestId);
                        continue;
                    }
                    VehicleStubCallbackInterface getAsyncCallback = requestInfo.getClientCallback();
                    if (serviceRequestIdsByCallback.get(getAsyncCallback) == null) {
                        serviceRequestIdsByCallback.put(getAsyncCallback, new ArrayList<>());
                    }
                    serviceRequestIdsByCallback.get(getAsyncCallback).add(
                            requestInfo.getServiceRequestId());
                }

                for (int i = 0; i < serviceRequestIdsByCallback.size(); i++) {
                    serviceRequestIdsByCallback.keyAt(i).onRequestsTimeout(
                            serviceRequestIdsByCallback.valueAt(i));
                }
            }
        }


        void addRequests(List<AsyncRequestInfo> requestInfo) {
            synchronized (mAsyncRequestPoolLock) {
                mPendingRequestPool.addPendingRequests(requestInfo);
            }
        }

        @Nullable AsyncRequestInfo finishRequestIfFound(long vhalRequestId) {
            synchronized (mAsyncRequestPoolLock) {
                AsyncRequestInfo requestInfo = mPendingRequestPool.getRequestIfFound(vhalRequestId);
                mPendingRequestPool.removeRequest(vhalRequestId);
                return requestInfo;
            }
        }

        int size() {
            synchronized (mAsyncRequestPoolLock) {
                return mPendingRequestPool.size();
            }
        }

        boolean contains(long vhalRequestId) {
            synchronized (mAsyncRequestPoolLock) {
                return mPendingRequestPool.getRequestIfFound(vhalRequestId) != null;
            }
        }

        void cancelRequests(List<Integer> serviceRequestIds) {
            Set<Integer> serviceRequestIdsSet = new ArraySet<>(serviceRequestIds);
            List<Long> vhalRequestIdsToCancel = new ArrayList<>();
            synchronized (mAsyncRequestPoolLock) {
                for (int i = 0; i < mPendingRequestPool.size(); i++) {
                    int serviceRequestId = mPendingRequestPool.valueAt(i)
                            .getServiceRequestId();
                    if (serviceRequestIdsSet.contains(serviceRequestId)) {
                        vhalRequestIdsToCancel.add(mPendingRequestPool.keyAt(i));
                    }
                }
                for (int i = 0; i < vhalRequestIdsToCancel.size(); i++) {
                    long vhalRequestIdToCancel = vhalRequestIdsToCancel.get(i);
                    Slogf.w(TAG, "the request for VHAL request ID: %d is cancelled",
                            vhalRequestIdToCancel);
                    mPendingRequestPool.removeRequest(vhalRequestIdToCancel);
                }
            }
        }

        void removeRequestsForCallback(VehicleStubCallbackInterface callback) {
            synchronized (mAsyncRequestPoolLock) {
                List<Long> requestIdsToRemove = new ArrayList<>();

                for (int i = 0; i < mPendingRequestPool.size(); i++) {
                    if (mPendingRequestPool.valueAt(i).getClientCallback() == callback) {
                        requestIdsToRemove.add(mPendingRequestPool.keyAt(i));
                    }
                }

                for (int i = 0; i < requestIdsToRemove.size(); i++) {
                    mPendingRequestPool.removeRequest(requestIdsToRemove.get(i));
                }
            }
        }
    }

    /**
     * An abstract interface for handling async get/set value requests from vehicle stub.
     */
    private abstract static class AsyncRequestsHandler<VhalRequestType, VhalRequestsType> {
        /**
         * Preallocsate size array for storing VHAL requests.
         */
        abstract void allocateVhalRequestSize(int size);

        /**
         * Add a vhal request to be sent later.
         */
        abstract void addVhalRequest(long vhalRequestId, HalPropValue halPropValue);

        /**
         * Get the list of stored request items.
         */
        abstract VhalRequestType[] getRequestItems();

        /**
         * Send the prepared requests to VHAL.
         */
        abstract void sendRequestsToVhal(IVehicle iVehicle, GetSetValuesCallback callbackForVhal)
                throws RemoteException, ServiceSpecificException;

        /**
         * Get the request ID for the request.
         */
        abstract long getVhalRequestId(VhalRequestType vhalRequest);
    }

    /**
     * An abstract class to handle async get/set value results from VHAL.
     */
    private abstract static class AsyncResultsHandler<VhalResultType, VehicleStubResultType> {
        protected Map<VehicleStubCallbackInterface, List<VehicleStubResultType>> mCallbackToResults;

        /**
         * Add an error result to be sent to vehicleStub through the callback later.
         */
        abstract void addErrorResult(VehicleStubCallbackInterface callback, int serviceRequestId,
                CarPropertyErrorCodes errorCodes);
        /**
         * Add a VHAL result to be sent to vehicleStub through the callback later.
         */
        abstract void addVhalResult(VehicleStubCallbackInterface callback, int serviceRequestId,
                VhalResultType result);
        /**
         * Send all the stored results to vehicleStub.
         */
        abstract void callVehicleStubCallback();

        /**
         * Get the request ID for the result.
         */
        abstract long getVhalRequestId(VhalResultType vhalRequest);

        protected void addVehicleStubResult(VehicleStubCallbackInterface callback,
                VehicleStubResultType vehicleStubResult) {
            if (mCallbackToResults.get(callback) == null) {
                mCallbackToResults.put(callback, new ArrayList<>());
            }
            mCallbackToResults.get(callback).add(vehicleStubResult);
        }
    }

    @Nullable
    private static IVehicle getAidlVehicle() {
        try {
            return IVehicle.Stub.asInterface(
                    ServiceManagerHelper.waitForDeclaredService(AIDL_VHAL_SERVICE));
        } catch (RuntimeException e) {
            Slogf.w(TAG, "Failed to get \"" + AIDL_VHAL_SERVICE + "\" service", e);
        }
        return null;
    }

    private class AidlSubscriptionClient extends IVehicleCallback.Stub
            implements SubscriptionClient {
        private final VehicleHalCallback mCallback;
        private final HalPropValueBuilder mBuilder;

        AidlSubscriptionClient(VehicleHalCallback callback, HalPropValueBuilder builder) {
            mCallback = callback;
            mBuilder = builder;
        }

        @Override
        public void onGetValues(GetValueResults responses) throws RemoteException {
            // We use GetSetValuesCallback for getValues and setValues operation.
            throw new UnsupportedOperationException(
                    "onGetValues should never be called on AidlSubscriptionClient");
        }

        @Override
        public void onSetValues(SetValueResults responses) throws RemoteException {
            // We use GetSetValuesCallback for getValues and setValues operation.
            throw new UnsupportedOperationException(
                    "onSetValues should never be called on AidlSubscriptionClient");
        }

        @Override
        public void onPropertyEvent(VehiclePropValues propValues, int sharedMemoryFileCount)
                throws RemoteException {
            VehiclePropValues origPropValues = (VehiclePropValues)
                    LargeParcelable.reconstructStableAIDLParcelable(propValues,
                            /* keepSharedMemory= */ false);
            ArrayList<HalPropValue> values = new ArrayList<>(origPropValues.payloads.length);
            for (VehiclePropValue value : origPropValues.payloads) {
                values.add(mBuilder.build(value));
            }
            mCallback.onPropertyEvent(values);
        }

        @Override
        public void onPropertySetError(VehiclePropErrors errors) throws RemoteException {
            VehiclePropErrors origErrors = (VehiclePropErrors)
                    LargeParcelable.reconstructStableAIDLParcelable(errors,
                            /* keepSharedMemory= */ false);
            ArrayList<VehiclePropError> errorList = new ArrayList<>(origErrors.payloads.length);
            for (VehiclePropError error : origErrors.payloads) {
                errorList.add(error);
            }
            mCallback.onPropertySetError(errorList);
        }

        @Override
        public void subscribe(SubscribeOptions[] options)
                throws RemoteException, ServiceSpecificException {
            mAidlVehicle.subscribe(this, options, /* maxSharedMemoryFileCount= */ 2);
        }

        @Override
        public void unsubscribe(int prop) throws RemoteException, ServiceSpecificException {
            mAidlVehicle.unsubscribe(this, new int[]{prop});
        }

        @Override
        public String getInterfaceHash() {
            return IVehicleCallback.HASH;
        }

        @Override
        public int getInterfaceVersion() {
            return IVehicleCallback.VERSION;
        }
    }

    private void onGetValues(GetValueResults responses) {
        Trace.traceBegin(TRACE_TAG, "AidlVehicleStub#onGetValues");
        GetValueResults origResponses = (GetValueResults)
                LargeParcelable.reconstructStableAIDLParcelable(responses,
                        /* keepSharedMemory= */ false);
        onGetSetValues(origResponses.payloads, new AsyncGetResultsHandler(mPropValueBuilder),
                mPendingSyncGetValueRequestPool);
        Trace.traceEnd(TRACE_TAG);
    }

    private void onSetValues(SetValueResults responses) {
        Trace.traceBegin(TRACE_TAG, "AidlVehicleStub#onSetValues");
        SetValueResults origResponses = (SetValueResults)
                LargeParcelable.reconstructStableAIDLParcelable(responses,
                        /* keepSharedMemory= */ false);
        onGetSetValues(origResponses.payloads, new AsyncSetResultsHandler(),
                mPendingSyncSetValueRequestPool);
        Trace.traceEnd(TRACE_TAG);
    }

    /**
     * A generic function for {@link onGetValues} / {@link onSetValues}.
     */
    private <VhalResultType> void onGetSetValues(VhalResultType[] vhalResults,
            AsyncResultsHandler asyncResultsHandler,
            PendingSyncRequestPool<VhalResultType> pendingSyncRequestPool) {
        synchronized (mLock) {
            for (VhalResultType result : vhalResults) {
                long vhalRequestId = asyncResultsHandler.getVhalRequestId(result);
                if (!mPendingAsyncRequestPool.contains(vhalRequestId)) {
                    // If we cannot find the request Id in the async map, we assume it is for a
                    // sync request.
                    completePendingSyncRequestLocked(pendingSyncRequestPool, vhalRequestId, result);
                    continue;
                }

                AsyncRequestInfo requestInfo = mPendingAsyncRequestPool.finishRequestIfFound(
                        vhalRequestId);
                if (requestInfo == null) {
                    Slogf.w(TAG,
                            "No pending request for ID: %s, possibly already timed out, "
                            + "or cancelled, or the client already died", vhalRequestId);
                    continue;
                }
                asyncResultsHandler.addVhalResult(requestInfo.getClientCallback(),
                        requestInfo.getServiceRequestId(), result);
            }
        }
        Trace.traceBegin(TRACE_TAG, "AidlVehicleStub call async result callback");
        asyncResultsHandler.callVehicleStubCallback();
        Trace.traceEnd(TRACE_TAG);
    }

    private static String printPropIdAreaId(HalPropValue value) {
        return "propID: " + value.getPropId() + ", areaID: " + value.getAreaId();
    }

    private final class GetSetValuesCallback extends IVehicleCallback.Stub {

        @Override
        public void onGetValues(GetValueResults responses) throws RemoteException {
            AidlVehicleStub.this.onGetValues(responses);
        }

        @Override
        public void onSetValues(SetValueResults responses) throws RemoteException {
            AidlVehicleStub.this.onSetValues(responses);
        }

        @Override
        public void onPropertyEvent(VehiclePropValues propValues, int sharedMemoryFileCount)
                throws RemoteException {
            throw new UnsupportedOperationException(
                    "GetSetValuesCallback only support onGetValues or onSetValues");
        }

        @Override
        public void onPropertySetError(VehiclePropErrors errors) throws RemoteException {
            throw new UnsupportedOperationException(
                    "GetSetValuesCallback only support onGetValues or onSetValues");
        }

        @Override
        public String getInterfaceHash() {
            return IVehicleCallback.HASH;
        }

        @Override
        public int getInterfaceVersion() {
            return IVehicleCallback.VERSION;
        }
    }

    /**
     * Mark a pending sync get/set property request as complete and deliver the result.
     */
    @GuardedBy("mLock")
    private <VhalResultType> void completePendingSyncRequestLocked(
            PendingSyncRequestPool<VhalResultType> pendingSyncRequestPool, long vhalRequestId,
            VhalResultType result) {
        Trace.traceBegin(TRACE_TAG, "AidlVehicleStub#completePendingSyncRequestLocked");
        AndroidFuture<VhalResultType> pendingRequest =
                pendingSyncRequestPool.finishRequestIfFound(vhalRequestId);
        if (pendingRequest == null) {
            Slogf.w(TAG, "No pending request for ID: " + vhalRequestId
                    + ", possibly already timed out");
            return;
        }

        Trace.traceBegin(TRACE_TAG, "AidlVehicleStub#complete pending request");
        // This might fail if the request already timed out.
        pendingRequest.complete(result);
        Trace.traceEnd(TRACE_TAG);
        Trace.traceEnd(TRACE_TAG);
    }

    private static final class AsyncGetRequestsHandler
            extends AsyncRequestsHandler<GetValueRequest, GetValueRequests> {
        private GetValueRequest[] mVhalRequestItems;
        private int mIndex;

        @Override
        public void allocateVhalRequestSize(int size) {
            mVhalRequestItems = new GetValueRequest[size];
        }

        @Override
        public void addVhalRequest(long vhalRequestId, HalPropValue halPropValue) {
            mVhalRequestItems[mIndex] = new GetValueRequest();
            mVhalRequestItems[mIndex].requestId = vhalRequestId;
            mVhalRequestItems[mIndex].prop = (VehiclePropValue) halPropValue.toVehiclePropValue();
            mIndex++;
        }

        @Override
        public GetValueRequest[] getRequestItems() {
            return mVhalRequestItems;
        }

        @Override
        public void sendRequestsToVhal(IVehicle iVehicle, GetSetValuesCallback callbackForVhal)
                throws RemoteException, ServiceSpecificException {
            Trace.traceBegin(TRACE_TAG, "Prepare LargeParcelable");
            GetValueRequests largeParcelableRequest = new GetValueRequests();
            largeParcelableRequest.payloads = mVhalRequestItems;

            // TODO(b/269669729): Don't try to use large parcelable if the request size is too
            // small.
            largeParcelableRequest = (GetValueRequests) LargeParcelable.toLargeParcelable(
                    largeParcelableRequest, () -> {
                        GetValueRequests newRequests = new GetValueRequests();
                        newRequests.payloads = new GetValueRequest[0];
                        return newRequests;
            });
            Trace.traceEnd(TRACE_TAG);
            Trace.traceBegin(TRACE_TAG, "IVehicle#getValues");
            iVehicle.getValues(callbackForVhal, largeParcelableRequest);
            Trace.traceEnd(TRACE_TAG);
        }

        @Override
        public long getVhalRequestId(GetValueRequest request) {
            return request.requestId;
        }
    }

    private static final class AsyncSetRequestsHandler
            extends AsyncRequestsHandler<SetValueRequest, SetValueRequests> {
        private SetValueRequest[] mVhalRequestItems;
        private int mIndex;

        @Override
        public void allocateVhalRequestSize(int size) {
            mVhalRequestItems = new SetValueRequest[size];
        }

        @Override
        public void addVhalRequest(long vhalRequestId, HalPropValue halPropValue) {
            mVhalRequestItems[mIndex] = new SetValueRequest();
            mVhalRequestItems[mIndex].requestId = vhalRequestId;
            mVhalRequestItems[mIndex].value = (VehiclePropValue) halPropValue.toVehiclePropValue();
            mIndex++;
        }

        @Override
        public SetValueRequest[] getRequestItems() {
            return mVhalRequestItems;
        }

        @Override
        public void sendRequestsToVhal(IVehicle iVehicle, GetSetValuesCallback callbackForVhal)
                throws RemoteException, ServiceSpecificException {
            SetValueRequests largeParcelableRequest = new SetValueRequests();
            largeParcelableRequest.payloads = mVhalRequestItems;
            largeParcelableRequest = (SetValueRequests) LargeParcelable.toLargeParcelable(
                    largeParcelableRequest, () -> {
                        SetValueRequests newRequests = new SetValueRequests();
                        newRequests.payloads = new SetValueRequest[0];
                        return newRequests;
            });
            iVehicle.setValues(callbackForVhal, largeParcelableRequest);
        }

        @Override
        public long getVhalRequestId(SetValueRequest request) {
            return request.requestId;
        }
    }

    private static final class AsyncGetResultsHandler extends
            AsyncResultsHandler<GetValueResult, GetVehicleStubAsyncResult> {
        private HalPropValueBuilder mPropValueBuilder;

        AsyncGetResultsHandler(HalPropValueBuilder propValueBuilder) {
            mPropValueBuilder = propValueBuilder;
            mCallbackToResults = new ArrayMap<VehicleStubCallbackInterface,
                    List<GetVehicleStubAsyncResult>>();
        }

        @Override
        void addErrorResult(VehicleStubCallbackInterface callback, int serviceRequestId,
                CarPropertyErrorCodes errorCodes) {
            addVehicleStubResult(callback, new GetVehicleStubAsyncResult(serviceRequestId,
                    errorCodes));
        }

        @Override
        void addVhalResult(VehicleStubCallbackInterface callback, int serviceRequestId,
                GetValueResult result) {
            addVehicleStubResult(callback, toVehicleStubResult(serviceRequestId, result));
        }

        @Override
        void callVehicleStubCallback() {
            for (Map.Entry<VehicleStubCallbackInterface, List<GetVehicleStubAsyncResult>> entry :
                    mCallbackToResults.entrySet()) {
                entry.getKey().onGetAsyncResults(entry.getValue());
            }
        }

        @Override
        long getVhalRequestId(GetValueResult result) {
            return result.requestId;
        }

        private GetVehicleStubAsyncResult toVehicleStubResult(int serviceRequestId,
                GetValueResult vhalResult) {
            if (vhalResult.status != StatusCode.OK) {
                CarPropertyErrorCodes carPropertyErrorCodes =
                        convertVhalStatusCodeToCarPropertyManagerErrorCodes(vhalResult.status);
                return new GetVehicleStubAsyncResult(serviceRequestId, carPropertyErrorCodes);
            } else if (vhalResult.prop == null) {
                // If status is OKAY but no property is returned, treat it as not_available.
                return new GetVehicleStubAsyncResult(serviceRequestId,
                        new CarPropertyErrorCodes(
                                CarPropertyManager.STATUS_ERROR_NOT_AVAILABLE,
                                /* vendorErrorCode= */ 0,
                                /* systemErrorCode= */ 0));
            }
            return new GetVehicleStubAsyncResult(serviceRequestId,
                    mPropValueBuilder.build(vhalResult.prop));
        }
    }

    private static final class AsyncSetResultsHandler extends
            AsyncResultsHandler<SetValueResult, SetVehicleStubAsyncResult> {
        AsyncSetResultsHandler() {
            mCallbackToResults = new ArrayMap<VehicleStubCallbackInterface,
                    List<SetVehicleStubAsyncResult>>();
        }

        @Override
        void addErrorResult(VehicleStubCallbackInterface callback, int serviceRequestId,
                CarPropertyErrorCodes errorCodes) {
            addVehicleStubResult(callback,
                    new SetVehicleStubAsyncResult(serviceRequestId, errorCodes));
        }

        @Override
        void addVhalResult(VehicleStubCallbackInterface callback, int serviceRequestId,
                SetValueResult result) {
            addVehicleStubResult(callback, toVehicleStubResult(serviceRequestId, result));

        }

        @Override
        void callVehicleStubCallback() {
            for (Map.Entry<VehicleStubCallbackInterface, List<SetVehicleStubAsyncResult>> entry :
                    mCallbackToResults.entrySet()) {
                entry.getKey().onSetAsyncResults(entry.getValue());
            }
        }

        @Override
        long getVhalRequestId(SetValueResult result) {
            return result.requestId;
        }

        private SetVehicleStubAsyncResult toVehicleStubResult(int serviceRequestId,
                SetValueResult vhalResult) {
            if (vhalResult.status != StatusCode.OK) {
                CarPropertyErrorCodes carPropertyErrorCodes =
                        convertVhalStatusCodeToCarPropertyManagerErrorCodes(vhalResult.status);
                return new SetVehicleStubAsyncResult(serviceRequestId, carPropertyErrorCodes);
            }
            return new SetVehicleStubAsyncResult(serviceRequestId);
        }
    }

    /**
     * Generic function for {@link get} or {@link set}.
     */
    private <VhalResultType> HalPropValue getOrSetSync(
            HalPropValue requestedPropValue,
            PendingSyncRequestPool<VhalResultType> pendingSyncRequestPool,
            AsyncRequestsHandler requestsHandler,
            Function<VhalResultType, HalPropValue> resultHandler)
            throws RemoteException, ServiceSpecificException {
        Trace.traceBegin(TRACE_TAG, "AidlVehicleStub#getOrSetSync");
        long vhalRequestId = mRequestId.getAndIncrement();

        AndroidFuture<VhalResultType> resultFuture = pendingSyncRequestPool.addRequest(
                vhalRequestId);

        requestsHandler.allocateVhalRequestSize(1);
        requestsHandler.addVhalRequest(vhalRequestId, requestedPropValue);
        requestsHandler.sendRequestsToVhal(mAidlVehicle, mGetSetValuesCallback);

        boolean gotResult = false;

        try {
            Trace.traceBegin(TRACE_TAG, "AidlVehicleStub#waitingForSyncResult");
            VhalResultType result = resultFuture.get(mSyncOpTimeoutInMs,
                    TimeUnit.MILLISECONDS);
            gotResult = true;
            return resultHandler.apply(result);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // Restore the interrupted status
            throw new ServiceSpecificException(StatusCode.INTERNAL_ERROR,
                    "thread interrupted, possibly exiting the thread");
        } catch (ExecutionException e) {
            throw new ServiceSpecificException(StatusCode.INTERNAL_ERROR,
                    "failed to resolve future, error: " + e);
        } catch (TimeoutException e) {
            throw new ServiceSpecificException(StatusCode.INTERNAL_ERROR,
                    "get/set value request timeout for: " + printPropIdAreaId(requestedPropValue));
        } finally {
            Trace.traceEnd(TRACE_TAG);
            if (!gotResult) {
                resultFuture = pendingSyncRequestPool.finishRequestIfFound(vhalRequestId);
                // Something wrong happened, the future is guaranteed not to be used again.
                resultFuture.cancel(/* mayInterruptIfRunning= */ false);
            }
            Trace.traceEnd(TRACE_TAG);
        }
    }

    /**
     * Generic function for {@link getAsync} or {@link setAsync}.
     */
    private <VhalRequestType, VhalRequestsType> void getOrSetAsync(
            List<AsyncGetSetRequest> vehicleStubAsyncRequests,
            VehicleStubCallbackInterface vehicleStubCallback,
            AsyncRequestsHandler<VhalRequestType, VhalRequestsType> asyncRequestsHandler,
            AsyncResultsHandler asyncResultsHandler) {
        prepareAndConvertAsyncRequests(vehicleStubAsyncRequests, vehicleStubCallback,
                asyncRequestsHandler);

        try {
            asyncRequestsHandler.sendRequestsToVhal(mAidlVehicle, mGetSetValuesCallback);
        } catch (RemoteException e) {
            handleAsyncExceptionFromVhal(
                    asyncRequestsHandler,
                    vehicleStubCallback,
                    new CarPropertyErrorCodes(
                            CarPropertyManager.STATUS_ERROR_INTERNAL_ERROR,
                            /* vendorErrorCode= */ 0,
                            /* systemErrorCode= */ 0),
                    asyncResultsHandler);
            return;
        } catch (ServiceSpecificException e) {
            CarPropertyErrorCodes carPropertyErrorCodes =
                    convertVhalStatusCodeToCarPropertyManagerErrorCodes(e.errorCode);
            handleAsyncExceptionFromVhal(asyncRequestsHandler, vehicleStubCallback,
                    carPropertyErrorCodes, asyncResultsHandler);
            return;
        }
    }

    /**
     * Prepare an async get/set request from client and convert it to vhal requests.
     *
     * <p> It does the following things:
     * <ul>
     * <li> Add a client callback death listener which will clear the pending requests when client
     * died
     * <li> Store the async requests to a pending request map.
     * <li> For each client request, generate a unique VHAL request ID and convert the request to
     * VHAL request type.
     * <li> Stores the time-out information for each request into a map so that we can register
     * timeout handlers later.
     * <li> Convert the vhal request items to a single large parcelable class.
     */
    private <VhalRequestType, VhalRequestsType> void prepareAndConvertAsyncRequests(
                    List<AsyncGetSetRequest> vehicleStubRequests,
                    VehicleStubCallbackInterface clientCallback,
                    AsyncRequestsHandler<VhalRequestType, VhalRequestsType> asyncRequestsHandler) {
        asyncRequestsHandler.allocateVhalRequestSize(vehicleStubRequests.size());
        synchronized (mLock) {
            // Add the death recipient so that all client info for a dead callback will be cleaned
            // up. Note that this must be in the same critical section as the following code to
            // store the client info into the map. This makes sure that even if the client is
            // died half way while adding the client info, it will wait until all the clients are
            // added and then remove them all.
            try {
                clientCallback.linkToDeath(() -> {
                    // This function will be invoked from a different thread. It needs to be
                    // guarded by a lock so that the whole 'prepareAndConvertAsyncRequests' finishes
                    // before we remove the callback.
                    synchronized (mLock) {
                        mPendingAsyncRequestPool.removeRequestsForCallback(clientCallback);
                    }
                });
            } catch (RemoteException e) {
                // The binder is already died.
                throw new IllegalStateException("Failed to link callback to death recipient, the "
                        + "client maybe already died");
            }

            List<AsyncRequestInfo> requestInfoList = new ArrayList<>();
            for (int i = 0; i < vehicleStubRequests.size(); i++) {
                AsyncGetSetRequest vehicleStubRequest = vehicleStubRequests.get(i);
                long vhalRequestId = mRequestId.getAndIncrement();
                asyncRequestsHandler.addVhalRequest(vhalRequestId,
                        vehicleStubRequest.getHalPropValue());
                requestInfoList.add(new AsyncRequestInfo(
                        vhalRequestId, vehicleStubRequest.getServiceRequestId(), clientCallback,
                        vehicleStubRequest.getTimeoutUptimeMs()));
            }
            mPendingAsyncRequestPool.addRequests(requestInfoList);
        }

    }

    /**
     * Callback to deliver async get/set error results back to the client.
     *
     * <p>When an exception is received, the callback delivers the error results on the same thread
     * where the caller is.
     */
    private <VhalRequestType, VhalRequestsType> void handleAsyncExceptionFromVhal(
            AsyncRequestsHandler<VhalRequestType, VhalRequestsType> asyncRequestsHandler,
            VehicleStubCallbackInterface vehicleStubCallback, CarPropertyErrorCodes errorCodes,
            AsyncResultsHandler asyncResultsHandler) {
        Slogf.w(TAG,
                "Received RemoteException or ServiceSpecificException from VHAL. VHAL is likely "
                        + "dead, system error code: %d, vendor error code: %d",
                errorCodes.getCarPropertyManagerErrorCode(), errorCodes.getVendorErrorCode());
        synchronized (mLock) {
            VhalRequestType[] requests = asyncRequestsHandler.getRequestItems();
            for (int i = 0; i < requests.length; i++) {
                long vhalRequestId = asyncRequestsHandler.getVhalRequestId(requests[i]);
                AsyncRequestInfo requestInfo = mPendingAsyncRequestPool.finishRequestIfFound(
                        vhalRequestId);
                if (requestInfo == null) {
                    Slogf.w(TAG,
                            "No pending request for ID: %s, possibly already timed out or "
                            + "the client already died", vhalRequestId);
                    continue;
                }
                asyncResultsHandler.addErrorResult(
                        vehicleStubCallback, requestInfo.getServiceRequestId(), errorCodes);
            }
        }
        asyncResultsHandler.callVehicleStubCallback();
    }

}
