/* * Copyright (C) 2019 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.experimentalcar; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; import android.annotation.FloatRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.car.Car; import android.car.VehiclePropertyIds; import android.car.experimental.DriverAwarenessEvent; import android.car.experimental.DriverAwarenessSupplierConfig; import android.car.experimental.DriverAwarenessSupplierService; import android.car.experimental.DriverDistractionChangeEvent; import android.car.experimental.ExperimentalCar; import android.car.experimental.IDriverAwarenessSupplier; import android.car.experimental.IDriverAwarenessSupplierCallback; import android.car.experimental.IDriverDistractionChangeListener; import android.car.experimental.IDriverDistractionManager; import android.car.hardware.CarPropertyValue; import android.car.hardware.property.CarPropertyEvent; import android.car.hardware.property.CarPropertyManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; import android.util.Pair; import android.util.proto.ProtoOutputStream; import com.android.car.CarServiceBase; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.car.internal.util.IndentingPrintWriter; import com.android.car.util.TransitionLog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TimerTask; /** * Driver Distraction Service for using the driver's awareness, the required awareness of the * driving environment to expose APIs for the driver's current distraction level. * *

Allows the registration of multiple {@link IDriverAwarenessSupplier} so that higher accuracy * signals can be used when possible, with a fallback to less accurate signals. The {@link * TouchDriverAwarenessSupplier} is always set to the fallback implementation - it is configured * to send change-events, so its data will not become stale. */ public final class DriverDistractionExperimentalFeatureService extends IDriverDistractionManager.Stub implements CarServiceBase { private static final String TAG = "CAR.DriverDistractionService"; /** * The minimum delay between dispatched distraction events, in milliseconds. */ @VisibleForTesting static final long DISPATCH_THROTTLE_MS = 50L; private static final float DEFAULT_AWARENESS_VALUE_FOR_LOG = 1.0f; private static final float MOVING_REQUIRED_AWARENESS = 1.0f; private static final float STATIONARY_REQUIRED_AWARENESS = 0.0f; private static final int MAX_EVENT_LOG_COUNT = 50; private static final int PROPERTY_UPDATE_RATE_HZ = 5; @VisibleForTesting static final float DEFAULT_AWARENESS_PERCENTAGE = 1.0f; private final HandlerThread mClientDispatchHandlerThread; private final Handler mClientDispatchHandler; private final Object mLock = new Object(); @GuardedBy("mLock") private final ArrayDeque mTransitionLogs = new ArrayDeque<>(); /** * All the active service connections. */ @GuardedBy("mLock") private final List mServiceConnections = new ArrayList<>(); /** * The binder for each supplier. */ @GuardedBy("mLock") private final Map mSupplierBinders = new HashMap<>(); /** * The configuration for each supplier. */ @GuardedBy("mLock") private final Map mSupplierConfigs = new HashMap<>(); /** * List of driver awareness suppliers that can be used to understand the current driver * awareness level. Ordered from highest to lowest priority. */ @GuardedBy("mLock") private final List mPrioritizedDriverAwarenessSuppliers = new ArrayList<>(); /** * Helper map for looking up the priority rank of a supplier by name. A higher integer value * represents a higher priority. */ @GuardedBy("mLock") private final Map mDriverAwarenessSupplierPriorities = new HashMap<>(); /** * List of clients listening to UX restriction events. */ private final RemoteCallbackList mDistractionClients = new RemoteCallbackList<>(); /** * Comparator used to sort {@link #mDriverAwarenessSupplierPriorities}. */ private final Comparator mPrioritizedSuppliersComparator = (left, right) -> { int leftPri = mDriverAwarenessSupplierPriorities.get(left); int rightPri = mDriverAwarenessSupplierPriorities.get(right); // sort descending return rightPri - leftPri; }; /** * Keep track of the most recent awareness event for each supplier for use when the data from * higher priority suppliers becomes stale. This is necessary in order to seamlessly handle * fallback scenarios when data from preferred providers becomes stale. */ @GuardedBy("mLock") private final Map mCurrentAwarenessEventsMap = new HashMap<>(); /** * The awareness event that is currently being used to determine the driver awareness level. * *

This is null until it is set by the first awareness supplier to send an event */ @GuardedBy("mLock") @Nullable private DriverAwarenessEventWrapper mCurrentDriverAwareness; /** * Timer to alert when the current driver awareness event has become stale. */ @GuardedBy("mLock") private ITimer mExpiredDriverAwarenessTimer; /** * The current, non-stale, driver distraction event. Defaults to 100% awareness. */ @GuardedBy("mLock") private DriverDistractionChangeEvent mCurrentDistractionEvent; /** * The required driver awareness based on the current driving environment, where 1.0 means that * full awareness is required and 0.0 means than no awareness is required. */ @FloatRange(from = 0.0f, to = 1.0f) @GuardedBy("mLock") private float mRequiredAwareness = STATIONARY_REQUIRED_AWARENESS; @GuardedBy("mLock") private Car mCar; @GuardedBy("mLock") private CarPropertyManager mPropertyManager; /** * The time that last event was emitted, measured in milliseconds since boot using the {@link * android.os.SystemClock#uptimeMillis()} time-base. */ @GuardedBy("mLock") private long mLastDispatchUptimeMillis; /** * Whether there is currently a pending dispatch to clients. */ @GuardedBy("mLock") private boolean mIsDispatchQueued; private final Context mContext; private final ITimeSource mTimeSource; private final Looper mLooper; private final Runnable mDispatchCurrentDistractionRunnable = () -> { synchronized (mLock) { // dispatch whatever the current value is at this time in the future dispatchCurrentDistractionEventToClientsLocked( mCurrentDistractionEvent); mIsDispatchQueued = false; } }; /** * Create an instance of {@link DriverDistractionExperimentalFeatureService}. * * @param context the context */ DriverDistractionExperimentalFeatureService(Context context) { this(context, new SystemTimeSource(), new SystemTimer(), Looper.myLooper(), null); } @VisibleForTesting DriverDistractionExperimentalFeatureService( Context context, ITimeSource timeSource, ITimer timer, Looper looper, Handler clientDispatchHandler) { mContext = context; mTimeSource = timeSource; mExpiredDriverAwarenessTimer = timer; mCurrentDistractionEvent = new DriverDistractionChangeEvent.Builder() .setElapsedRealtimeTimestamp(mTimeSource.elapsedRealtime()) .setAwarenessPercentage(DEFAULT_AWARENESS_PERCENTAGE) .build(); mClientDispatchHandlerThread = new HandlerThread(TAG); mClientDispatchHandlerThread.start(); if (clientDispatchHandler == null) { mClientDispatchHandler = new Handler(mClientDispatchHandlerThread.getLooper()); } else { mClientDispatchHandler = clientDispatchHandler; } mLooper = looper; } @Override public void init() { // The touch supplier is an internal implementation, so it can be started initiated by its // constructor, unlike other suppliers ComponentName touchComponent = new ComponentName(mContext, TouchDriverAwarenessSupplier.class); TouchDriverAwarenessSupplier touchSupplier = new TouchDriverAwarenessSupplier(mContext, new DriverAwarenessSupplierCallback(touchComponent), mLooper); addDriverAwarenessSupplier(touchComponent, touchSupplier, /* priority= */ 0); touchSupplier.onReady(); String[] preferredDriverAwarenessSuppliers = mContext.getResources().getStringArray( R.array.preferredDriverAwarenessSuppliers); for (int i = 0; i < preferredDriverAwarenessSuppliers.length; i++) { String supplierStringName = preferredDriverAwarenessSuppliers[i]; ComponentName externalComponent = ComponentName.unflattenFromString(supplierStringName); // the touch supplier has priority 0 and preferred suppliers are higher based on order int priority = i + 1; bindDriverAwarenessSupplierService(externalComponent, priority); } synchronized (mLock) { mCar = Car.createCar(mContext); if (mCar != null) { mPropertyManager = (CarPropertyManager) mCar.getCarManager(Car.PROPERTY_SERVICE); } else { Log.e(TAG, "Unable to connect to car in init"); } } if (mPropertyManager != null) { mPropertyManager.registerCallback(mSpeedPropertyEventCallback, VehiclePropertyIds.PERF_VEHICLE_SPEED, PROPERTY_UPDATE_RATE_HZ); } else { Log.e(TAG, "Unable to get car property service."); } } @Override public void release() { logd("release"); mDistractionClients.kill(); synchronized (mLock) { mExpiredDriverAwarenessTimer.cancel(); mClientDispatchHandler.removeCallbacksAndMessages(null); for (ServiceConnection serviceConnection : mServiceConnections) { mContext.unbindService(serviceConnection); } if (mPropertyManager != null) { mPropertyManager.unregisterCallback(mSpeedPropertyEventCallback); } if (mCar != null) { mCar.disconnect(); } } } @Override public void dump(IndentingPrintWriter writer) { writer.println("*DriverDistractionExperimentalFeatureService*"); mDistractionClients.dump(writer, "Distraction Clients "); writer.println("Prioritized Driver Awareness Suppliers (highest to lowest priority):"); synchronized (mLock) { for (int i = 0; i < mPrioritizedDriverAwarenessSuppliers.size(); i++) { writer.println( String.format(" %d: %s", i, mPrioritizedDriverAwarenessSuppliers.get( i).getClass().getName())); } writer.println("Current Driver Awareness:"); writer.println(" Value: " + (mCurrentDriverAwareness == null ? "unknown" : mCurrentDriverAwareness.mAwarenessEvent.getAwarenessValue())); writer.println(" Supplier: " + (mCurrentDriverAwareness == null ? "unknown" : mCurrentDriverAwareness.mSupplier.getClass().getSimpleName())); writer.println(" Timestamp (ms since boot): " + (mCurrentDriverAwareness == null ? "unknown" : mCurrentDriverAwareness.mAwarenessEvent.getTimeStamp())); writer.println("Current Required Awareness: " + mRequiredAwareness); writer.println("Last Distraction Event:"); writer.println(" Value: " + (mCurrentDistractionEvent == null ? "unknown" : mCurrentDistractionEvent.getAwarenessPercentage())); writer.println(" Timestamp (ms since boot): " + (mCurrentDistractionEvent == null ? "unknown" : mCurrentDistractionEvent.getElapsedRealtimeTimestamp())); writer.println("Dispatch Status:"); writer.println(" mLastDispatchUptimeMillis: " + mLastDispatchUptimeMillis); writer.println(" mIsDispatchQueued: " + mIsDispatchQueued); writer.println("Change log:"); for (TransitionLog log : mTransitionLogs) { writer.println(log); } } } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public void dumpProto(ProtoOutputStream proto) {} /** * Bind to a {@link DriverAwarenessSupplierService} by its component name. * * @param componentName the name of the {@link DriverAwarenessSupplierService} to bind to. * @param priority the priority rank of this supplier */ private void bindDriverAwarenessSupplierService(ComponentName componentName, int priority) { Intent intent = new Intent(); intent.setComponent(componentName); ServiceConnection connection = new DriverAwarenessServiceConnection(priority); synchronized (mLock) { mServiceConnections.add(connection); } if (!mContext.bindServiceAsUser(intent, connection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT, UserHandle.SYSTEM)) { Log.e(TAG, "Unable to bind with intent: " + intent); // TODO(b/146471650) attempt to rebind } } @VisibleForTesting void handleDriverAwarenessEvent(DriverAwarenessEventWrapper awarenessEventWrapper) { synchronized (mLock) { handleDriverAwarenessEventLocked(awarenessEventWrapper); } } /** * Handle the driver awareness event by: *

* * @param awarenessEventWrapper the driver awareness event that has occurred */ @GuardedBy("mLock") private void handleDriverAwarenessEventLocked( DriverAwarenessEventWrapper awarenessEventWrapper) { // update the current awareness event for the supplier, checking that it is the newest event IDriverAwarenessSupplier supplier = awarenessEventWrapper.mSupplier; long timestamp = awarenessEventWrapper.mAwarenessEvent.getTimeStamp(); if (!mCurrentAwarenessEventsMap.containsKey(supplier) || mCurrentAwarenessEventsMap.get(supplier).mAwarenessEvent.getTimeStamp() < timestamp) { mCurrentAwarenessEventsMap.put(awarenessEventWrapper.mSupplier, awarenessEventWrapper); } int oldSupplierPriority = mDriverAwarenessSupplierPriorities.get(supplier); float oldAwarenessValue = DEFAULT_AWARENESS_VALUE_FOR_LOG; if (mCurrentDriverAwareness != null) { oldAwarenessValue = mCurrentDriverAwareness.mAwarenessEvent.getAwarenessValue(); } updateCurrentAwarenessValueLocked(); int newSupplierPriority = mDriverAwarenessSupplierPriorities.get( mCurrentDriverAwareness.mSupplier); if (mSupplierConfigs.get(mCurrentDriverAwareness.mSupplier).getMaxStalenessMillis() != DriverAwarenessSupplierService.NO_STALENESS && newSupplierPriority >= oldSupplierPriority) { // only reschedule an expiration if this is for a supplier that is the same or higher // priority than the old value. If there is a higher priority supplier with non-stale // data, then mCurrentDriverAwareness won't change even though we received a new event. scheduleExpirationTimerLocked(); } if (oldAwarenessValue != mCurrentDriverAwareness.mAwarenessEvent.getAwarenessValue()) { logd("Driver awareness updated: " + mCurrentDriverAwareness.mAwarenessEvent.getAwarenessValue()); addTransitionLogLocked(oldAwarenessValue, awarenessEventWrapper.mAwarenessEvent.getAwarenessValue(), "Driver awareness updated by " + awarenessEventWrapper.mSupplier.getClass().getSimpleName()); } updateCurrentDistractionEventLocked(); } /** * Get the current awareness value. */ @VisibleForTesting DriverAwarenessEventWrapper getCurrentDriverAwareness() { return mCurrentDriverAwareness; } /** * Set the drier awareness suppliers. Allows circumventing the {@link #init()} logic. */ @VisibleForTesting void setDriverAwarenessSuppliers( List> suppliers) { mPrioritizedDriverAwarenessSuppliers.clear(); mDriverAwarenessSupplierPriorities.clear(); for (int i = 0; i < suppliers.size(); i++) { Pair pair = suppliers.get(i); mSupplierConfigs.put(pair.first, pair.second); mDriverAwarenessSupplierPriorities.put(pair.first, i); mPrioritizedDriverAwarenessSuppliers.add(pair.first); } mPrioritizedDriverAwarenessSuppliers.sort(mPrioritizedSuppliersComparator); } /** * {@link CarPropertyEvent} listener registered with the {@link CarPropertyManager} for getting * speed change notifications. */ private final CarPropertyManager.CarPropertyEventCallback mSpeedPropertyEventCallback = new CarPropertyManager.CarPropertyEventCallback() { @Override public void onChangeEvent(CarPropertyValue value) { synchronized (mLock) { handleSpeedEventLocked(value); } } @Override public void onErrorEvent(int propId, int zone) { Log.e(TAG, "Error in callback for vehicle speed"); } }; @VisibleForTesting @GuardedBy("mLock") void handleSpeedEventLocked(@NonNull CarPropertyValue value) { if (value.getPropertyId() != VehiclePropertyIds.PERF_VEHICLE_SPEED) { Log.e(TAG, "Unexpected property id: " + value.getPropertyId()); return; } float oldValue = mRequiredAwareness; if ((Float) value.getValue() > 0) { mRequiredAwareness = MOVING_REQUIRED_AWARENESS; } else { mRequiredAwareness = STATIONARY_REQUIRED_AWARENESS; } if (Float.compare(oldValue, mRequiredAwareness) != 0) { logd("Required awareness updated: " + mRequiredAwareness); addTransitionLogLocked(oldValue, mRequiredAwareness, "Required awareness"); updateCurrentDistractionEventLocked(); } } @GuardedBy("mLock") private void updateCurrentDistractionEventLocked() { if (mCurrentDriverAwareness == null) { logd("Driver awareness level is not yet known"); return; } float awarenessPercentage; if (mRequiredAwareness == 0) { // avoid divide by 0 error - awareness percentage should be 100% when required // awareness is 0 awarenessPercentage = 1.0f; } else { // Cap awareness percentage at 100% awarenessPercentage = Math.min( mCurrentDriverAwareness.mAwarenessEvent.getAwarenessValue() / mRequiredAwareness, 1.0f); } if (Float.compare(mCurrentDistractionEvent.getAwarenessPercentage(), awarenessPercentage) == 0) { // no need to dispatch unless there's a change return; } addTransitionLogLocked(mCurrentDistractionEvent.getAwarenessPercentage(), awarenessPercentage, "Awareness percentage"); mCurrentDistractionEvent = new DriverDistractionChangeEvent.Builder() .setElapsedRealtimeTimestamp(mTimeSource.elapsedRealtime()) .setAwarenessPercentage(awarenessPercentage) .build(); long nowUptimeMillis = mTimeSource.uptimeMillis(); if (shouldThrottleDispatchEventLocked(nowUptimeMillis)) { scheduleAwarenessDispatchLocked(nowUptimeMillis); } else { // if event doesn't need to be throttled, emit immediately DriverDistractionChangeEvent changeEvent = mCurrentDistractionEvent; mClientDispatchHandler.post( () -> dispatchCurrentDistractionEventToClientsLocked(changeEvent)); } } @GuardedBy("mLock") private void scheduleAwarenessDispatchLocked(long uptimeMillis) { if (mIsDispatchQueued) { logd("Dispatch event is throttled and already scheduled."); return; } // schedule a dispatch for when throttle window has passed long delayMs = mLastDispatchUptimeMillis + DISPATCH_THROTTLE_MS - uptimeMillis; if (delayMs < 0) { Log.e(TAG, String.format( "Delay for (%s) calculated to be negative (%s), so dispatching immediately", mCurrentDistractionEvent, delayMs)); delayMs = 0; } logd(String.format("Dispatch event (%s) is throttled. Scheduled to emit in %sms", mCurrentDistractionEvent, delayMs)); mIsDispatchQueued = true; mClientDispatchHandler.postDelayed(mDispatchCurrentDistractionRunnable, delayMs); } @GuardedBy("mLock") private boolean shouldThrottleDispatchEventLocked(long uptimeMillis) { return uptimeMillis < mLastDispatchUptimeMillis + DISPATCH_THROTTLE_MS; } @GuardedBy("mLock") private void dispatchCurrentDistractionEventToClientsLocked( DriverDistractionChangeEvent changeEvent) { mLastDispatchUptimeMillis = mTimeSource.uptimeMillis(); logd("Dispatching event to clients: " + changeEvent); int numClients = mDistractionClients.beginBroadcast(); for (int i = 0; i < numClients; i++) { IDriverDistractionChangeListener callback = mDistractionClients.getBroadcastItem(i); try { callback.onDriverDistractionChange(changeEvent); } catch (RemoteException ignores) { // ignore } } mDistractionClients.finishBroadcast(); } /** * Internally register the supplier with the specified priority. */ private void addDriverAwarenessSupplier( ComponentName componentName, IDriverAwarenessSupplier awarenessSupplier, int priority) { synchronized (mLock) { mSupplierBinders.put(componentName, awarenessSupplier); mDriverAwarenessSupplierPriorities.put(awarenessSupplier, priority); mPrioritizedDriverAwarenessSuppliers.add(awarenessSupplier); mPrioritizedDriverAwarenessSuppliers.sort(mPrioritizedSuppliersComparator); } } /** * Remove references to a supplier. */ private void removeDriverAwarenessSupplier(ComponentName componentName) { synchronized (mLock) { IDriverAwarenessSupplier supplier = mSupplierBinders.get(componentName); mSupplierBinders.remove(componentName); mDriverAwarenessSupplierPriorities.remove(supplier); mPrioritizedDriverAwarenessSuppliers.remove(supplier); } } /** * Update {@link #mCurrentDriverAwareness} based on the current driver awareness events for each * supplier. */ @GuardedBy("mLock") private void updateCurrentAwarenessValueLocked() { for (IDriverAwarenessSupplier supplier : mPrioritizedDriverAwarenessSuppliers) { long supplierMaxStaleness = mSupplierConfigs.get(supplier).getMaxStalenessMillis(); DriverAwarenessEventWrapper eventForSupplier = mCurrentAwarenessEventsMap.get(supplier); if (eventForSupplier == null) { continue; } if (supplierMaxStaleness == DriverAwarenessSupplierService.NO_STALENESS) { // this supplier can't be stale, so use its information mCurrentDriverAwareness = eventForSupplier; return; } long oldestFreshTimestamp = mTimeSource.elapsedRealtime() - supplierMaxStaleness; if (eventForSupplier.mAwarenessEvent.getTimeStamp() > oldestFreshTimestamp) { // value is still fresh, so use it mCurrentDriverAwareness = eventForSupplier; return; } } if (mCurrentDriverAwareness == null) { // There must always at least be a fallback supplier with NO_STALENESS configuration. // Since we control this configuration, getting this exception represents a developer // error in initialization. throw new IllegalStateException( "Unable to determine the current driver awareness value"); } } /** * Sets a timer to update the refresh the awareness value once the current value has become * stale. */ @GuardedBy("mLock") private void scheduleExpirationTimerLocked() { // reschedule the current awareness expiration task mExpiredDriverAwarenessTimer.reset(); long delay = mCurrentDriverAwareness.mAwarenessEvent.getTimeStamp() - mTimeSource.elapsedRealtime() + mCurrentDriverAwareness.mMaxStaleness; if (delay < 0) { // somehow the event is already stale synchronized (mLock) { updateCurrentAwarenessValueLocked(); } return; } mExpiredDriverAwarenessTimer.schedule(new TimerTask() { @Override public void run() { logd("Driver awareness has become stale. Selecting new awareness level."); synchronized (mLock) { updateCurrentAwarenessValueLocked(); updateCurrentDistractionEventLocked(); } } }, delay); logd(String.format( "Current awareness value is stale after %sms and is scheduled to expire in %sms", mCurrentDriverAwareness.mMaxStaleness, delay)); } /** * Add the state change to the transition log. * * @param oldValue the old value * @param newValue the new value * @param extra name of the value being changed */ @GuardedBy("mLock") private void addTransitionLogLocked(float oldValue, float newValue, String extra) { if (mTransitionLogs.size() >= MAX_EVENT_LOG_COUNT) { mTransitionLogs.remove(); } TransitionLog tLog = new TransitionLog(TAG, oldValue, newValue, System.currentTimeMillis(), extra); mTransitionLogs.add(tLog); } private static void logd(String message) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, message); } } @Override public DriverDistractionChangeEvent getLastDistractionEvent() throws RemoteException { IExperimentalCarImpl.assertPermission(mContext, ExperimentalCar.PERMISSION_READ_CAR_DRIVER_DISTRACTION); synchronized (mLock) { return mCurrentDistractionEvent; } } @Override public void addDriverDistractionChangeListener(IDriverDistractionChangeListener listener) throws RemoteException { IExperimentalCarImpl.assertPermission(mContext, ExperimentalCar.PERMISSION_READ_CAR_DRIVER_DISTRACTION); if (listener == null) { throw new IllegalArgumentException("IDriverDistractionChangeListener is null"); } mDistractionClients.register(listener); DriverDistractionChangeEvent changeEvent = mCurrentDistractionEvent; mClientDispatchHandler.post(() -> { try { listener.onDriverDistractionChange(changeEvent); } catch (RemoteException ignores) { // ignore } }); } @Override public void removeDriverDistractionChangeListener(IDriverDistractionChangeListener listener) throws RemoteException { IExperimentalCarImpl.assertPermission(mContext, ExperimentalCar.PERMISSION_READ_CAR_DRIVER_DISTRACTION); if (listener == null) { Log.e(TAG, "unregisterUxRestrictionsChangeListener(): listener null"); throw new IllegalArgumentException("Listener is null"); } mDistractionClients.unregister(listener); } /** * The service connection between this distraction service and a {@link * DriverAwarenessSupplierService}, communicated through {@link IDriverAwarenessSupplier}. */ private class DriverAwarenessServiceConnection implements ServiceConnection { final int mPriority; /** * Create an instance of {@link DriverAwarenessServiceConnection}. * * @param priority the priority of the {@link DriverAwarenessSupplierService} that this * connection is for */ DriverAwarenessServiceConnection(int priority) { mPriority = priority; } @Override public void onServiceConnected(ComponentName name, IBinder binder) { logd("onServiceConnected, name: " + name + ", binder: " + binder); IDriverAwarenessSupplier service = IDriverAwarenessSupplier.Stub.asInterface( binder); addDriverAwarenessSupplier(name, service, mPriority); try { service.setCallback(new DriverAwarenessSupplierCallback(name)); service.onReady(); } catch (RemoteException e) { Log.e(TAG, "Unable to call onReady on supplier", e); } } @Override public void onServiceDisconnected(ComponentName name) { logd("onServiceDisconnected, name: " + name); removeDriverAwarenessSupplier(name); // TODO(b/146471650) rebind to driver awareness suppliers on service disconnect } } /** * Driver awareness listener that keeps a references to some attributes of the supplier. */ private class DriverAwarenessSupplierCallback extends IDriverAwarenessSupplierCallback.Stub { private final ComponentName mComponentName; /** * Construct an instance of {@link DriverAwarenessSupplierCallback}. * * @param componentName the driver awareness supplier for this listener */ DriverAwarenessSupplierCallback(ComponentName componentName) { mComponentName = componentName; } @Override public void onDriverAwarenessUpdated(DriverAwarenessEvent event) { IDriverAwarenessSupplier supplier; long maxStaleness; synchronized (mLock) { supplier = mSupplierBinders.get(mComponentName); maxStaleness = mSupplierConfigs.get(supplier).getMaxStalenessMillis(); } if (supplier == null) { // this should never happen. Initialization process would not be correct. throw new IllegalStateException( "No supplier registered for component " + mComponentName); } logd(String.format("Driver awareness updated for %s: %s", supplier.getClass().getSimpleName(), event)); handleDriverAwarenessEvent( new DriverAwarenessEventWrapper(event, supplier, maxStaleness)); } @Override public void onConfigLoaded(DriverAwarenessSupplierConfig config) throws RemoteException { synchronized (mLock) { mSupplierConfigs.put(mSupplierBinders.get(mComponentName), config); } } } /** * Wrapper for {@link DriverAwarenessEvent} that includes some information from the supplier * that emitted the event. */ @VisibleForTesting static class DriverAwarenessEventWrapper { final DriverAwarenessEvent mAwarenessEvent; final IDriverAwarenessSupplier mSupplier; final long mMaxStaleness; /** * Construct an instance of {@link DriverAwarenessEventWrapper}. * * @param awarenessEvent the driver awareness event being wrapped * @param supplier the driver awareness supplier for this listener * @param maxStaleness the max staleness of the supplier that emitted this event (included * to avoid making a binder call) */ DriverAwarenessEventWrapper( DriverAwarenessEvent awarenessEvent, IDriverAwarenessSupplier supplier, long maxStaleness) { mAwarenessEvent = awarenessEvent; mSupplier = supplier; mMaxStaleness = maxStaleness; } @Override public String toString() { return String.format( "DriverAwarenessEventWrapper{mAwarenessChangeEvent=%s, mSupplier=%s, " + "mMaxStaleness=%s}", mAwarenessEvent, mSupplier, mMaxStaleness); } } }