PropertyManager.java

/*
 * Copyright 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 androidx.car.app.hardware.common;

import static androidx.annotation.RestrictTo.Scope.LIBRARY;

import android.car.VehicleAreaType;
import android.car.hardware.CarPropertyValue;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;
import android.util.Pair;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.car.app.utils.LogTags;
import androidx.concurrent.futures.CallbackToFutureAdapter;

import com.google.common.util.concurrent.ListenableFuture;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Manages the communication between the apps and backend Android platform car service.
 *
 * <p>It takes requests from {@link androidx.car.app.hardware.info.AutomotiveCarInfo}, handles the
 * conversion between vehicle zone and areaId, checks preconditions. After that, it uses
 * {@link PropertyRequestProcessor} to complete the request.
 *
 * @hide
 */
@RestrictTo(LIBRARY)
public class PropertyManager {
    private final Context mContext;
    final PropertyRequestProcessor mPropertyRequestProcessor;
    final Object mLock = new Object();

    // The callback is used by the CarPropertyManager to update property values in the
    // PropertyResponseCache
    final PropertyProcessorCallback mPropertyProcessorCallback = new PropertyProcessorCallback();

    /*
     * The cache contains listeners and properties that registered by listeners. It shares the same
     * lock with the map for active listeners. Needs to update the cache and the actively
     * listener map together.
     */
    @GuardedBy("mLock")
    final PropertyResponseCache mListenerAndResponseCache = new PropertyResponseCache();

    /*
     * Contains registered listeners and the interval time for sampling data to listeners. It
     * shares the same lock with the property value cache. Needs to update the cache and the
     * actively listener map together.
     */
    @GuardedBy("mLock")
    final Map<OnCarPropertyResponseListener, Long> mListenerToSamplingIntervalMap = new HashMap<>();

    // Executor has two threads for dispatching response and unregister properties.
    final ScheduledExecutorService mScheduledExecutorService =
            Executors.newScheduledThreadPool(/* corePoolSize= */2);

    public PropertyManager(@NonNull Context context) {
        mContext = context;
        mPropertyRequestProcessor = new PropertyRequestProcessor(context,
                mPropertyProcessorCallback);
    }

    /**
     * Submits a request for registering the listener to get property updates.
     *
     * @param propertyIds           a list of property id in {@link android.car.VehiclePropertyIds}
     * @param sampleRate            sample rate in Hz
     * @param listener              {@link OnCarPropertyResponseListener}
     * @param executor              execute the task for registering properties
     * @throws SecurityException    if the application did not grant permissions for
     *                              registering properties
     */
    @SuppressWarnings("FutureReturnValueIgnored")
    public void submitRegisterListenerRequest(@NonNull List<Integer> propertyIds, float sampleRate,
            @NonNull OnCarPropertyResponseListener listener, @NonNull Executor executor) {
        checkPermissions(propertyIds);
        long samplingIntervalMs;
        synchronized (mLock) {
            mListenerAndResponseCache.putListenerAndPropertyIds(listener, propertyIds);
            if (sampleRate == 0) {
                throw new IllegalArgumentException("Sample rate cannot be zero.");
            }
            samplingIntervalMs = (long) (1000 / sampleRate);
            mListenerToSamplingIntervalMap.put(listener, samplingIntervalMs);
        }

        // register properties
        executor.execute(() -> {
            for (int propertyId : propertyIds) {
                try {
                    mPropertyRequestProcessor.registerProperty(propertyId, sampleRate);
                } catch (IllegalArgumentException e) {
                    // the property is not implemented
                    Log.e(LogTags.TAG_CAR_HARDWARE,
                            "Failed to register for property: " + propertyId, e);
                    mPropertyProcessorCallback.onErrorEvent(CarInternalError.create(propertyId,
                            VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL,
                            CarValue.STATUS_UNIMPLEMENTED));
                } catch (Exception e) {
                    Log.e(LogTags.TAG_CAR_HARDWARE,
                            "Failed to register for property: " + propertyId, e);
                    mPropertyProcessorCallback.onErrorEvent(CarInternalError.create(propertyId,
                            VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL, CarValue.STATUS_UNAVAILABLE));
                }
            }
        });
        mScheduledExecutorService.schedule(()-> dispatchResponseWithDelay(listener),
                samplingIntervalMs, TimeUnit.MILLISECONDS);
    }

    /**
     * Submits a request for unregistering the listener to get property updates.
     *
     * @param listener                  {@link OnCarPropertyResponseListener} to be unregistered.
     * @throws IllegalStateException    if the listener was not registered yet
     * @throws SecurityException        if the application did not grant permissions for
     *                                  unregistering properties
     */
    public void submitUnregisterListenerRequest(@NonNull OnCarPropertyResponseListener listener) {
        List<Integer> propertyIds;
        List<Integer> propertyIdsToBeUnregistered;
        synchronized (mLock) {
            propertyIds = mListenerAndResponseCache.getPropertyIdsByListener(listener);
            if (propertyIds == null) {
                throw new IllegalStateException("Listener was not registered yet.");
            }
            propertyIdsToBeUnregistered = mListenerAndResponseCache.removeListener(listener);
            mListenerToSamplingIntervalMap.remove(listener);
        }
        if (propertyIdsToBeUnregistered.size() != 0) {
            mScheduledExecutorService.execute(() -> {
                for (int propertyId : propertyIdsToBeUnregistered) {
                    mPropertyRequestProcessor.unregisterProperty(propertyId);
                }
            });
        }
    }

    /**
     * Submits {@link CarPropertyResponse} for getting property values.
     *
     * @param rawRequests           a list of {@link GetPropertyRequest}
     * @param executor              executes the expensive operation such as fetching property
     *                              values from cars
     * @throws SecurityException    if the application did not grant permissions for getting
     *                              property
     *
     * @return {@link ListenableFuture} contains a list of {@link CarPropertyResponse}
     */
    @NonNull
    public ListenableFuture<List<CarPropertyResponse<?>>> submitGetPropertyRequest(
            @NonNull List<GetPropertyRequest> rawRequests, @NonNull Executor executor) {
        List<Integer> propertyIds = new ArrayList<>();
        for (GetPropertyRequest request : rawRequests) {
            propertyIds.add(request.getPropertyId());
        }
        checkPermissions(propertyIds);
        List<Pair<Integer, Integer>> requests = parseRawRequest(rawRequests);
        return CallbackToFutureAdapter.getFuture(completer -> {
            // Getting properties' value is expensive operation.
            executor.execute(() ->
                    mPropertyRequestProcessor.fetchCarPropertyValues(requests, (values, errors) ->
                                    completer.set(createResponses(values, errors))));
            return "Get property values done";
        });
    }

    /**
     * Dispatches a list of {@link CarPropertyResponse} without delay.
     *
     * <p>For on_change properties and error events, dispatches them to listeners without delay.
     *
     * @param propertyId property id in {@link android.car.VehiclePropertyIds}
     */
    void dispatchResponsesWithoutDelay(int propertyId) {
        synchronized (mLock) {
            Set<OnCarPropertyResponseListener> listeners =
                    mListenerAndResponseCache.getListenersByPropertyId(propertyId);
            if (listeners == null) {
                return;
            }
            for (OnCarPropertyResponseListener listener : listeners) {
                // build the group of property
                List<CarPropertyResponse<?>> propertyResponses =
                        mListenerAndResponseCache.getResponsesByListener(listener);
                if (propertyResponses != null) {
                    listener.onCarPropertyResponses(propertyResponses);
                }
            }
        }
    }

    /**
     * Dispatches {@link CarPropertyResponse} to the listener in {@link DelayQueue}.
     */
    @SuppressWarnings("FutureReturnValueIgnored")
    void dispatchResponseWithDelay(OnCarPropertyResponseListener listener) {
        List<CarPropertyResponse<?>> propertyResponses = null;
        Long delayTime;
        synchronized (mLock) {
            delayTime = mListenerToSamplingIntervalMap.get(listener);
            if (delayTime != null) {
                propertyResponses = mListenerAndResponseCache.getResponsesByListener(listener);

                //Schedules for next dispatch
                mScheduledExecutorService.schedule(()-> dispatchResponseWithDelay(listener),
                        delayTime, TimeUnit.MILLISECONDS);
            }
        }
        if (propertyResponses != null) {
            listener.onCarPropertyResponses(propertyResponses);
        }
    }

    /**
     * Registers all properties in the car service.
     *
     * <p>The callback updates the value in cache. For on_change and error events, the callback
     * trigger dispatching task without delay.
     */
    class PropertyProcessorCallback extends PropertyRequestProcessor.PropertyEventCallback {
        @Override
        public void onChangeEvent(CarPropertyValue carPropertyValue) {
            synchronized (mLock) {
                // check timestamp
                if (mListenerAndResponseCache.updateResponseIfNeeded(carPropertyValue)) {
                    int propertyId = carPropertyValue.getPropertyId();
                    if (PropertyUtils.isOnChangeProperty(propertyId)) {
                        mScheduledExecutorService.execute(() ->
                                dispatchResponsesWithoutDelay(propertyId));
                    }
                }
            }
        }

        @Override
        public void onErrorEvent(CarInternalError carInternalError) {
            synchronized (mLock) {
                mListenerAndResponseCache.updateInternalError(carInternalError);
                mScheduledExecutorService.execute(() ->
                        dispatchResponsesWithoutDelay(carInternalError.getPropertyId()));
            }
        }
    }

    private static List<CarPropertyResponse<?>> createResponses(
            List<CarPropertyValue<?>> propertyValues, List<CarInternalError> propertyErrors) {
        // TODO(b/190869722): handle AreaId to VehicleZone map in V1.2
        List<CarPropertyResponse<?>> carResponses = new ArrayList<>();
        for (CarPropertyValue<?> value : propertyValues) {
            int statusCode = PropertyUtils.mapToStatusCodeInCarValue(value.getStatus());
            long timeInMillis = TimeUnit.MILLISECONDS.convert(value.getTimestamp(),
                    TimeUnit.NANOSECONDS);
            carResponses.add(CarPropertyResponse.create(
                    value.getPropertyId(), statusCode, timeInMillis, value.getValue()));
        }
        for (CarInternalError error: propertyErrors) {
            carResponses.add(CarPropertyResponse.createErrorResponse(error.getPropertyId(),
                    error.getErrorCode()));
        }
        return carResponses;
    }

    // Maps VehicleZones to AreaIds.
    private List<Pair<Integer, Integer>> parseRawRequest(List<GetPropertyRequest> requestList) {
        List<Pair<Integer, Integer>> requestsWithAreaId = new ArrayList<>(requestList.size());
        for (GetPropertyRequest request : requestList) {
            if (PropertyUtils.isGlobalProperty(request.getPropertyId())) {
                // ignore the VehicleZone, set areaId to 0.
                requestsWithAreaId.add(new Pair<>(request.getPropertyId(), 0));
            }
        }
        return requestsWithAreaId;
    }

    private void checkPermissions(List<Integer> propertyIds) {
        Set<String> requiredPermission = PropertyUtils.getReadPermissionsByPropertyIds(propertyIds);
        for (String permission : requiredPermission) {
            if (mContext.checkCallingOrSelfPermission(permission)
                    != PackageManager.PERMISSION_GRANTED) {
                throw new SecurityException("Missed permission: " + permission);
            }
        }
    }
}