/* * Copyright (C) 2020 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 static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; import android.annotation.NonNull; import android.car.Car; import android.car.builtin.os.ServiceManagerHelper; import android.car.builtin.util.Slogf; import android.car.occupantawareness.IOccupantAwarenessEventCallback; import android.car.occupantawareness.OccupantAwarenessDetection; import android.car.occupantawareness.OccupantAwarenessDetection.VehicleOccupantRole; import android.car.occupantawareness.SystemStatusEvent; import android.car.occupantawareness.SystemStatusEvent.DetectionTypeFlags; import android.content.Context; import android.hardware.automotive.occupant_awareness.IOccupantAwareness; import android.hardware.automotive.occupant_awareness.IOccupantAwarenessClientCallback; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.util.Log; import android.util.proto.ProtoOutputStream; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.car.internal.util.IndentingPrintWriter; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.lang.ref.WeakReference; /** * A service that listens to an Occupant Awareness Detection system across a HAL boundary and * exposes the data to system clients in Android via a {@link * android.car.occupantawareness.OccupantAwarenessManager}. * *

The service exposes the following detections types: * *

Presence Detection

* * Detects whether a person is present for each seat location. * *

Gaze Detection

* * Detects where an occupant is looking and for how long they have been looking at the specified * target. * *

Driver Monitoring

* * Detects whether a driver is looking on or off-road and for how long they have been looking there. */ public class OccupantAwarenessService extends android.car.occupantawareness.IOccupantAwarenessManager.Stub implements CarServiceBase { private static final String TAG = CarLog.tagFor(OccupantAwarenessService.class); private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG); // HAL service identifier name. @VisibleForTesting static final String OAS_SERVICE_ID = "android.hardware.automotive.occupant_awareness.IOccupantAwareness/default"; private final Object mLock = new Object(); private final Context mContext; @GuardedBy("mLock") private IOccupantAwareness mOasHal; private final ChangeListenerToHalService mHalListener = new ChangeListenerToHalService(this); private static class ChangeCallbackList extends RemoteCallbackList { private final WeakReference mOasService; ChangeCallbackList(OccupantAwarenessService oasService) { mOasService = new WeakReference<>(oasService); } /** Handle callback death. */ @Override public void onCallbackDied(IOccupantAwarenessEventCallback listener) { Slogf.i(TAG, "binderDied: " + listener.asBinder()); OccupantAwarenessService service = mOasService.get(); if (service != null) { service.handleClientDisconnected(); } } } private final ChangeCallbackList mListeners = new ChangeCallbackList(this); /** Creates an OccupantAwarenessService instance given a {@link Context}. */ public OccupantAwarenessService(Context context) { mContext = context; } /** Creates an OccupantAwarenessService instance given a {@link Context}. */ @VisibleForTesting OccupantAwarenessService(Context context, IOccupantAwareness oasInterface) { mContext = context; mOasHal = oasInterface; } @Override public void init() { logd("Initializing service"); connectToHalServiceIfNotConnected(true); } @Override public void release() { logd("Will stop detection and disconnect listeners"); stopDetectionGraph(); mListeners.kill(); } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public void dump(IndentingPrintWriter writer) { writer.println("*OccupantAwarenessService*"); synchronized (mLock) { writer.println(String.format( "%s to HAL service", mOasHal == null ? "NOT connected" : "Connected")); } writer.println( String.format( "%d change listeners subscribed.", mListeners.getRegisteredCallbackCount())); } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public void dumpProto(ProtoOutputStream proto) {} /** Attempts to connect to the HAL service if it is not already connected. */ private void connectToHalServiceIfNotConnected(boolean forceConnect) { logd("connectToHalServiceIfNotConnected()"); synchronized (mLock) { // If already connected, nothing more needs to be done. if (mOasHal != null && !forceConnect) { logd("Client is already connected, nothing more to do"); return; } // Attempt to find the HAL service. if (mOasHal == null) { logd("Attempting to connect to client at: " + OAS_SERVICE_ID); mOasHal = android.hardware.automotive.occupant_awareness.IOccupantAwareness.Stub .asInterface(ServiceManagerHelper.getService(OAS_SERVICE_ID)); if (mOasHal == null) { Slogf.e(TAG, "Failed to find OAS hal_service at: [" + OAS_SERVICE_ID + "]"); return; } } // Register for callbacks. try { mOasHal.setCallback(mHalListener); } catch (RemoteException e) { mOasHal = null; Slogf.e(TAG, "Failed to set callback: " + e); return; } logd("Successfully connected to hal_service at: [" + OAS_SERVICE_ID + "]"); } } /** Sends a message via the HAL to start the detection graph. */ private void startDetectionGraph() { logd("Attempting to start detection graph"); // Grab a copy of 'mOasHal' to avoid sitting on the lock longer than is necessary. IOccupantAwareness hal; synchronized (mLock) { hal = mOasHal; } if (hal != null) { try { hal.startDetection(); } catch (RemoteException e) { Slogf.e(TAG, "startDetection() HAL invocation failed: " + e, e); synchronized (mLock) { mOasHal = null; } } } else { Slogf.e(TAG, "No HAL is connected. Cannot request graph start"); } } /** Sends a message via the HAL to stop the detection graph. */ private void stopDetectionGraph() { logd("Attempting to stop detection graph."); // Grab a copy of 'mOasHal' to avoid sitting on the lock longer than is necessary. IOccupantAwareness hal; synchronized (mLock) { hal = mOasHal; } if (hal != null) { try { hal.stopDetection(); } catch (RemoteException e) { Slogf.e(TAG, "stopDetection() HAL invocation failed: " + e, e); synchronized (mLock) { mOasHal = null; } } } else { Slogf.e(TAG, "No HAL is connected. Cannot request graph stop"); } } /** * Gets the vehicle capabilities for a given role. * *

Capabilities are static for a given vehicle configuration and need only be queried once * per vehicle. Once capability is determined, clients should query system status to see if the * subsystem is currently ready to serve. * *

Requires {@link android.car.Car.PERMISSION_READ_CAR_OCCUPANT_AWARENESS_STATE} read * permissions to access. * * @param role {@link VehicleOccupantRole} to query for. * @return Flags indicating supported capabilities for the role. */ public @DetectionTypeFlags int getCapabilityForRole(@VehicleOccupantRole int role) { CarServiceUtils.assertPermission(mContext, Car.PERMISSION_READ_CAR_OCCUPANT_AWARENESS_STATE); connectToHalServiceIfNotConnected(false); // Grab a copy of 'mOasHal' to avoid sitting on the lock longer than is necessary. IOccupantAwareness hal; synchronized (mLock) { hal = mOasHal; } if (hal != null) { try { return hal.getCapabilityForRole(role); } catch (RemoteException e) { Slogf.e(TAG, "getCapabilityForRole() HAL invocation failed: " + e, e); synchronized (mLock) { mOasHal = null; } return SystemStatusEvent.DETECTION_TYPE_NONE; } } else { Slogf.e(TAG, "getCapabilityForRole(): No HAL interface has been provided. Cannot get" + " capabilities"); return SystemStatusEvent.DETECTION_TYPE_NONE; } } /** * Registers a {@link IOccupantAwarenessEventCallback} to be notified for changes in the system * state. * *

Requires {@link android.car.Car.PERMISSION_READ_CAR_OCCUPANT_AWARENESS_STATE} read * permissions to access. * * @param listener {@link IOccupantAwarenessEventCallback} listener to register. */ @Override public void registerEventListener(@NonNull IOccupantAwarenessEventCallback listener) { CarServiceUtils.assertPermission(mContext, Car.PERMISSION_READ_CAR_OCCUPANT_AWARENESS_STATE); connectToHalServiceIfNotConnected(false); synchronized (mLock) { if (mOasHal == null) { Slogf.e(TAG, "Attempting to register a listener, but could not connect to HAL."); return; } logd("Registering a new listener"); mListeners.register(listener); // After the first client connects, request that the detection graph start. if (mListeners.getRegisteredCallbackCount() == 1) { startDetectionGraph(); } } } /** * Unregister the given {@link IOccupantAwarenessEventCallback} listener from receiving events. * *

Requires {@link android.car.Car.PERMISSION_READ_CAR_OCCUPANT_AWARENESS_STATE} read * permissions to access. * * @param listener {@link IOccupantAwarenessEventCallback} client to unregister. */ @Override public void unregisterEventListener(@NonNull IOccupantAwarenessEventCallback listener) { CarServiceUtils.assertPermission(mContext, Car.PERMISSION_READ_CAR_OCCUPANT_AWARENESS_STATE); connectToHalServiceIfNotConnected(false); synchronized (mLock) { mListeners.unregister(listener); } // When the last client disconnects, request that the detection graph stop. handleClientDisconnected(); } /** Processes a detection event and propagates it to registered clients. */ @VisibleForTesting void processStatusEvent(@NonNull SystemStatusEvent statusEvent) { int idx = mListeners.beginBroadcast(); while (idx-- > 0) { IOccupantAwarenessEventCallback listener = mListeners.getBroadcastItem(idx); try { listener.onStatusChanged(statusEvent); } catch (RemoteException e) { // It's likely the connection snapped. Let binder death handle the situation. Slogf.e(TAG, "onStatusChanged() invocation failed: " + e, e); } } mListeners.finishBroadcast(); } /** Processes a detection event and propagates it to registered clients. */ @VisibleForTesting void processDetectionEvent(@NonNull OccupantAwarenessDetection detection) { int idx = mListeners.beginBroadcast(); while (idx-- > 0) { IOccupantAwarenessEventCallback listener = mListeners.getBroadcastItem(idx); try { listener.onDetectionEvent(detection); } catch (RemoteException e) { // It's likely the connection snapped. Let binder death handle the situation. Slogf.e(TAG, "onDetectionEvent() invocation failed: " + e, e); } } mListeners.finishBroadcast(); } /** Handle client disconnections, possibly stopping the detection graph. */ void handleClientDisconnected() { // If the last client disconnects, requests that the graph stops. synchronized (mLock) { if (mListeners.getRegisteredCallbackCount() == 0) { stopDetectionGraph(); } } } private static void logd(String msg) { if (DBG) { Slogf.d(TAG, msg); } } /** * Class that implements the listener interface and gets called back from the {@link * android.hardware.automotive.occupant_awareness.IOccupantAwarenessClientCallback} across the * binder interface. */ private static class ChangeListenerToHalService extends IOccupantAwarenessClientCallback.Stub { private final WeakReference mOasService; ChangeListenerToHalService(OccupantAwarenessService oasService) { mOasService = new WeakReference<>(oasService); } @Override public void onSystemStatusChanged(int inputDetectionFlags, byte inputStatus) { OccupantAwarenessService service = mOasService.get(); if (service != null) { service.processStatusEvent( OccupantAwarenessUtils.convertToStatusEvent( inputDetectionFlags, inputStatus)); } } @Override public void onDetectionEvent( android.hardware.automotive.occupant_awareness.OccupantDetections detections) { OccupantAwarenessService service = mOasService.get(); if (service != null) { for (android.hardware.automotive.occupant_awareness.OccupantDetection detection : detections.detections) { service.processDetectionEvent( OccupantAwarenessUtils.convertToDetectionEvent( detections.timeStampMillis, detection)); } } } @Override public int getInterfaceVersion() { return this.VERSION; } @Override public String getInterfaceHash() { return this.HASH; } } }