/* * 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 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 com.android.car.CarServiceBase; 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);
            }
        }
    }
    /**
     * 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