| /* |
| * 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; |
| |
| import android.annotation.Nullable; |
| import android.car.Car; |
| import android.car.VehicleAreaType; |
| import android.car.drivingstate.CarDrivingStateEvent; |
| import android.car.drivingstate.CarDrivingStateEvent.CarDrivingState; |
| import android.car.drivingstate.ICarDrivingState; |
| import android.car.drivingstate.ICarDrivingStateChangeListener; |
| import android.car.hardware.CarPropertyConfig; |
| import android.car.hardware.CarPropertyValue; |
| import android.car.hardware.property.CarPropertyEvent; |
| import android.car.hardware.property.ICarPropertyEventListener; |
| import android.content.Context; |
| import android.hardware.automotive.vehicle.V2_0.VehicleGear; |
| import android.hardware.automotive.vehicle.V2_0.VehicleProperty; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.RemoteCallbackList; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.util.IndentingPrintWriter; |
| import android.util.Slog; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.util.LinkedList; |
| import java.util.List; |
| |
| /** |
| * A service that infers the current driving state of the vehicle. It computes the driving state |
| * from listening to relevant properties from {@link CarPropertyService} |
| */ |
| public class CarDrivingStateService extends ICarDrivingState.Stub implements CarServiceBase { |
| private static final String TAG = CarLog.tagFor(CarDrivingStateService.class); |
| private static final boolean DBG = false; |
| private static final int MAX_TRANSITION_LOG_SIZE = 20; |
| private static final int PROPERTY_UPDATE_RATE = 5; // Update rate in Hz |
| private static final int NOT_RECEIVED = -1; |
| private final Context mContext; |
| private final CarPropertyService mPropertyService; |
| // List of clients listening to driving state events. |
| private final RemoteCallbackList<ICarDrivingStateChangeListener> mDrivingStateClients = |
| new RemoteCallbackList<>(); |
| // Array of properties that the service needs to listen to from CarPropertyService for deriving |
| // the driving state. |
| private static final int[] REQUIRED_PROPERTIES = { |
| VehicleProperty.PERF_VEHICLE_SPEED, |
| VehicleProperty.GEAR_SELECTION, |
| VehicleProperty.PARKING_BRAKE_ON}; |
| private final HandlerThread mClientDispatchThread = CarServiceUtils.getHandlerThread( |
| getClass().getSimpleName()); |
| private final Handler mClientDispatchHandler = new Handler(mClientDispatchThread.getLooper()); |
| private final Object mLock = new Object(); |
| |
| // For dumpsys logging |
| @GuardedBy("mLock") |
| private final LinkedList<Utils.TransitionLog> mTransitionLogs = new LinkedList<>(); |
| |
| @GuardedBy("mLock") |
| private int mLastGear; |
| |
| @GuardedBy("mLock") |
| private long mLastGearTimestamp = NOT_RECEIVED; |
| |
| @GuardedBy("mLock") |
| private float mLastSpeed; |
| |
| @GuardedBy("mLock") |
| private long mLastSpeedTimestamp = NOT_RECEIVED; |
| |
| @GuardedBy("mLock") |
| private boolean mLastParkingBrakeState; |
| |
| @GuardedBy("mLock") |
| private long mLastParkingBrakeTimestamp = NOT_RECEIVED; |
| |
| @GuardedBy("mLock") |
| private List<Integer> mSupportedGears; |
| |
| @GuardedBy("mLock") |
| private CarDrivingStateEvent mCurrentDrivingState; |
| |
| public CarDrivingStateService(Context context, CarPropertyService propertyService) { |
| mContext = context; |
| mPropertyService = propertyService; |
| mCurrentDrivingState = createDrivingStateEvent(CarDrivingStateEvent.DRIVING_STATE_UNKNOWN); |
| } |
| |
| @Override |
| public void init() { |
| if (!checkPropertySupport()) { |
| Slog.e(TAG, "init failure. Driving state will always be fully restrictive"); |
| return; |
| } |
| // Gets the boot state first, before getting any events from car. |
| synchronized (mLock) { |
| mCurrentDrivingState = createDrivingStateEvent(inferDrivingStateLocked()); |
| addTransitionLogLocked(TAG + " Boot", CarDrivingStateEvent.DRIVING_STATE_UNKNOWN, |
| mCurrentDrivingState.eventValue, mCurrentDrivingState.timeStamp); |
| } |
| subscribeToProperties(); |
| } |
| |
| @Override |
| public void release() { |
| for (int property : REQUIRED_PROPERTIES) { |
| mPropertyService.unregisterListener(property, mICarPropertyEventListener); |
| } |
| while (mDrivingStateClients.getRegisteredCallbackCount() > 0) { |
| for (int i = mDrivingStateClients.getRegisteredCallbackCount() - 1; i >= 0; i--) { |
| ICarDrivingStateChangeListener client = |
| mDrivingStateClients.getRegisteredCallbackItem(i); |
| if (client == null) { |
| continue; |
| } |
| mDrivingStateClients.unregister(client); |
| } |
| } |
| mCurrentDrivingState = createDrivingStateEvent(CarDrivingStateEvent.DRIVING_STATE_UNKNOWN); |
| } |
| |
| /** |
| * Checks if the {@link CarPropertyService} supports the required properties. |
| * |
| * @return {@code true} if supported, {@code false} if not |
| */ |
| private boolean checkPropertySupport() { |
| List<CarPropertyConfig> configs = mPropertyService |
| .getPropertyConfigList(REQUIRED_PROPERTIES); |
| for (int propertyId : REQUIRED_PROPERTIES) { |
| boolean found = false; |
| for (CarPropertyConfig config : configs) { |
| if (config.getPropertyId() == propertyId) { |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| Slog.e(TAG, "Required property not supported: " + propertyId); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Subscribe to the {@link CarPropertyService} for required sensors. |
| */ |
| private void subscribeToProperties() { |
| for (int propertyId : REQUIRED_PROPERTIES) { |
| mPropertyService.registerListener(propertyId, PROPERTY_UPDATE_RATE, |
| mICarPropertyEventListener); |
| } |
| |
| } |
| |
| // Binder methods |
| |
| /** |
| * Register a {@link ICarDrivingStateChangeListener} to be notified for changes to the driving |
| * state. |
| * |
| * @param listener {@link ICarDrivingStateChangeListener} |
| */ |
| @Override |
| public void registerDrivingStateChangeListener(ICarDrivingStateChangeListener listener) { |
| if (listener == null) { |
| if (DBG) { |
| Slog.e(TAG, "registerDrivingStateChangeListener(): listener null"); |
| } |
| throw new IllegalArgumentException("Listener is null"); |
| } |
| mDrivingStateClients.register(listener); |
| } |
| |
| /** |
| * Unregister the given Driving State Change listener |
| * |
| * @param listener client to unregister |
| */ |
| @Override |
| public void unregisterDrivingStateChangeListener(ICarDrivingStateChangeListener listener) { |
| if (listener == null) { |
| Slog.e(TAG, "unregisterDrivingStateChangeListener(): listener null"); |
| throw new IllegalArgumentException("Listener is null"); |
| } |
| |
| mDrivingStateClients.unregister(listener); |
| } |
| |
| /** |
| * Gets the current driving state |
| * |
| * @return {@link CarDrivingStateEvent} for the given event type |
| */ |
| @Override |
| @Nullable |
| public CarDrivingStateEvent getCurrentDrivingState() { |
| synchronized (mLock) { |
| return mCurrentDrivingState; |
| } |
| } |
| |
| @Override |
| public void injectDrivingState(CarDrivingStateEvent event) { |
| ICarImpl.assertPermission(mContext, Car.PERMISSION_CONTROL_APP_BLOCKING); |
| |
| dispatchEventToClients(event); |
| } |
| |
| private void dispatchEventToClients(CarDrivingStateEvent event) { |
| boolean success = mClientDispatchHandler.post(() -> { |
| int numClients = mDrivingStateClients.beginBroadcast(); |
| for (int i = 0; i < numClients; i++) { |
| ICarDrivingStateChangeListener callback = mDrivingStateClients.getBroadcastItem(i); |
| try { |
| callback.onDrivingStateChanged(event); |
| } catch (RemoteException e) { |
| Slog.e(TAG, |
| String.format("Dispatch to listener %s failed for event (%s)", callback, |
| event)); |
| } |
| } |
| mDrivingStateClients.finishBroadcast(); |
| }); |
| |
| if (!success) { |
| Slog.e(TAG, "Unable to post (" + event + ") event to dispatch handler"); |
| } |
| } |
| |
| @Override |
| public void dump(IndentingPrintWriter writer) { |
| writer.println("*CarDrivingStateService*"); |
| mDrivingStateClients.dump(writer, "Driving State Clients "); |
| writer.println("Driving state change log:"); |
| synchronized (mLock) { |
| for (Utils.TransitionLog tLog : mTransitionLogs) { |
| writer.println(tLog); |
| } |
| writer.println("Current Driving State: " + mCurrentDrivingState.eventValue); |
| if (mSupportedGears != null) { |
| writer.println("Supported gears:"); |
| for (Integer gear : mSupportedGears) { |
| writer.print("Gear:" + gear); |
| } |
| } |
| } |
| } |
| |
| /** |
| * {@link CarPropertyEvent} listener registered with the {@link CarPropertyService} for getting |
| * property change notifications. |
| */ |
| private final ICarPropertyEventListener mICarPropertyEventListener = |
| new ICarPropertyEventListener.Stub() { |
| @Override |
| public void onEvent(List<CarPropertyEvent> events) throws RemoteException { |
| synchronized (mLock) { |
| for (CarPropertyEvent event : events) { |
| handlePropertyEventLocked(event); |
| } |
| } |
| } |
| }; |
| |
| /** |
| * Handle events coming from {@link CarPropertyService}. Compute the driving state, map it to |
| * the corresponding UX Restrictions and dispatch the events to the registered clients. |
| */ |
| @VisibleForTesting |
| void handlePropertyEventLocked(CarPropertyEvent event) { |
| if (event.getEventType() != CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE) { |
| return; |
| } |
| CarPropertyValue value = event.getCarPropertyValue(); |
| int propId = value.getPropertyId(); |
| long curTimestamp = value.getTimestamp(); |
| if (DBG) { |
| Slog.d(TAG, "Property Changed: propId=" + propId); |
| } |
| switch (propId) { |
| case VehicleProperty.PERF_VEHICLE_SPEED: |
| float curSpeed = (Float) value.getValue(); |
| if (DBG) { |
| Slog.d(TAG, "Speed: " + curSpeed + "@" + curTimestamp); |
| } |
| if (curTimestamp > mLastSpeedTimestamp) { |
| mLastSpeedTimestamp = curTimestamp; |
| mLastSpeed = curSpeed; |
| } else if (DBG) { |
| Slog.d(TAG, "Ignoring speed with older timestamp:" + curTimestamp); |
| } |
| break; |
| case VehicleProperty.GEAR_SELECTION: |
| if (mSupportedGears == null) { |
| mSupportedGears = getSupportedGears(); |
| } |
| int curGear = (Integer) value.getValue(); |
| if (DBG) { |
| Slog.d(TAG, "Gear: " + curGear + "@" + curTimestamp); |
| } |
| if (curTimestamp > mLastGearTimestamp) { |
| mLastGearTimestamp = curTimestamp; |
| mLastGear = (Integer) value.getValue(); |
| } else if (DBG) { |
| Slog.d(TAG, "Ignoring Gear with older timestamp:" + curTimestamp); |
| } |
| break; |
| case VehicleProperty.PARKING_BRAKE_ON: |
| boolean curParkingBrake = (boolean) value.getValue(); |
| if (DBG) { |
| Slog.d(TAG, "Parking Brake: " + curParkingBrake + "@" + curTimestamp); |
| } |
| if (curTimestamp > mLastParkingBrakeTimestamp) { |
| mLastParkingBrakeTimestamp = curTimestamp; |
| mLastParkingBrakeState = curParkingBrake; |
| } else if (DBG) { |
| Slog.d(TAG, "Ignoring Parking Brake status with an older timestamp:" |
| + curTimestamp); |
| } |
| break; |
| default: |
| Slog.e(TAG, "Received property event for unhandled propId=" + propId); |
| break; |
| } |
| |
| int drivingState = inferDrivingStateLocked(); |
| // Check if the driving state has changed. If it has, update our records and |
| // dispatch the new events to the listeners. |
| if (DBG) { |
| Slog.d(TAG, "Driving state new->old " + drivingState + "->" |
| + mCurrentDrivingState.eventValue); |
| } |
| if (drivingState != mCurrentDrivingState.eventValue) { |
| addTransitionLogLocked(TAG, mCurrentDrivingState.eventValue, drivingState, |
| System.currentTimeMillis()); |
| // Update if there is a change in state. |
| mCurrentDrivingState = createDrivingStateEvent(drivingState); |
| if (DBG) { |
| Slog.d(TAG, "dispatching to " + mDrivingStateClients.getRegisteredCallbackCount() |
| + " clients"); |
| } |
| // Dispatch to clients on a separate thread to prevent a deadlock |
| final CarDrivingStateEvent currentDrivingStateEvent = mCurrentDrivingState; |
| dispatchEventToClients(currentDrivingStateEvent); |
| } |
| } |
| |
| private List<Integer> getSupportedGears() { |
| List<CarPropertyConfig> propertyList = mPropertyService |
| .getPropertyConfigList(REQUIRED_PROPERTIES); |
| for (CarPropertyConfig p : propertyList) { |
| if (p.getPropertyId() == VehicleProperty.GEAR_SELECTION) { |
| return p.getConfigArray(); |
| } |
| } |
| return null; |
| } |
| |
| @GuardedBy("mLock") |
| private void addTransitionLogLocked(String name, int from, int to, long timestamp) { |
| if (mTransitionLogs.size() >= MAX_TRANSITION_LOG_SIZE) { |
| mTransitionLogs.remove(); |
| } |
| |
| Utils.TransitionLog tLog = new Utils.TransitionLog(name, from, to, timestamp); |
| mTransitionLogs.add(tLog); |
| } |
| |
| /** |
| * Infers the current driving state of the car from the other Car Sensor properties like |
| * Current Gear, Speed etc. |
| * |
| * @return Current driving state |
| */ |
| @GuardedBy("mLock") |
| @CarDrivingState |
| private int inferDrivingStateLocked() { |
| updateVehiclePropertiesIfNeededLocked(); |
| if (DBG) { |
| Slog.d(TAG, "Last known Gear:" + mLastGear + " Last known speed:" + mLastSpeed); |
| } |
| |
| /* |
| Logic to start off deriving driving state: |
| 1. If gear == parked, then Driving State is parked. |
| 2. If gear != parked, |
| 2a. if parking brake is applied, then Driving state is parked. |
| 2b. if parking brake is not applied or unknown/unavailable, then driving state |
| is still unknown. |
| 3. If driving state is unknown at the end of step 2, |
| 3a. if speed == 0, then driving state is idling |
| 3b. if speed != 0, then driving state is moving |
| 3c. if speed unavailable, then driving state is unknown |
| */ |
| |
| if (isVehicleKnownToBeParkedLocked()) { |
| return CarDrivingStateEvent.DRIVING_STATE_PARKED; |
| } |
| |
| // We don't know if the vehicle is parked, let's look at the speed. |
| if (mLastSpeedTimestamp == NOT_RECEIVED || mLastSpeed < 0) { |
| return CarDrivingStateEvent.DRIVING_STATE_UNKNOWN; |
| } else if (mLastSpeed == 0f) { |
| return CarDrivingStateEvent.DRIVING_STATE_IDLING; |
| } else { |
| return CarDrivingStateEvent.DRIVING_STATE_MOVING; |
| } |
| } |
| |
| /** |
| * Find if we have signals to know if the vehicle is parked |
| * |
| * @return true if we have enough information to say the vehicle is parked. |
| * false, if the vehicle is either not parked or if we don't have any information. |
| */ |
| @GuardedBy("mLock") |
| private boolean isVehicleKnownToBeParkedLocked() { |
| // If we know the gear is in park, return true |
| if (mLastGearTimestamp != NOT_RECEIVED && mLastGear == VehicleGear.GEAR_PARK) { |
| return true; |
| } else if (mLastParkingBrakeTimestamp != NOT_RECEIVED) { |
| // if gear is not in park or unknown, look for status of parking brake if transmission |
| // type is manual. |
| if (isCarManualTransmissionTypeLocked()) { |
| return mLastParkingBrakeState; |
| } |
| } |
| // if neither information is available, return false to indicate we can't determine |
| // if the vehicle is parked. |
| return false; |
| } |
| |
| /** |
| * If Supported gears information is available and GEAR_PARK is not one of the supported gears, |
| * transmission type is considered to be Manual. Automatic transmission is assumed otherwise. |
| */ |
| @GuardedBy("mLock") |
| private boolean isCarManualTransmissionTypeLocked() { |
| if (mSupportedGears != null |
| && !mSupportedGears.isEmpty() |
| && !mSupportedGears.contains(VehicleGear.GEAR_PARK)) { |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Try querying the gear selection and parking brake if we haven't received the event yet. |
| * This could happen if the gear change occurred before car service booted up like in the |
| * case of a HU restart in the middle of a drive. Since gear and parking brake are |
| * on-change only properties, we could be in this situation where we will have to query |
| * VHAL. |
| */ |
| @GuardedBy("mLock") |
| private void updateVehiclePropertiesIfNeededLocked() { |
| if (mLastGearTimestamp == NOT_RECEIVED) { |
| CarPropertyValue propertyValue = mPropertyService.getPropertySafe( |
| VehicleProperty.GEAR_SELECTION, |
| VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL); |
| if (propertyValue != null) { |
| mLastGear = (Integer) propertyValue.getValue(); |
| mLastGearTimestamp = propertyValue.getTimestamp(); |
| if (DBG) { |
| Slog.d(TAG, "updateVehiclePropertiesIfNeeded: gear:" + mLastGear); |
| } |
| } |
| } |
| |
| if (mLastParkingBrakeTimestamp == NOT_RECEIVED) { |
| CarPropertyValue propertyValue = mPropertyService.getPropertySafe( |
| VehicleProperty.PARKING_BRAKE_ON, |
| VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL); |
| if (propertyValue != null) { |
| mLastParkingBrakeState = (boolean) propertyValue.getValue(); |
| mLastParkingBrakeTimestamp = propertyValue.getTimestamp(); |
| if (DBG) { |
| Slog.d(TAG, "updateVehiclePropertiesIfNeeded: brake:" + mLastParkingBrakeState); |
| } |
| } |
| } |
| |
| if (mLastSpeedTimestamp == NOT_RECEIVED) { |
| CarPropertyValue propertyValue = mPropertyService.getPropertySafe( |
| VehicleProperty.PERF_VEHICLE_SPEED, |
| VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL); |
| if (propertyValue != null) { |
| mLastSpeed = (float) propertyValue.getValue(); |
| mLastSpeedTimestamp = propertyValue.getTimestamp(); |
| if (DBG) { |
| Slog.d(TAG, "updateVehiclePropertiesIfNeeded: speed:" + mLastSpeed); |
| } |
| } |
| } |
| } |
| |
| private static CarDrivingStateEvent createDrivingStateEvent(int eventValue) { |
| return new CarDrivingStateEvent(eventValue, SystemClock.elapsedRealtimeNanos()); |
| } |
| |
| } |