/* * 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.hardware.CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_STATIC; 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.internal.common.CommonConstants.EMPTY_INT_ARRAY; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; import static com.android.car.internal.property.CarPropertyErrorCodes.convertVhalStatusCodeToCarPropertyManagerErrorCodes; import static com.android.car.internal.property.CarPropertyErrorCodes.STATUS_OK; import static com.android.car.internal.property.CarPropertyHelper.isSystemProperty; import static com.android.car.internal.property.GetSetValueResult.newGetValueResult; import static com.android.car.internal.property.InputSanitizationUtils.sanitizeUpdateRateHz; import android.annotation.IntDef; import android.annotation.Nullable; import android.car.VehiclePropertyIds; import android.car.builtin.os.BuildHelper; import android.car.builtin.util.Slogf; import android.car.hardware.CarPropertyConfig; import android.car.hardware.CarPropertyValue; import android.car.hardware.property.CarPropertyEvent; import android.car.hardware.property.CarPropertyManager; import android.car.hardware.property.CarPropertyManager.CarSetPropertyErrorCode; import android.car.hardware.property.VehicleHalStatusCode.VehicleHalStatusCodeInt; import android.content.Context; import android.hardware.automotive.vehicle.VehiclePropError; import android.hardware.automotive.vehicle.VehicleProperty; import android.hardware.automotive.vehicle.VehiclePropertyStatus; 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.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.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.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.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.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; // Only changed in testing. private PropertyHalServiceConfigs mPropertyHalServiceConfigs = PropertyHalServiceConfigs.getInstance(); @GuardedBy("mLock") private final PairSparseArray 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 public String toString() { if (requestId == CAR_PROP_SVC_REQUEST_ID) { return "PropertyHalService.subscribeProperty"; } return "PropertyHalService.setCarPropertyValuesAsync(requestId=" + requestId.toString() + ")"; } } private static final class GetSetValueResultWrapper { private GetSetValueResult mGetSetValueResult; private long mAsyncRequestStartTime; private final int mRetryCount; private GetSetValueResultWrapper(GetSetValueResult getSetValueResult, long asyncRequestStartTime, int retryCount) { mGetSetValueResult = getSetValueResult; mAsyncRequestStartTime = asyncRequestStartTime; mRetryCount = retryCount; } private GetSetValueResult getGetSetValueResult() { return mGetSetValueResult; } private long getAsyncRequestStartTime() { return mAsyncRequestStartTime; } private int getRetryCount() { return mRetryCount; } } 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(); } }; // 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 mResultBinderToVehicleStubCallback = new ArrayMap<>(); @GuardedBy("mLock") private final SparseArray 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 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> mHalPropIdToWaitingUpdateRequestInfo = new SparseArray<>(); // 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 mSubManager = new SubscriptionManager<>(); private class AsyncRequestTimeoutCallback implements TimeoutCallback { @Override public void onRequestsTimeout(List serviceRequestIds) { ArrayMap> 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.w(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)); } } } private class VehicleStubCallback extends VehicleStubCallbackInterface { private final IAsyncPropertyResultCallback mAsyncPropertyResultCallback; private final IBinder mClientBinder; VehicleStubCallback( IAsyncPropertyResultCallback asyncPropertyResultCallback) { mAsyncPropertyResultCallback = asyncPropertyResultCallback; mClientBinder = asyncPropertyResultCallback.asBinder(); } private static List logAndReturnResults(Histogram histogram, List getSetValueResultWrapperList, @AsyncRequestType int asyncRequestType) { List getSetValueResults = new ArrayList<>(); float systemCurrentTimeMillis = (float) System.currentTimeMillis(); for (int i = 0; i < getSetValueResultWrapperList.size(); i++) { GetSetValueResultWrapper getSetValueResultWrapper = getSetValueResultWrapperList.get(i); GetSetValueResult getSetValueResult = getSetValueResultWrapper .getGetSetValueResult(); histogram.logSample(systemCurrentTimeMillis - getSetValueResultWrapper.getAsyncRequestStartTime()); getSetValueResults.add(getSetValueResult); if (DBG) { Slogf.d(TAG, "E2E latency for %sPropertiesAsync for requestId: %d is %d", requestTypeToString(asyncRequestType), getSetValueResult.getRequestId(), getSetValueResultWrapper.getAsyncRequestStartTime()); } 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; } private void sendGetValueResults(List results) { if (results.isEmpty()) { return; } List 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 results) { if (results.isEmpty()) { return; } List 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 retryRequests) { List vehicleStubAsyncGetRequests = new ArrayList<>(); List timeoutGetResults = new ArrayList<>(); List vehicleStubAsyncSetRequests = new ArrayList<>(); List timeoutSetResults = new ArrayList<>(); List 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; } // This is a wrapper for death recipient that will unlink itself upon binder death. private final class DeathRecipientWrapper implements DeathRecipient { private DeathRecipient mInnerRecipient; DeathRecipientWrapper(DeathRecipient innerRecipient) { mInnerRecipient = innerRecipient; } @Override public void binderDied() { mInnerRecipient.binderDied(); mClientBinder.unlinkToDeath(this, /* flags= */ 0); } } @Override public void linkToDeath(DeathRecipient recipient) throws RemoteException { mClientBinder.linkToDeath(new DeathRecipientWrapper(recipient), /* flags= */ 0); } // Parses an async getProperty result and convert it to an okay/error result. private GetSetValueResult parseGetAsyncResults( GetVehicleStubAsyncResult getVehicleStubAsyncResult, AsyncPropRequestInfo clientRequestInfo) { int carPropMgrErrorCode = getVehicleStubAsyncResult.getErrorCode(); if (carPropMgrErrorCode != STATUS_OK) { // All other error results will be delivered back through callback. return clientRequestInfo.toErrorResult( getVehicleStubAsyncResult.getCarPropertyErrorCodes()); } // 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( new CarPropertyErrorCodes( CarPropertyManager.STATUS_ERROR_INTERNAL_ERROR, /* vendorErrorCode= */ 0, /* systemErrorCode= */ 0)); } HalPropValue halPropValue = getVehicleStubAsyncResult.getHalPropValue(); if (halPropValue.getStatus() == VehiclePropertyStatus.UNAVAILABLE) { return clientRequestInfo.toErrorResult( new CarPropertyErrorCodes( CarPropertyManager.STATUS_ERROR_NOT_AVAILABLE, /* vendorErrorCode= */ 0, /* systemErrorCode= */ 0)); } if (halPropValue.getStatus() != VehiclePropertyStatus.AVAILABLE) { return clientRequestInfo.toErrorResult( new CarPropertyErrorCodes( CarPropertyManager.STATUS_ERROR_INTERNAL_ERROR, /* vendorErrorCode= */ 0, /* systemErrorCode= */ 0)); } 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( new CarPropertyErrorCodes( CarPropertyManager.STATUS_ERROR_INTERNAL_ERROR, /* vendorErrorCode= */ 0, /* systemErrorCode= */ 0)); } } @Override public void onGetAsyncResults( List getVehicleStubAsyncResults) { List 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 setValueResults = new ArrayList<>(); List 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; } int carPropMgrErrorCode = getVehicleStubAsyncResult.getErrorCode(); if (carPropMgrErrorCode == CarPropertyErrorCodes.STATUS_TRY_AGAIN) { // 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())); 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. int errorCode = result.getCarPropertyErrorCodes() .getCarPropertyManagerErrorCode(); if (errorCode != STATUS_OK) { Slogf.w(TAG, "the init value get request: %s failed, ignore the result, " + "error: %d", clientRequestInfo, errorCode); 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())); removePendingAsyncPropRequestInfoLocked(assocSetValueRequestInfo); } } updateSubscriptionRateForAsyncSetRequestLocked(); } sendGetValueResults(getValueResults); sendSetValueResults(setValueResults); if (!retryRequests.isEmpty()) { mHandler.postDelayed(() -> { retryIfNotExpired(retryRequests); }, ASYNC_RETRY_SLEEP_IN_MS); } } @Override public void onSetAsyncResults( List setVehicleStubAsyncResults) { List setValueResults = new ArrayList<>(); List 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; } int carPropMgrErrorCode = setVehicleStubAsyncResult.getErrorCode(); if (carPropMgrErrorCode == CarPropertyErrorCodes.STATUS_TRY_AGAIN) { // The request might need to be retried. retryRequests.add(clientRequestInfo); removePendingAsyncPropRequestInfoLocked(clientRequestInfo); continue; } if (carPropMgrErrorCode != STATUS_OK) { // All other error results will be delivered back through callback. setValueResults.add(new GetSetValueResultWrapper(clientRequestInfo .toErrorResult( setVehicleStubAsyncResult.getCarPropertyErrorCodes()), clientRequestInfo.getAsyncRequestStartTime(), clientRequestInfo.getRetryCount())); 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())); } } updateSubscriptionRateForAsyncSetRequestLocked(); } sendSetValueResults(setValueResults); if (!retryRequests.isEmpty()) { mHandler.postDelayed(() -> { retryIfNotExpired(retryRequests); }, ASYNC_RETRY_SLEEP_IN_MS); } } private void generateTimeoutResult(AsyncPropRequestInfo requestInfo, List timeoutGetResults, List timeoutSetResults) { GetSetValueResult timeoutResult = requestInfo.toErrorResult( new CarPropertyErrorCodes( CarPropertyManager.STATUS_ERROR_TIMEOUT, /* vendorErrorCode= */ 0, /* systemErrorCode= */ 0)); 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())); 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())); break; } } @Override public void onRequestsTimeout(List serviceRequestIds) { List timeoutGetResults = new ArrayList<>(); List timeoutSetResults = new ArrayList<>(); synchronized (mLock) { for (int i = 0; i < serviceRequestIds.size(); i++) { int serviceRequestId = serviceRequestIds.get(i); AsyncPropRequestInfo requestInfo = getAndRemovePendingAsyncPropRequestInfoLocked(serviceRequestId); if (requestInfo == null) { Slogf.w(TAG, "Service request ID %d time out but no " + "pending request is found. The request may have already been " + "cancelled 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}. * *

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.w(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) { AsyncPropRequestInfo requestInfo = getPendingAsyncPropRequestInfoLocked(serviceRequestId); if (requestInfo == null) { Slogf.w(TAG, "the request for propertyHalService request " + "ID: %d already timed out or already completed", serviceRequestId); return null; } removePendingAsyncPropRequestInfoLocked(requestInfo); 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) { int serviceRequestId = pendingRequest.getServiceRequestId(); mPendingAsyncRequests.removeRequest(serviceRequestId); 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(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 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 List of configs available. */ public SparseArray> getPropertyList() { if (DBG) { Slogf.d(TAG, "getPropertyList"); } synchronized (mLock) { SparseArray> 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: " + 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 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(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 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 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"); } } } private static void storeResultForRequest(GetSetValueResult result, AsyncPropRequestInfo request, Map> 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())); } /** * 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> callbackToSetValueResults) { List pendingSetRequests = mHalPropIdToWaitingUpdateRequestInfo.get( halPropId); if (pendingSetRequests == null) { return; } List 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 toHalSubscribeOptions( ArrayList carSubscriptions) { ArrayList 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: * *

    *
  1. Lock is obtained by thread 1. *
  2. mSubManager is updated by one thread to state 1. *
  3. New update rate (local variable) is calculated based on state 1. *
  4. Lock is released by thread 1. *
  5. Lock is obtained by thread 2. *
  6. mSubManager is updated by thread 2 to state 2. *
  7. New update rate (local variable) is calculated based on state 2. *
  8. Lock is released by thread 2. *
  9. Thread 2 calls subscribeProperty to VHAL based on state 2. *
  10. Thread 1 calls subscribeProperty to VHAL based on state 1. *
  11. 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 diffSubscribeOptions = new ArrayList<>(); List 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 halPropValues) { List eventsToDispatch = new ArrayList<>(); // A map to store potential succeeded set value results which is caused by the values // updated to the target values. Map> 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)); } } 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 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> 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 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 = convertVhalStatusCodeToCarPropertyManagerErrorCodes( vehiclePropError.errorCode); GetSetValueResult errorResult = pendingRequest.toErrorResult( carPropertyErrorCodes); Slogf.w(TAG, "Pending async set request received property set error with " + "error: %d, vendor error code: %d, fail the pending request: %s", carPropertyErrorCodes.getCarPropertyManagerErrorCode(), carPropertyErrorCodes.getVendorErrorCode(), 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 prepareVehicleStubRequests(@AsyncRequestType int requestType, List serviceRequests, long timeoutInMs, VehicleStubCallback vehicleStubCallback, @Nullable List assocSetValueRequestInfo, @Nullable List outRequestInfo, long asyncRequestStartTime) { // TODO(b/242326085): Change local variables into memory pool to reduce memory // allocation/release cycle List vehicleStubRequests = new ArrayList<>(); List pendingRequestInfo = new ArrayList<>(); List 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)); 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 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. * *

    This method gets the CarPropertyValue using async methods.

    */ public void getCarPropertyValuesAsync( List serviceRequests, IAsyncPropertyResultCallback asyncPropertyResultCallback, long timeoutInMs, long asyncRequestStartTime) { VehicleStubCallback vehicleStubCallback = createVehicleStubCallback( asyncPropertyResultCallback); List vehicleStubRequests = prepareVehicleStubRequests( GET, serviceRequests, timeoutInMs, vehicleStubCallback, /* assocSetValueRequestInfo= */ null, /* outRequestInfo= */ null, asyncRequestStartTime); if (vehicleStubRequests.isEmpty()) { return; } sendVehicleStubRequests(GET, vehicleStubRequests, vehicleStubCallback); } private List filterWaitForUpdateRequests(List requests, Function isWaitForPropertyUpdate) { List 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 serviceRequests, VehicleStubCallback vehicleStubCallback, long timeoutInMs, List waitForUpdateSetRequestInfo, long asyncRequestStartTime) { // Stores a list of async GET_INITIAL_VALUE request to be sent. List 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 serviceRequests, IAsyncPropertyResultCallback asyncPropertyResultCallback, long timeoutInMs, long asyncRequestStartTime) { List pendingSetRequestInfo = new ArrayList<>(); VehicleStubCallback vehicleStubCallback = createVehicleStubCallback( asyncPropertyResultCallback); List setValueRequests = prepareVehicleStubRequests( SET, serviceRequests, timeoutInMs, vehicleStubCallback, /* assocSetValueRequestInfo= */ null, /* outRequestInfo= */ pendingSetRequestInfo, asyncRequestStartTime); List waitForUpdateSetRequestInfo = filterWaitForUpdateRequests( pendingSetRequestInfo, (request) -> request.isWaitForPropertyUpdate()); if (waitForUpdateSetRequestInfo.size() != 0) { List 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 serviceRequestIdsToCancel = new ArrayList<>(); Set 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 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 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(); } } 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); } }