/* * 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 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