diff options
author | Jordan Jozwiak <jjoz@google.com> | 2019-12-23 21:43:48 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2019-12-23 21:43:48 +0000 |
commit | d0f0fb15e5522ea8679192cab2873e548892aea9 (patch) | |
tree | 612678b3af4b0d9808ce07d8c8c740dc868e944f /experimental | |
parent | 56f0e121c84ee9305cc70f5a2355b8e4223bf447 (diff) | |
parent | 9789ac56447795283ca5bbdcb828a63a4070b71c (diff) | |
download | Car-d0f0fb15e5522ea8679192cab2873e548892aea9.tar.gz |
Merge "Consume driver awareness values from suppliers"
Diffstat (limited to 'experimental')
22 files changed, 1672 insertions, 12 deletions
diff --git a/experimental/experimental_api/src/android/car/experimental/DriverAwarenessEvent.aidl b/experimental/experimental_api/src/android/car/experimental/DriverAwarenessEvent.aidl new file mode 100644 index 0000000000..e9f38b5cec --- /dev/null +++ b/experimental/experimental_api/src/android/car/experimental/DriverAwarenessEvent.aidl @@ -0,0 +1,19 @@ +/* + * 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 android.car.experimental; + +parcelable DriverAwarenessEvent;
\ No newline at end of file diff --git a/experimental/experimental_api/src/android/car/experimental/DriverAwarenessEvent.java b/experimental/experimental_api/src/android/car/experimental/DriverAwarenessEvent.java new file mode 100644 index 0000000000..d961a4311c --- /dev/null +++ b/experimental/experimental_api/src/android/car/experimental/DriverAwarenessEvent.java @@ -0,0 +1,113 @@ +/* + * 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 android.car.experimental; + +import android.annotation.FloatRange; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Event about a driver's awareness level at a certain point in time. + * + * <p>Driver Awareness is an abstract concept based on a driver's cognitive situational awareness + * of the environment around them. This metric can be approximated based on signals about the + * driver's behavior, such as where they are looking or how much their interact with the headunit + * in the car. + * + * <p>Furthermore, what constitutes the boundaries of no awareness and full awareness must be based + * on the UX Research through real-world studies and driving simulation. It is the responsibility + * of {@link DriverAwarenessSupplier}s to understand how their sensor input fits with current + * research in order to determine the appropriate awareness value. + * + * @hide + */ +public final class DriverAwarenessEvent implements Parcelable { + + private final long mTimeStamp; + + @FloatRange(from = 0.0f, to = 1.0f) + private final float mAwarenessValue; + + /** + * Creator for {@link Parcelable}. + */ + public static final Parcelable.Creator<DriverAwarenessEvent> CREATOR = + new Parcelable.Creator<DriverAwarenessEvent>() { + public DriverAwarenessEvent createFromParcel(Parcel in) { + return new DriverAwarenessEvent(in); + } + + public DriverAwarenessEvent[] newArray(int size) { + return new DriverAwarenessEvent[size]; + } + }; + + /** + * Creates an instance of a {@link DriverAwarenessEvent}. + * + * @param timeStamp the time that the awareness value was sampled, in milliseconds elapsed + * since system boot + * @param awarenessValue the driver's awareness level at this point in time, ranging from 0, no + * awareness, to 1, full awareness + */ + public DriverAwarenessEvent(long timeStamp, + @FloatRange(from = 0.0f, to = 1.0f) float awarenessValue) { + mTimeStamp = timeStamp; + mAwarenessValue = awarenessValue; + } + + /** + * Parcelable constructor. + */ + private DriverAwarenessEvent(Parcel in) { + mTimeStamp = in.readLong(); + mAwarenessValue = in.readFloat(); + } + + /** + * Returns the time at which this driver awareness value was inferred based on the car's + * sensors. It is the elapsed time in milliseconds since system boot. + */ + public long getTimeStamp() { + return mTimeStamp; + } + + /** + * The current driver awareness value, where 0 is no awareness and 1 is full awareness. + */ + @FloatRange(from = 0.0f, to = 1.0f) + public float getAwarenessValue() { + return mAwarenessValue; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(mTimeStamp); + dest.writeFloat(mAwarenessValue); + } + + + @Override + public String toString() { + return String.format("DriverAwarenessEvent{timeStamp=%s, awarenessValue=%s}", + mTimeStamp, mAwarenessValue); + } +} diff --git a/experimental/experimental_api/src/android/car/experimental/DriverAwarenessSupplierConfig.aidl b/experimental/experimental_api/src/android/car/experimental/DriverAwarenessSupplierConfig.aidl new file mode 100644 index 0000000000..f7b13981d5 --- /dev/null +++ b/experimental/experimental_api/src/android/car/experimental/DriverAwarenessSupplierConfig.aidl @@ -0,0 +1,19 @@ +/* + * 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 android.car.experimental; + +parcelable DriverAwarenessSupplierConfig;
\ No newline at end of file diff --git a/experimental/experimental_api/src/android/car/experimental/DriverAwarenessSupplierConfig.java b/experimental/experimental_api/src/android/car/experimental/DriverAwarenessSupplierConfig.java new file mode 100644 index 0000000000..1fd867aeb6 --- /dev/null +++ b/experimental/experimental_api/src/android/car/experimental/DriverAwarenessSupplierConfig.java @@ -0,0 +1,82 @@ +/* + * 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 android.car.experimental; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Configuration for an instance of {@link android.car.experimental.IDriverAwarenessSupplier}. + */ +public class DriverAwarenessSupplierConfig implements Parcelable { + + private final long mMaxStalenessMillis; + + /** + * Creator for {@link Parcelable}. + */ + public static final Parcelable.Creator<DriverAwarenessSupplierConfig> CREATOR = + new Parcelable.Creator<DriverAwarenessSupplierConfig>() { + public DriverAwarenessSupplierConfig createFromParcel(Parcel in) { + return new DriverAwarenessSupplierConfig(in); + } + + public DriverAwarenessSupplierConfig[] newArray(int size) { + return new DriverAwarenessSupplierConfig[size]; + } + }; + + /** + * Creates an instance of a {@link DriverAwarenessSupplierConfig}. + */ + public DriverAwarenessSupplierConfig(long maxStalenessMillis) { + mMaxStalenessMillis = maxStalenessMillis; + } + + /** + * Parcelable constructor. + */ + private DriverAwarenessSupplierConfig(Parcel in) { + mMaxStalenessMillis = in.readLong(); + } + + /** + * Returns the duration in milliseconds after which the input from this supplier should be + * considered stale. + * + * <p>Data should be sent more frequently than the staleness rate defined here. + */ + public long getMaxStalenessMillis() { + return mMaxStalenessMillis; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(mMaxStalenessMillis); + } + + + @Override + public String toString() { + return String.format("DriverAwarenessEvent{mMaxStalenessMillis=%s}", mMaxStalenessMillis); + } +} diff --git a/experimental/experimental_api/src/android/car/experimental/DriverAwarenessSupplierService.java b/experimental/experimental_api/src/android/car/experimental/DriverAwarenessSupplierService.java new file mode 100644 index 0000000000..df48d101ca --- /dev/null +++ b/experimental/experimental_api/src/android/car/experimental/DriverAwarenessSupplierService.java @@ -0,0 +1,153 @@ +/* + * 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 android.car.experimental; + + +import android.annotation.CallSuper; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +/** + * The supplier for providing a stream of the driver's current situational awareness. + * + * <p>A perfect understanding of driver awareness requires years of extensive research and signals + * that suggest the cognitive situational awareness of the driver. Implementations of this class + * attempt to approximate driver awareness using concrete, but less accurate signals, such as gaze + * or touch. + * + * <p>Suppliers should notify of updates to the driver awareness level through {@link + * #onDriverAwarenessUpdated(DriverAwarenessEvent)}. + * + * <p>Suppliers define the amount of time until their data should be considered stale through + * {@link #getMaxStalenessMillis()}. After that amount of time data from this supplier will no + * longer be considered fresh. {@link #NO_STALENESS} is meant to be used by change-based suppliers + * such as a touch supplier - it is not appropriate for data signals that change continuous over + * time. + * + * <p>If this supplier has its own internal configuration, that configuration must be configurable + * by locale. + * + * <p>It is the attention supplier's responsibility to make sure that it only sends high-quality + * data events. + */ +public abstract class DriverAwarenessSupplierService extends Service { + private static final String TAG = "DriverAwarenessSupplierService"; + + /** + * Value that can be returned by {@link #getMaxStalenessMillis()} to indicate that an attention + * supplier sends change-events instead of push events on a regular interval. Should only be + * used for a supplier that is guaranteed to always be running (e.g. it won't crash or have + * periods of poor data). + */ + public static final long NO_STALENESS = -1; + + private final Object mLock = new Object(); + + private SupplierBinder mSupplierBinder; + + @GuardedBy("mLock") + private IDriverAwarenessSupplierCallback mDriverAwarenessSupplierCallback; + + /** + * Returns the duration in milliseconds after which the input from this supplier should be + * considered stale. This method should return a positive value greater than 0. There is no + * technical limit on the value returned here, but a value of 1000ms (1 second) would likely be + * considered too high since the driving environment can change drastically in that amount of + * time. + * + * <p>This can also return {@link #NO_STALENESS} if the supplier only emits change events and + * has no risk of failing to emit those change events within a reasonable amount of time. + * + * <p>Data should be sent more frequently than the staleness period defined here. + */ + public abstract long getMaxStalenessMillis(); + + /** + * The distraction service is ready to start receiving events via {@link + * #onDriverAwarenessUpdated(DriverAwarenessEvent)}. + */ + protected abstract void onReady(); + + @Override + @CallSuper + public IBinder onBind(Intent intent) { + logd("onBind, intent: " + intent); + if (mSupplierBinder == null) { + mSupplierBinder = new SupplierBinder(); + } + return mSupplierBinder; + } + + /** + * The driver awareness has changed - emit that update to the {@link + * com.android.experimentalcar.DriverDistractionExperimentalFeatureService}. + */ + protected void onDriverAwarenessUpdated(DriverAwarenessEvent event) { + logd("onDriverAwarenessUpdated: " + event); + synchronized (mLock) { + if (mDriverAwarenessSupplierCallback != null) { + try { + mDriverAwarenessSupplierCallback.onDriverAwarenessUpdated(event); + } catch (RemoteException e) { + Log.e(TAG, "Remote exception", e); + } + } + } + } + + private void handleReady() { + synchronized (mLock) { + try { + mDriverAwarenessSupplierCallback.onConfigLoaded( + new DriverAwarenessSupplierConfig(getMaxStalenessMillis())); + } catch (RemoteException e) { + Log.e(TAG, "Unable to send config - abandoning ready process", e); + return; + } + } + onReady(); + } + + /** + * The binder between this service and + * {@link com.android.experimentalcar.DriverDistractionExperimentalFeatureService}. + */ + private class SupplierBinder extends IDriverAwarenessSupplier.Stub { + + @Override + public void onReady() { + handleReady(); + } + + @Override + public void setCallback(IDriverAwarenessSupplierCallback callback) { + synchronized (mLock) { + mDriverAwarenessSupplierCallback = callback; + } + } + } + + private static void logd(String message) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, message); + } + } +} diff --git a/experimental/experimental_api/src/android/car/experimental/IDriverAwarenessSupplier.aidl b/experimental/experimental_api/src/android/car/experimental/IDriverAwarenessSupplier.aidl new file mode 100644 index 0000000000..6eaadebba1 --- /dev/null +++ b/experimental/experimental_api/src/android/car/experimental/IDriverAwarenessSupplier.aidl @@ -0,0 +1,33 @@ +/* + * 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 android.car.experimental; + +import android.car.experimental.IDriverAwarenessSupplierCallback; + +/** + * Binder API for a driver awareness supplier. + * + * @hide + */ +oneway interface IDriverAwarenessSupplier { + + /** Called once the distraction service is ready to receive events */ + void onReady() = 0; + + /** Set the callback to be used for this supplier */ + void setCallback(IDriverAwarenessSupplierCallback callback) = 1; +}
\ No newline at end of file diff --git a/experimental/experimental_api/src/android/car/experimental/IDriverAwarenessSupplierCallback.aidl b/experimental/experimental_api/src/android/car/experimental/IDriverAwarenessSupplierCallback.aidl new file mode 100644 index 0000000000..7b6ec1fa8e --- /dev/null +++ b/experimental/experimental_api/src/android/car/experimental/IDriverAwarenessSupplierCallback.aidl @@ -0,0 +1,50 @@ +/* + * 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 android.car.experimental; + +import android.car.experimental.DriverAwarenessEvent; +import android.car.experimental.DriverAwarenessSupplierConfig; + +/** + * Binder callback for IDriverAwarenessSupplier. + * + * @hide + */ +interface IDriverAwarenessSupplierCallback { + /** + * Sets the awareness level for the driver. Determining sufficient data quality is the + * responsibility of the AwarenessSupplier and events with insufficient data quality should not + * be sent. + * + * <p>Suppliers could crash in the background or fail to send continuously high data and + * therefore should push events, even if the Awareness level hasn't changed, with a frequency + * greater than their specified AwarenessSupplier#getMaxStalenessMillis(). + * + * <p>Should be called once when first registered. + * + * @param event a snapshot of the driver's awareness at a certain point in time. + */ + void onDriverAwarenessUpdated(in DriverAwarenessEvent event) = 0; + + /** + * Sends the configuration for IDriverAwarenessSupplier configuration that this is a callback + * for. + * + * @param config for the IDriverAwarenessSupplier + */ + void onConfigLoaded(in DriverAwarenessSupplierConfig config) = 1; +} diff --git a/experimental/experimentalcarservice_unit_test/Android.bp b/experimental/experimentalcarservice_unit_test/Android.bp new file mode 100644 index 0000000000..891a151340 --- /dev/null +++ b/experimental/experimentalcarservice_unit_test/Android.bp @@ -0,0 +1,48 @@ +// 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. + +// Unit tests for the experimental car service +android_test { + + name: "ExperimentalCarServiceTests", + + srcs: ["src/**/*.java"], + + platform_apis: true, + certificate: "platform", + + libs: [ + "android.car", + "android.test.runner", + "android.test.base", + "android.test.mock", + ], + + static_libs: [ + "androidx.test.core", + "androidx.test.ext.junit", + "androidx.test.rules", + "car-frameworks-service", + "mockito-target-extended", + "truth-prebuilt", + "experimentalcar-service-test-static-lib" + ], + + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + + instrumentation_for: "ExperimentalCarService", +} diff --git a/experimental/experimentalcarservice_unit_test/AndroidManifest.xml b/experimental/experimentalcarservice_unit_test/AndroidManifest.xml new file mode 100644 index 0000000000..247d8f1681 --- /dev/null +++ b/experimental/experimentalcarservice_unit_test/AndroidManifest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2016 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:sharedUserId="com.google.android.car.uid.kitchensink" + package="com.android.experimentalcar.experimentalcarservice_unittest"> + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.experimentalcar.experimentalcarservice_unittest" + android:label="Unit Tests for Experimental Car APIs"/> + + <application android:label="ExperimentalCarServiceUnitTest" + tools:replace="android:label" + android:debuggable="true"> + <uses-library android:name="android.test.runner"/> + </application> +</manifest> diff --git a/experimental/experimentalcarservice_unit_test/src/com/android/experimentalcar/DriverDistractionExperimentalFeatureServiceTest.java b/experimental/experimentalcarservice_unit_test/src/com/android/experimentalcar/DriverDistractionExperimentalFeatureServiceTest.java new file mode 100644 index 0000000000..59ad9c519a --- /dev/null +++ b/experimental/experimentalcarservice_unit_test/src/com/android/experimentalcar/DriverDistractionExperimentalFeatureServiceTest.java @@ -0,0 +1,189 @@ +/* + * 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.google.common.truth.Truth.assertThat; + +import android.car.experimental.DriverAwarenessEvent; +import android.car.experimental.DriverAwarenessSupplierConfig; +import android.car.experimental.DriverAwarenessSupplierService; +import android.car.experimental.IDriverAwarenessSupplier; +import android.car.experimental.IDriverAwarenessSupplierCallback; +import android.content.Context; +import android.os.RemoteException; +import android.util.Pair; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; + +@RunWith(MockitoJUnitRunner.class) +public class DriverDistractionExperimentalFeatureServiceTest { + + private static final long INITIAL_TIME = 1000L; + private static final long PREFERRED_SUPPLIER_STALENESS = 10L; + + private final IDriverAwarenessSupplier mFallbackSupplier = + new IDriverAwarenessSupplier.Stub() { + @Override + public void onReady() throws RemoteException { + } + + @Override + public void setCallback(IDriverAwarenessSupplierCallback callback) + throws RemoteException { + } + }; + private final DriverAwarenessSupplierConfig mFallbackConfig = new DriverAwarenessSupplierConfig( + DriverAwarenessSupplierService.NO_STALENESS); + + private final IDriverAwarenessSupplier mPreferredSupplier = + new IDriverAwarenessSupplier.Stub() { + @Override + public void onReady() throws RemoteException { + } + + @Override + public void setCallback(IDriverAwarenessSupplierCallback callback) + throws RemoteException { + } + }; + private final DriverAwarenessSupplierConfig mPreferredSupplierConfig = + new DriverAwarenessSupplierConfig(PREFERRED_SUPPLIER_STALENESS); + + @Mock + private Context mContext; + + private DriverDistractionExperimentalFeatureService mService; + private FakeTimeSource mTimeSource; + private FakeTimer mTimer; + + @Before + public void setUp() throws Exception { + mTimeSource = new FakeTimeSource(INITIAL_TIME); + mTimer = new FakeTimer(); + mService = new DriverDistractionExperimentalFeatureService(mContext, mTimeSource, mTimer); + } + + @After + public void tearDown() throws Exception { + if (mService != null) { + mService.release(); + } + } + + @Test + public void testHandleDriverAwarenessEvent_updatesCurrentValue_withLatestEvent() + throws Exception { + mService.setDriverAwarenessSuppliers(Arrays.asList( + new Pair<>(mFallbackSupplier, mFallbackConfig))); + + float firstEmittedEvent = 0.7f; + emitEvent(mFallbackSupplier, INITIAL_TIME + 1, firstEmittedEvent); + + assertThat(getCurrentAwarenessValue()).isEqualTo(firstEmittedEvent); + } + + @Test + public void testHandleDriverAwarenessEvent_hasPreferredEvent_ignoresFallbackEvent() + throws Exception { + mService.setDriverAwarenessSuppliers(Arrays.asList( + new Pair<>(mFallbackSupplier, mFallbackConfig), + new Pair<>(mPreferredSupplier, mPreferredSupplierConfig))); + + // emit an event from the preferred supplier before the fallback supplier + float preferredValue = 0.6f; + emitEvent(mPreferredSupplier, INITIAL_TIME + 1, preferredValue); + float fallbackValue = 0.7f; + emitEvent(mFallbackSupplier, INITIAL_TIME + 2, fallbackValue); + + // even though the fallback supplier has a more recent timestamp, it is not the current + // since the event from the preferred supplier is still fresh + assertThat(getCurrentAwarenessValue()).isEqualTo(preferredValue); + } + + @Test + public void testHandleDriverAwarenessEvent_ignoresOldEvents() throws Exception { + mService.setDriverAwarenessSuppliers(Arrays.asList( + new Pair<>(mFallbackSupplier, mFallbackConfig))); + + float firstEmittedEvent = 0.7f; + emitEvent(mFallbackSupplier, INITIAL_TIME + 1, firstEmittedEvent); + long oldTime = INITIAL_TIME - 100; + emitEvent(mFallbackSupplier, oldTime, 0.6f); + + // the event with the old timestamp shouldn't overwrite the value with a more recent + // timestamp + assertThat(getCurrentAwarenessValue()).isEqualTo(firstEmittedEvent); + } + + @Test + public void testPreferredAwarenessEvent_becomesStale_fallsBackToFallbackEvent() + throws Exception { + mService.setDriverAwarenessSuppliers(Arrays.asList( + new Pair<>(mFallbackSupplier, mFallbackConfig), + new Pair<>(mPreferredSupplier, mPreferredSupplierConfig))); + + // emit an event from the preferred supplier before the fallback supplier + float preferredValue = 0.6f; + long preferredEventTime = INITIAL_TIME + 1; + mTimeSource.setElapsedRealtime(preferredEventTime); + emitEvent(mPreferredSupplier, preferredEventTime, preferredValue); + float fallbackValue = 0.7f; + long fallbackEventTime = INITIAL_TIME + 2; + mTimeSource.setElapsedRealtime(fallbackEventTime); + emitEvent(mFallbackSupplier, fallbackEventTime, fallbackValue); + + // the preferred supplier still has a fresh event + assertThat(getCurrentAwarenessValue()).isEqualTo(preferredValue); + + // go into the future + mTimeSource.setElapsedRealtime(preferredEventTime + PREFERRED_SUPPLIER_STALENESS + 1); + mTimer.executePendingTask(); + + // the preferred supplier's data has become stale + assertThat(getCurrentAwarenessValue()).isEqualTo(fallbackValue); + } + + private float getCurrentAwarenessValue() { + return mService.getCurrentDriverAwareness().mAwarenessEvent.getAwarenessValue(); + } + + /** + * Handle an event as if it were emitted from the specified supplier with the specified time and + * value. + */ + private void emitEvent(IDriverAwarenessSupplier supplier, long time, float value) + throws RemoteException { + long maxStaleness; + if (supplier == mFallbackSupplier) { + maxStaleness = DriverAwarenessSupplierService.NO_STALENESS; + } else { + maxStaleness = PREFERRED_SUPPLIER_STALENESS; + } + mService.handleDriverAwarenessEvent( + new DriverDistractionExperimentalFeatureService.DriverAwarenessEventWrapper( + new DriverAwarenessEvent(time, value), + supplier, + maxStaleness)); + } +} diff --git a/experimental/experimentalcarservice_unit_test/src/com/android/experimentalcar/FakeTimeSource.java b/experimental/experimentalcarservice_unit_test/src/com/android/experimentalcar/FakeTimeSource.java new file mode 100644 index 0000000000..eb82e1eeb8 --- /dev/null +++ b/experimental/experimentalcarservice_unit_test/src/com/android/experimentalcar/FakeTimeSource.java @@ -0,0 +1,44 @@ +/* + * 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; + +/** + * Fake implementation of {@link ITimeSource}. + */ +public class FakeTimeSource implements ITimeSource { + + private long mElapsedRealtime; + + /** + * Create an instance of {@link FakeTimeSource} with an initial time. + */ + FakeTimeSource(long elapsedRealtime) { + mElapsedRealtime = elapsedRealtime; + } + + /** + * Set the value that will be returned {@link #elapsedRealtime()}. + */ + void setElapsedRealtime(long elapsedRealtime) { + mElapsedRealtime = elapsedRealtime; + } + + @Override + public long elapsedRealtime() { + return mElapsedRealtime; + } +} diff --git a/experimental/experimentalcarservice_unit_test/src/com/android/experimentalcar/FakeTimer.java b/experimental/experimentalcarservice_unit_test/src/com/android/experimentalcar/FakeTimer.java new file mode 100644 index 0000000000..0bd9748324 --- /dev/null +++ b/experimental/experimentalcarservice_unit_test/src/com/android/experimentalcar/FakeTimer.java @@ -0,0 +1,44 @@ +/* + * 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 java.util.TimerTask; + +/** + * Fake implementation for {@link ITimer}. + */ +public class FakeTimer implements ITimer { + + private TimerTask mPendingTask; + + @Override + public void reset() { + mPendingTask = null; + } + + @Override + public void schedule(TimerTask task, long delay) { + mPendingTask = task; + } + + /** + * Immediately execute the scheduled task. + */ + void executePendingTask() { + mPendingTask.run(); + } +} diff --git a/experimental/service/Android.bp b/experimental/service/Android.bp index f2fba99097..b0c59c44fe 100644 --- a/experimental/service/Android.bp +++ b/experimental/service/Android.bp @@ -16,13 +16,12 @@ // Build the Experimental Car service. +experimentalcar_service_sources = ["src/**/*.java"] android_app { name: "ExperimentalCarService", - srcs: [ - "src/**/*.java" - ], + srcs: experimentalcar_service_sources, resource_dirs: ["res"], @@ -53,3 +52,32 @@ android_app { }, }, } + +//#################################################################################### +// Build a static library to help mocking various car services in testing. This is meant to be used +// for internal unit tests around the car service. +//#################################################################################### +android_library { + name: "experimentalcar-service-test-static-lib", + + srcs: experimentalcar_service_sources, + + resource_dirs: ["res"], + + libs: [ + "android.car", + ], + + static_libs: [ + "car-service-common-util-static-lib", + "car-experimental-api-static-lib", + ], + + min_sdk_version: "25", + + product_variables: { + pdk: { + enabled: false, + }, + }, +} diff --git a/experimental/service/AndroidManifest.xml b/experimental/service/AndroidManifest.xml index c28f400d67..86060d7907 100644 --- a/experimental/service/AndroidManifest.xml +++ b/experimental/service/AndroidManifest.xml @@ -15,19 +15,20 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - package="com.android.experimentalcar" - coreApp="true" - android:sharedUserId="android.uid.system"> + package="com.android.experimentalcar" + coreApp="true" + android:sharedUserId="android.uid.system"> - <original-package android:name="com.android.experimentalcar" /> + <original-package android:name="com.android.experimentalcar"/> <application android:label="@string/app_title" android:directBootAware="true" android:allowBackup="false" android:persistent="false"> <service android:name=".ExperimentalCarService" - android:singleUser="true"> + android:singleUser="true"> </service> + <service android:name=".TouchDriverAwarenessSupplier"/> + <service android:name=".SampleExternalDriverAwarenessSupplier"/> </application> </manifest> diff --git a/experimental/service/src/com/android/experimentalcar/DriverDistractionExperimentalFeatureService.java b/experimental/service/src/com/android/experimentalcar/DriverDistractionExperimentalFeatureService.java index 7a4f630ac7..f0d7d56d1c 100644 --- a/experimental/service/src/com/android/experimentalcar/DriverDistractionExperimentalFeatureService.java +++ b/experimental/service/src/com/android/experimentalcar/DriverDistractionExperimentalFeatureService.java @@ -16,28 +16,540 @@ package com.android.experimentalcar; +import android.annotation.Nullable; +import android.car.experimental.DriverAwarenessEvent; +import android.car.experimental.DriverAwarenessSupplierConfig; +import android.car.experimental.DriverAwarenessSupplierService; +import android.car.experimental.IDriverAwarenessSupplier; +import android.car.experimental.IDriverAwarenessSupplierCallback; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +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.Utils; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; +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. + * + * <p>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 implements CarServiceBase { + private static final String TAG = "CAR.DriverDistractionService"; + + private static final float DEFAULT_AWARENESS_VALUE_FOR_LOG = 1.0f; + private static final int MAX_DRIVER_AWARENESS_EVENT_LOG_COUNT = 50; + + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private final ArrayDeque<Utils.TransitionLog> mDriverAwarenessTransitionLogs = + new ArrayDeque<>(); + + /** + * All the active service connections. + */ + @GuardedBy("mLock") + private final List<ServiceConnection> mServiceConnections = new ArrayList<>(); + + /** + * The binder for each supplier. + */ + @GuardedBy("mLock") + private final Map<ComponentName, IDriverAwarenessSupplier> mSupplierBinders = new HashMap<>(); + + /** + * The configuration for each supplier. + */ + @GuardedBy("mLock") + private final Map<IDriverAwarenessSupplier, DriverAwarenessSupplierConfig> 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<IDriverAwarenessSupplier> 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<IDriverAwarenessSupplier, Integer> mDriverAwarenessSupplierPriorities = + new HashMap<>(); + + /** + * Comparator used to sort {@link #mDriverAwarenessSupplierPriorities}. + */ + private final Comparator<IDriverAwarenessSupplier> 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<IDriverAwarenessSupplier, DriverAwarenessEventWrapper> + mCurrentAwarenessEventsMap = + new HashMap<>(); + + /** + * The awareness event that is currently being used to determine the driver awareness level. + * + * <p>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; + + private final ITimeSource mTimeSource; + private final Context mContext; + + /** + * Create an instance of {@link DriverDistractionExperimentalFeatureService}. + * + * @param context the context + * @param timeSource the source that provides the current time + * @param timer the timer used for scheduling + */ + DriverDistractionExperimentalFeatureService( + Context context, + ITimeSource timeSource, + ITimer timer) { + mContext = context; + mTimeSource = timeSource; + mExpiredDriverAwarenessTimer = timer; + } + @Override public void init() { - // Nothing to do + // 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(); + addDriverAwarenessSupplier(touchComponent, touchSupplier, 0); + touchSupplier.setCallback(new DriverAwarenessSupplierCallback(touchComponent)); + touchSupplier.onReady(); + + // TODO(b/143492728) load preferred suppliers from xml - this is just an example + ComponentName externalComponent = new ComponentName(mContext, + SampleExternalDriverAwarenessSupplier.class); + bindDriverAwarenessSupplierService(externalComponent, /* priority= */ 1); } @Override public void release() { - // Nothing to do + logd("release"); + synchronized (mLock) { + for (ServiceConnection serviceConnection : mServiceConnections) { + mContext.unbindService(serviceConnection); + } + } } @Override public void dump(PrintWriter writer) { writer.println("*DriverDistractionExperimentalFeatureService*"); + 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 (since boot): " + + (mCurrentDriverAwareness == null ? "unknown" + : mCurrentDriverAwareness.mAwarenessEvent.getTimeStamp())); + writer.println("Driver Awareness change log:"); + for (Utils.TransitionLog log : mDriverAwarenessTransitionLogs) { + 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: + * <ul> + * <li>Cache the driver awareness event for its supplier</li> + * <li>Update the current awareness value</li> + * <li>Register to refresh the awareness value again when the new current expires</li> + * </ul> + * + * @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()); + addDriverAwarenessTransitionLogLocked(oldAwarenessValue, + awarenessEventWrapper.mAwarenessEvent.getAwarenessValue(), + awarenessEventWrapper.mSupplier.getClass().getSimpleName()); + } + } + + /** + * 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<Pair<IDriverAwarenessSupplier, DriverAwarenessSupplierConfig>> suppliers) { + mPrioritizedDriverAwarenessSuppliers.clear(); + mDriverAwarenessSupplierPriorities.clear(); + for (int i = 0; i < suppliers.size(); i++) { + Pair<IDriverAwarenessSupplier, DriverAwarenessSupplierConfig> pair = suppliers.get(i); + mSupplierConfigs.put(pair.first, pair.second); + mDriverAwarenessSupplierPriorities.put(pair.first, i); + mPrioritizedDriverAwarenessSuppliers.add(pair.first); + } + mPrioritizedDriverAwarenessSuppliers.sort(mPrioritizedSuppliersComparator); + } + + /** + * 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(); + } + } + }, delay); + + logd(String.format( + "Current awareness value is stale after %sms and is scheduled to expire in %sms", + mCurrentDriverAwareness.mMaxStaleness, delay)); + } + + /** + * Add the driver awareness state change to the transition log. + * + * @param oldValue the old driver awareness value + * @param newValue the new driver awareness value + * @param supplierName the name of the supplier that is responsible for the new value + */ + @GuardedBy("mLock") + private void addDriverAwarenessTransitionLogLocked(float oldValue, float newValue, + String supplierName) { + if (mDriverAwarenessTransitionLogs.size() >= MAX_DRIVER_AWARENESS_EVENT_LOG_COUNT) { + mDriverAwarenessTransitionLogs.remove(); + } + + Utils.TransitionLog tLog = new Utils.TransitionLog(TAG, (int) (oldValue * 100), + (int) (newValue * 100), System.currentTimeMillis(), + "Driver awareness updated by " + supplierName); + mDriverAwarenessTransitionLogs.add(tLog); + } + + private static void logd(String message) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, message); + } + } + + /** + * 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); + } } } diff --git a/experimental/service/src/com/android/experimentalcar/IExperimentalCarImpl.java b/experimental/service/src/com/android/experimentalcar/IExperimentalCarImpl.java index 4d50d67ba7..bed163d94e 100644 --- a/experimental/service/src/com/android/experimentalcar/IExperimentalCarImpl.java +++ b/experimental/service/src/com/android/experimentalcar/IExperimentalCarImpl.java @@ -174,7 +174,10 @@ public final class IExperimentalCarImpl extends IExperimentalCar.Stub { case ExperimentalCar.TEST_EXPERIMENTAL_FEATURE_SERVICE: return new TestDemoExperimentalFeatureService(); case ExperimentalCar.DRIVER_DISTRACTION_EXPERIMENTAL_FEATURE_SERVICE: - return new DriverDistractionExperimentalFeatureService(); + return new DriverDistractionExperimentalFeatureService( + mContext, + new SystemTimeSource(), + new SystemTimer()); default: return null; } diff --git a/experimental/service/src/com/android/experimentalcar/ITimeSource.java b/experimental/service/src/com/android/experimentalcar/ITimeSource.java new file mode 100644 index 0000000000..3a597c314b --- /dev/null +++ b/experimental/service/src/com/android/experimentalcar/ITimeSource.java @@ -0,0 +1,30 @@ +/* + * 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.os.SystemClock; + +/** + * Interface to abstract dependency on {@link android.os.SystemClock}. + */ +public interface ITimeSource { + + /** + * See {@link SystemClock#elapsedRealtime()}. + */ + long elapsedRealtime(); +} diff --git a/experimental/service/src/com/android/experimentalcar/ITimer.java b/experimental/service/src/com/android/experimentalcar/ITimer.java new file mode 100644 index 0000000000..dbaa3c0111 --- /dev/null +++ b/experimental/service/src/com/android/experimentalcar/ITimer.java @@ -0,0 +1,35 @@ +/* + * 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 java.util.TimerTask; + +/** + * Interface to abstract dependency on {@link java.util.Timer}. + */ +public interface ITimer { + + /** + * Cancel the timer and prepare for new events. + */ + void reset(); + + /** + * See {@link java.util.Timer#schedule(TimerTask, long)}. + */ + void schedule(TimerTask task, long delay); +} diff --git a/experimental/service/src/com/android/experimentalcar/SampleExternalDriverAwarenessSupplier.java b/experimental/service/src/com/android/experimentalcar/SampleExternalDriverAwarenessSupplier.java new file mode 100644 index 0000000000..917ac20ae9 --- /dev/null +++ b/experimental/service/src/com/android/experimentalcar/SampleExternalDriverAwarenessSupplier.java @@ -0,0 +1,58 @@ +/* + * 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.car.experimental.DriverAwarenessEvent; +import android.car.experimental.DriverAwarenessSupplierService; +import android.content.Intent; +import android.os.IBinder; +import android.os.SystemClock; +import android.util.Log; + +/** + * Simple example of how an external driver awareness supplier service could be implemented. + */ +public class SampleExternalDriverAwarenessSupplier extends DriverAwarenessSupplierService { + + private static final String TAG = "SampleExternalDriverAwarenessSupplier"; + private static final float INITIAL_DRIVER_AWARENESS_VALUE = 1.0f; + private static final long MAX_STALENESS_MILLIS = 100L; + + @Override + public long getMaxStalenessMillis() { + return MAX_STALENESS_MILLIS; + } + + @Override + public void onReady() { + // send an initial event, as required by the IDriverAwarenessSupplierCallback spec + onDriverAwarenessUpdated(new DriverAwarenessEvent(SystemClock.elapsedRealtime(), + INITIAL_DRIVER_AWARENESS_VALUE)); + } + + @Override + public IBinder onBind(Intent intent) { + logd("onBind, intent: " + intent); + return super.onBind(intent); + } + + private static void logd(String message) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, message); + } + } +} diff --git a/experimental/service/src/com/android/experimentalcar/SystemTimeSource.java b/experimental/service/src/com/android/experimentalcar/SystemTimeSource.java new file mode 100644 index 0000000000..217b6fe91a --- /dev/null +++ b/experimental/service/src/com/android/experimentalcar/SystemTimeSource.java @@ -0,0 +1,30 @@ +/* + * 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.os.SystemClock; + +/** + * Time source implementation using {@link SystemClock}. + */ +public class SystemTimeSource implements ITimeSource { + + @Override + public long elapsedRealtime() { + return SystemClock.elapsedRealtime(); + } +} diff --git a/experimental/service/src/com/android/experimentalcar/SystemTimer.java b/experimental/service/src/com/android/experimentalcar/SystemTimer.java new file mode 100644 index 0000000000..9eb1dc5842 --- /dev/null +++ b/experimental/service/src/com/android/experimentalcar/SystemTimer.java @@ -0,0 +1,39 @@ +/* + * 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 java.util.Timer; +import java.util.TimerTask; + +/** + * Implementation of {@link ITimer} that uses {@link Timer}. + */ +public class SystemTimer implements ITimer { + + private Timer mTimer = new Timer(); + + @Override + public void reset() { + mTimer.cancel(); + mTimer = new Timer(); + } + + @Override + public void schedule(TimerTask task, long delay) { + mTimer.schedule(task, delay); + } +} diff --git a/experimental/service/src/com/android/experimentalcar/TouchDriverAwarenessSupplier.java b/experimental/service/src/com/android/experimentalcar/TouchDriverAwarenessSupplier.java new file mode 100644 index 0000000000..f144ad413a --- /dev/null +++ b/experimental/service/src/com/android/experimentalcar/TouchDriverAwarenessSupplier.java @@ -0,0 +1,99 @@ +/* + * 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.car.experimental.DriverAwarenessEvent; +import android.car.experimental.DriverAwarenessSupplierConfig; +import android.car.experimental.DriverAwarenessSupplierService; +import android.car.experimental.IDriverAwarenessSupplier; +import android.car.experimental.IDriverAwarenessSupplierCallback; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.Log; + +import java.util.Random; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + + +/** + * A driver awareness supplier that estimates the driver's current awareness level based on touches + * on the headunit. + */ +// TODO(b/136663803) update with actual implementation and configuration +public class TouchDriverAwarenessSupplier extends IDriverAwarenessSupplier.Stub { + + private static final String TAG = "Car.TouchDriverAwarenessSupplier"; + private static final float INITIAL_DRIVER_AWARENESS_VALUE = 1.0f; + private static final long MAX_STALENESS = DriverAwarenessSupplierService.NO_STALENESS; + + // demo classes - emit a random value every 1s as a proof of concept + private final Random mRandom = new Random(); + private final ScheduledExecutorService mDemoScheduler = + Executors.newScheduledThreadPool(1); + + private ScheduledFuture<?> mDemoScheduleHandle; + private IDriverAwarenessSupplierCallback mDriverAwarenessSupplierCallback; + + private final Runnable mEmitDemoAwarenessRunnable = () -> { + long timestamp = SystemClock.elapsedRealtime(); + float demoAwareness = mRandom.nextFloat(); + try { + mDriverAwarenessSupplierCallback.onDriverAwarenessUpdated( + new DriverAwarenessEvent(timestamp, demoAwareness)); + } catch (RemoteException e) { + Log.e(TAG, "Unable to emit awareness event", e); + } + }; + + @Override + public void onReady() { + try { + mDriverAwarenessSupplierCallback.onConfigLoaded( + new DriverAwarenessSupplierConfig(MAX_STALENESS)); + } catch (RemoteException e) { + Log.e(TAG, "Unable to send config - abandoning ready process", e); + return; + } + startSupplyingDemoDriverAwareness(); + } + + @Override + public void setCallback(IDriverAwarenessSupplierCallback callback) { + mDriverAwarenessSupplierCallback = callback; + } + + private void startSupplyingDemoDriverAwareness() { + // send an initial event, as required by the IDriverAwarenessSupplierCallback spec + try { + mDriverAwarenessSupplierCallback.onDriverAwarenessUpdated( + new DriverAwarenessEvent(SystemClock.elapsedRealtime(), + INITIAL_DRIVER_AWARENESS_VALUE)); + } catch (RemoteException e) { + Log.e(TAG, "Unable to emit initial awareness event", e); + } + + // TODO(b/136663803) update with actual implementation and configuration + mDemoScheduleHandle = mDemoScheduler.scheduleAtFixedRate( + mEmitDemoAwarenessRunnable, + /* initialDelay= */ 0, + /* period= */ 1, + TimeUnit.SECONDS); + } +} |