public class

PropertyManager

extends java.lang.Object

 java.lang.Object

↳androidx.car.app.hardware.common.PropertyManager

Gradle dependencies

compile group: 'androidx.car.app', name: 'app-automotive', version: '1.2.0-rc01'

  • groupId: androidx.car.app
  • artifactId: app-automotive
  • version: 1.2.0-rc01

Artifact androidx.car.app:app-automotive:1.2.0-rc01 it located at Google repository (https://maven.google.com/)

Overview

Manages the communication between the apps and backend Android platform car service.

It takes requests from AutomotiveCarInfo, handles the conversion between vehicle zone and areaId, checks preconditions. After that, it uses androidx.car.app.hardware.common.PropertyRequestProcessor to complete the request.

Summary

Constructors
publicPropertyManager(Context context)

Methods
public <any>submitGetPropertyRequest(java.util.List<GetPropertyRequest> rawRequests, java.util.concurrent.Executor executor)

Submits CarPropertyResponse for getting property values.

public voidsubmitRegisterListenerRequest(java.util.List<java.lang.Integer> propertyIds, float sampleRate, OnCarPropertyResponseListener listener, java.util.concurrent.Executor executor)

Submits a request for registering the listener to get property updates.

public voidsubmitUnregisterListenerRequest(OnCarPropertyResponseListener listener)

Submits a request for unregistering the listener to get property updates.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public PropertyManager(Context context)

Methods

public void submitRegisterListenerRequest(java.util.List<java.lang.Integer> propertyIds, float sampleRate, OnCarPropertyResponseListener listener, java.util.concurrent.Executor executor)

Submits a request for registering the listener to get property updates.

Parameters:

propertyIds: a list of property id in
sampleRate: sample rate in Hz
listener: OnCarPropertyResponseListener
executor: execute the task for registering properties

public void submitUnregisterListenerRequest(OnCarPropertyResponseListener listener)

Submits a request for unregistering the listener to get property updates.

Parameters:

listener: OnCarPropertyResponseListener to be unregistered.

public <any> submitGetPropertyRequest(java.util.List<GetPropertyRequest> rawRequests, java.util.concurrent.Executor executor)

Submits CarPropertyResponse for getting property values.

Parameters:

rawRequests: a list of GetPropertyRequest
executor: executes the expensive operation such as fetching property values from cars

Returns:

contains a list of CarPropertyResponse

Source

/*
 * 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);
            }
        }
    }
}