/* * 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 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 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 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 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 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 getSupportedGears() { List 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()); } }