diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 8175d25573cc7..fbabeeee06f22 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2963,6 +2963,7 @@ package android.companion.virtual { method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int); method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int); method public int getDeviceId(); + method @Nullable public android.companion.virtual.sensor.VirtualSensor getVirtualSensor(int, @NonNull String); method public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer); method public void removeActivityListener(@NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener); method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setShowPointerIcon(boolean); @@ -2980,6 +2981,7 @@ package android.companion.virtual { method public int getLockState(); method @Nullable public String getName(); method @NonNull public java.util.Set getUsersWithMatchingAccounts(); + method @NonNull public java.util.List getVirtualSensorConfigs(); method public void writeToParcel(@NonNull android.os.Parcel, int); field public static final int ACTIVITY_POLICY_DEFAULT_ALLOWED = 0; // 0x0 field public static final int ACTIVITY_POLICY_DEFAULT_BLOCKED = 1; // 0x1 @@ -2996,6 +2998,7 @@ package android.companion.virtual { public static final class VirtualDeviceParams.Builder { ctor public VirtualDeviceParams.Builder(); method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder addDevicePolicy(int, int); + method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder addVirtualSensorConfig(@NonNull android.companion.virtual.sensor.VirtualSensorConfig); method @NonNull public android.companion.virtual.VirtualDeviceParams build(); method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAllowedActivities(@NonNull java.util.Set); method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAllowedCrossTaskNavigations(@NonNull java.util.Set); @@ -3053,6 +3056,50 @@ package android.companion.virtual.audio { } +package android.companion.virtual.sensor { + + public class VirtualSensor { + method @NonNull public String getName(); + method public int getType(); + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendSensorEvent(@NonNull android.companion.virtual.sensor.VirtualSensorEvent); + } + + public static interface VirtualSensor.SensorStateChangeCallback { + method public void onStateChanged(boolean, @NonNull java.time.Duration, @NonNull java.time.Duration); + } + + public final class VirtualSensorConfig implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public String getName(); + method public int getType(); + method @Nullable public String getVendor(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class VirtualSensorConfig.Builder { + ctor public VirtualSensorConfig.Builder(int, @NonNull String); + method @NonNull public android.companion.virtual.sensor.VirtualSensorConfig build(); + method @NonNull public android.companion.virtual.sensor.VirtualSensorConfig.Builder setStateChangeCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.sensor.VirtualSensor.SensorStateChangeCallback); + method @NonNull public android.companion.virtual.sensor.VirtualSensorConfig.Builder setVendor(@Nullable String); + } + + public final class VirtualSensorEvent implements android.os.Parcelable { + method public int describeContents(); + method public long getTimestampNanos(); + method @NonNull public float[] getValues(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class VirtualSensorEvent.Builder { + ctor public VirtualSensorEvent.Builder(@NonNull float[]); + method @NonNull public android.companion.virtual.sensor.VirtualSensorEvent build(); + method @NonNull public android.companion.virtual.sensor.VirtualSensorEvent.Builder setTimestampNanos(long); + } + +} + package android.content { public class ApexEnvironment { diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl index 295d69d4b27db..0837d85bb91c3 100644 --- a/core/java/android/companion/virtual/IVirtualDevice.aidl +++ b/core/java/android/companion/virtual/IVirtualDevice.aidl @@ -19,6 +19,9 @@ package android.companion.virtual; import android.app.PendingIntent; import android.companion.virtual.audio.IAudioConfigChangedCallback; import android.companion.virtual.audio.IAudioRoutingCallback; +import android.companion.virtual.sensor.IVirtualSensorStateChangeCallback; +import android.companion.virtual.sensor.VirtualSensorConfig; +import android.companion.virtual.sensor.VirtualSensorEvent; import android.graphics.Point; import android.graphics.PointF; import android.hardware.input.VirtualKeyEvent; @@ -96,6 +99,24 @@ interface IVirtualDevice { boolean sendScrollEvent(IBinder token, in VirtualMouseScrollEvent event); boolean sendTouchEvent(IBinder token, in VirtualTouchEvent event); + /** + * Creates a virtual sensor, capable of injecting sensor events into the system. + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + void createVirtualSensor(IBinder tokenm, in VirtualSensorConfig config); + + /** + * Removes the sensor corresponding to the given token from the system. + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + void unregisterSensor(IBinder token); + + /** + * Sends an event to the virtual sensor corresponding to the given token. + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + boolean sendSensorEvent(IBinder token, in VirtualSensorEvent event); + /** * Launches a pending intent on the given display that is owned by this virtual device. */ diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index c14bb1beb0252..fbb4bd0bdb3f8 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -28,6 +28,8 @@ import android.app.PendingIntent; import android.companion.AssociationInfo; import android.companion.virtual.audio.VirtualAudioDevice; import android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback; +import android.companion.virtual.sensor.VirtualSensor; +import android.companion.virtual.sensor.VirtualSensorConfig; import android.content.ComponentName; import android.content.Context; import android.graphics.Point; @@ -58,6 +60,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.IntConsumer; @@ -250,7 +253,10 @@ public final class VirtualDeviceManager { }; @Nullable private VirtualAudioDevice mVirtualAudioDevice; + @NonNull + private List mVirtualSensors = new ArrayList<>(); + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) private VirtualDevice( IVirtualDeviceManager service, Context context, @@ -264,6 +270,10 @@ public final class VirtualDeviceManager { associationId, params, mActivityListenerBinder); + final List virtualSensorConfigs = params.getVirtualSensorConfigs(); + for (int i = 0; i < virtualSensorConfigs.size(); ++i) { + mVirtualSensors.add(createVirtualSensor(virtualSensorConfigs.get(i))); + } } /** @@ -277,6 +287,23 @@ public final class VirtualDeviceManager { } } + /** + * Returns this device's sensor with the given type and name, if any. + * + * @see VirtualDeviceParams.Builder#addVirtualSensorConfig + * + * @param type The type of the sensor. + * @param name The name of the sensor. + * @return The matching sensor if found, {@code null} otherwise. + */ + @Nullable + public VirtualSensor getVirtualSensor(int type, @NonNull String name) { + return mVirtualSensors.stream() + .filter(sensor -> sensor.getType() == type && sensor.getName().equals(name)) + .findAny() + .orElse(null); + } + /** * Launches a given pending intent on the give display ID. * @@ -379,6 +406,7 @@ public final class VirtualDeviceManager { @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close() { try { + // This also takes care of unregistering all virtual sensors. mVirtualDevice.close(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -563,6 +591,28 @@ public final class VirtualDeviceManager { } } + /** + * Creates a virtual sensor, capable of injecting sensor events into the system. Only for + * internal use, since device sensors must remain valid for the entire lifetime of the + * device. + * + * @param config The configuration of the sensor. + * @hide + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + @NonNull + public VirtualSensor createVirtualSensor(@NonNull VirtualSensorConfig config) { + Objects.requireNonNull(config); + try { + final IBinder token = new Binder( + "android.hardware.sensor.VirtualSensor:" + config.getName()); + mVirtualDevice.createVirtualSensor(token, config); + return new VirtualSensor(config.getType(), config.getName(), mVirtualDevice, token); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Adds an activity listener to listen for events such as top activity change or virtual * display task stack became empty. diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java index f8c2e34a0e2ab..bad26c6ed10d2 100644 --- a/core/java/android/companion/virtual/VirtualDeviceParams.java +++ b/core/java/android/companion/virtual/VirtualDeviceParams.java @@ -23,20 +23,22 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; +import android.companion.virtual.sensor.VirtualSensorConfig; import android.content.ComponentName; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; import android.util.ArraySet; +import android.util.SparseArray; import android.util.SparseIntArray; -import com.android.internal.util.Preconditions; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.Set; @@ -158,6 +160,7 @@ public final class VirtualDeviceParams implements Parcelable { @Nullable private final String mName; // Mapping of @PolicyType to @DevicePolicy @NonNull private final SparseIntArray mDevicePolicies; + @NonNull private final List mVirtualSensorConfigs; private VirtualDeviceParams( @LockState int lockState, @@ -169,24 +172,22 @@ public final class VirtualDeviceParams implements Parcelable { @NonNull Set blockedActivities, @ActivityPolicy int defaultActivityPolicy, @Nullable String name, - @NonNull SparseIntArray devicePolicies) { - Preconditions.checkNotNull(usersWithMatchingAccounts); - Preconditions.checkNotNull(allowedCrossTaskNavigations); - Preconditions.checkNotNull(blockedCrossTaskNavigations); - Preconditions.checkNotNull(allowedActivities); - Preconditions.checkNotNull(blockedActivities); - Preconditions.checkNotNull(devicePolicies); - + @NonNull SparseIntArray devicePolicies, + @NonNull List virtualSensorConfigs) { mLockState = lockState; - mUsersWithMatchingAccounts = new ArraySet<>(usersWithMatchingAccounts); - mAllowedCrossTaskNavigations = new ArraySet<>(allowedCrossTaskNavigations); - mBlockedCrossTaskNavigations = new ArraySet<>(blockedCrossTaskNavigations); + mUsersWithMatchingAccounts = + new ArraySet<>(Objects.requireNonNull(usersWithMatchingAccounts)); + mAllowedCrossTaskNavigations = + new ArraySet<>(Objects.requireNonNull(allowedCrossTaskNavigations)); + mBlockedCrossTaskNavigations = + new ArraySet<>(Objects.requireNonNull(blockedCrossTaskNavigations)); mDefaultNavigationPolicy = defaultNavigationPolicy; - mAllowedActivities = new ArraySet<>(allowedActivities); - mBlockedActivities = new ArraySet<>(blockedActivities); + mAllowedActivities = new ArraySet<>(Objects.requireNonNull(allowedActivities)); + mBlockedActivities = new ArraySet<>(Objects.requireNonNull(blockedActivities)); mDefaultActivityPolicy = defaultActivityPolicy; mName = name; - mDevicePolicies = devicePolicies; + mDevicePolicies = Objects.requireNonNull(devicePolicies); + mVirtualSensorConfigs = Objects.requireNonNull(virtualSensorConfigs); } @SuppressWarnings("unchecked") @@ -201,6 +202,8 @@ public final class VirtualDeviceParams implements Parcelable { mDefaultActivityPolicy = parcel.readInt(); mName = parcel.readString8(); mDevicePolicies = parcel.readSparseIntArray(); + mVirtualSensorConfigs = new ArrayList<>(); + parcel.readTypedList(mVirtualSensorConfigs, VirtualSensorConfig.CREATOR); } /** @@ -316,6 +319,15 @@ public final class VirtualDeviceParams implements Parcelable { return mDevicePolicies.get(policyType, DEVICE_POLICY_DEFAULT); } + /** + * Returns the configurations for all sensors that should be created for this device. + * + * @see Builder#addVirtualSensorConfig + */ + public @NonNull List getVirtualSensorConfigs() { + return mVirtualSensorConfigs; + } + @Override public int describeContents() { return 0; @@ -333,6 +345,7 @@ public final class VirtualDeviceParams implements Parcelable { dest.writeInt(mDefaultActivityPolicy); dest.writeString8(mName); dest.writeSparseIntArray(mDevicePolicies); + dest.writeTypedList(mVirtualSensorConfigs); } @Override @@ -428,6 +441,7 @@ public final class VirtualDeviceParams implements Parcelable { private boolean mDefaultActivityPolicyConfigured = false; @Nullable private String mName; @NonNull private SparseIntArray mDevicePolicies = new SparseIntArray(); + @NonNull private List mVirtualSensorConfigs = new ArrayList<>(); /** * Sets the lock state of the device. The permission {@code ADD_ALWAYS_UNLOCKED_DISPLAY} @@ -467,8 +481,7 @@ public final class VirtualDeviceParams implements Parcelable { @NonNull public Builder setUsersWithMatchingAccounts( @NonNull Set usersWithMatchingAccounts) { - Preconditions.checkNotNull(usersWithMatchingAccounts); - mUsersWithMatchingAccounts = usersWithMatchingAccounts; + mUsersWithMatchingAccounts = Objects.requireNonNull(usersWithMatchingAccounts); return this; } @@ -491,7 +504,6 @@ public final class VirtualDeviceParams implements Parcelable { @NonNull public Builder setAllowedCrossTaskNavigations( @NonNull Set allowedCrossTaskNavigations) { - Preconditions.checkNotNull(allowedCrossTaskNavigations); if (mDefaultNavigationPolicyConfigured && mDefaultNavigationPolicy != NAVIGATION_POLICY_DEFAULT_BLOCKED) { throw new IllegalArgumentException( @@ -500,7 +512,7 @@ public final class VirtualDeviceParams implements Parcelable { } mDefaultNavigationPolicy = NAVIGATION_POLICY_DEFAULT_BLOCKED; mDefaultNavigationPolicyConfigured = true; - mAllowedCrossTaskNavigations = allowedCrossTaskNavigations; + mAllowedCrossTaskNavigations = Objects.requireNonNull(allowedCrossTaskNavigations); return this; } @@ -523,7 +535,6 @@ public final class VirtualDeviceParams implements Parcelable { @NonNull public Builder setBlockedCrossTaskNavigations( @NonNull Set blockedCrossTaskNavigations) { - Preconditions.checkNotNull(blockedCrossTaskNavigations); if (mDefaultNavigationPolicyConfigured && mDefaultNavigationPolicy != NAVIGATION_POLICY_DEFAULT_ALLOWED) { throw new IllegalArgumentException( @@ -532,7 +543,7 @@ public final class VirtualDeviceParams implements Parcelable { } mDefaultNavigationPolicy = NAVIGATION_POLICY_DEFAULT_ALLOWED; mDefaultNavigationPolicyConfigured = true; - mBlockedCrossTaskNavigations = blockedCrossTaskNavigations; + mBlockedCrossTaskNavigations = Objects.requireNonNull(blockedCrossTaskNavigations); return this; } @@ -551,7 +562,6 @@ public final class VirtualDeviceParams implements Parcelable { */ @NonNull public Builder setAllowedActivities(@NonNull Set allowedActivities) { - Preconditions.checkNotNull(allowedActivities); if (mDefaultActivityPolicyConfigured && mDefaultActivityPolicy != ACTIVITY_POLICY_DEFAULT_BLOCKED) { throw new IllegalArgumentException( @@ -559,7 +569,7 @@ public final class VirtualDeviceParams implements Parcelable { } mDefaultActivityPolicy = ACTIVITY_POLICY_DEFAULT_BLOCKED; mDefaultActivityPolicyConfigured = true; - mAllowedActivities = allowedActivities; + mAllowedActivities = Objects.requireNonNull(allowedActivities); return this; } @@ -578,7 +588,6 @@ public final class VirtualDeviceParams implements Parcelable { */ @NonNull public Builder setBlockedActivities(@NonNull Set blockedActivities) { - Preconditions.checkNotNull(blockedActivities); if (mDefaultActivityPolicyConfigured && mDefaultActivityPolicy != ACTIVITY_POLICY_DEFAULT_ALLOWED) { throw new IllegalArgumentException( @@ -586,7 +595,7 @@ public final class VirtualDeviceParams implements Parcelable { } mDefaultActivityPolicy = ACTIVITY_POLICY_DEFAULT_ALLOWED; mDefaultActivityPolicyConfigured = true; - mBlockedActivities = blockedActivities; + mBlockedActivities = Objects.requireNonNull(blockedActivities); return this; } @@ -620,11 +629,50 @@ public final class VirtualDeviceParams implements Parcelable { return this; } + /** + * Adds a configuration for a sensor that should be created for this virtual device. + * + * Device sensors must remain valid for the entire lifetime of the device, hence they are + * created together with the device itself, and removed when the device is removed. + * + * Requires {@link #DEVICE_POLICY_CUSTOM} to be set for {@link #POLICY_TYPE_SENSORS}. + * + * @see android.companion.virtual.sensor.VirtualSensor + * @see #addDevicePolicy + */ + @NonNull + public Builder addVirtualSensorConfig(@NonNull VirtualSensorConfig virtualSensorConfig) { + mVirtualSensorConfigs.add(Objects.requireNonNull(virtualSensorConfig)); + return this; + } + /** * Builds the {@link VirtualDeviceParams} instance. + * + * @throws IllegalArgumentException if there's mismatch between policy definition and + * the passed parameters or if there are sensor configs with the same type and name. + * */ @NonNull public VirtualDeviceParams build() { + if (!mVirtualSensorConfigs.isEmpty() + && (mDevicePolicies.get(POLICY_TYPE_SENSORS, DEVICE_POLICY_DEFAULT) + != DEVICE_POLICY_CUSTOM)) { + throw new IllegalArgumentException( + "DEVICE_POLICY_CUSTOM for POLICY_TYPE_SENSORS is required for creating " + + "virtual sensors."); + } + SparseArray> sensorNameByType = new SparseArray(); + for (int i = 0; i < mVirtualSensorConfigs.size(); ++i) { + VirtualSensorConfig config = mVirtualSensorConfigs.get(i); + Set sensorNames = sensorNameByType.get(config.getType(), new ArraySet<>()); + if (!sensorNames.add(config.getName())) { + throw new IllegalArgumentException( + "Sensor names must be unique for a particular sensor type."); + } + sensorNameByType.put(config.getType(), sensorNames); + } + return new VirtualDeviceParams( mLockState, mUsersWithMatchingAccounts, @@ -635,7 +683,8 @@ public final class VirtualDeviceParams implements Parcelable { mBlockedActivities, mDefaultActivityPolicy, mName, - mDevicePolicies); + mDevicePolicies, + mVirtualSensorConfigs); } } } diff --git a/core/java/android/companion/virtual/sensor/IVirtualSensorStateChangeCallback.aidl b/core/java/android/companion/virtual/sensor/IVirtualSensorStateChangeCallback.aidl new file mode 100644 index 0000000000000..b99cc7eb67a53 --- /dev/null +++ b/core/java/android/companion/virtual/sensor/IVirtualSensorStateChangeCallback.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 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.companion.virtual.sensor; + +/** + * Interface for notification of listener registration changes for a virtual sensor. + * + * @hide + */ +oneway interface IVirtualSensorStateChangeCallback { + + /** + * Called when the registered listeners to a virtual sensor have changed. + * + * @param enabled Whether the sensor is enabled. + * @param samplingPeriodMicros The requested sensor's sampling period in microseconds. + * @param batchReportingLatencyMicros The requested maximum time interval in microseconds + * between the delivery of two batches of sensor events. + */ + void onStateChanged(boolean enabled, int samplingPeriodMicros, int batchReportLatencyMicros); +} diff --git a/core/java/android/companion/virtual/sensor/VirtualSensor.java b/core/java/android/companion/virtual/sensor/VirtualSensor.java new file mode 100644 index 0000000000000..a184481bdf99d --- /dev/null +++ b/core/java/android/companion/virtual/sensor/VirtualSensor.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2022 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.companion.virtual.sensor; + +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.companion.virtual.IVirtualDevice; +import android.os.IBinder; +import android.os.RemoteException; + +import java.time.Duration; + +/** + * Representation of a sensor on a remote device, capable of sending events, such as an + * accelerometer or a gyroscope. + * + * This registers the sensor device with the sensor framework as a runtime sensor. + * + * @hide + */ +@SystemApi +public class VirtualSensor { + + /** + * Interface for notification of listener registration changes for a virtual sensor. + */ + public interface SensorStateChangeCallback { + /** + * Called when the registered listeners to a virtual sensor have changed. + * + * @param enabled Whether the sensor is enabled. + * @param samplingPeriod The requested sampling period of the sensor. + * @param batchReportLatency The requested maximum time interval between the delivery of two + * batches of sensor events. + */ + void onStateChanged(boolean enabled, @NonNull Duration samplingPeriod, + @NonNull Duration batchReportLatency); + } + + private final int mType; + private final String mName; + private final IVirtualDevice mVirtualDevice; + private final IBinder mToken; + + /** + * @hide + */ + public VirtualSensor(int type, String name, IVirtualDevice virtualDevice, IBinder token) { + mType = type; + mName = name; + mVirtualDevice = virtualDevice; + mToken = token; + } + + /** + * Returns the + * type of the sensor. + */ + public int getType() { + return mType; + } + + /** + * Returns the name of the sensor. + */ + @NonNull + public String getName() { + return mName; + } + + /** + * Send a sensor event to the system. + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void sendSensorEvent(@NonNull VirtualSensorEvent event) { + try { + mVirtualDevice.sendSensorEvent(mToken, event); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.aidl b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.aidl new file mode 100644 index 0000000000000..48b463ab5d50b --- /dev/null +++ b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 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.companion.virtual.sensor; + +parcelable VirtualSensorConfig; diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java new file mode 100644 index 0000000000000..7982fa59daf3e --- /dev/null +++ b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2022 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.companion.virtual.sensor; + +import static java.util.concurrent.TimeUnit.MICROSECONDS; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * Configuration for creation of a virtual sensor. + * @see VirtualSensor + * @hide + */ +@SystemApi +public final class VirtualSensorConfig implements Parcelable { + + private final int mType; + @NonNull + private final String mName; + @Nullable + private final String mVendor; + @Nullable + private final IVirtualSensorStateChangeCallback mStateChangeCallback; + + private VirtualSensorConfig(int type, @NonNull String name, @Nullable String vendor, + @Nullable IVirtualSensorStateChangeCallback stateChangeCallback) { + mType = type; + mName = name; + mVendor = vendor; + mStateChangeCallback = stateChangeCallback; + } + + private VirtualSensorConfig(@NonNull Parcel parcel) { + mType = parcel.readInt(); + mName = parcel.readString8(); + mVendor = parcel.readString8(); + mStateChangeCallback = + IVirtualSensorStateChangeCallback.Stub.asInterface(parcel.readStrongBinder()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int flags) { + parcel.writeInt(mType); + parcel.writeString8(mName); + parcel.writeString8(mVendor); + parcel.writeStrongBinder( + mStateChangeCallback != null ? mStateChangeCallback.asBinder() : null); + } + + /** + * Returns the + * type of the sensor. + */ + public int getType() { + return mType; + } + + /** + * Returns the name of the sensor, which must be unique per sensor type for each virtual device. + */ + @NonNull + public String getName() { + return mName; + } + + /** + * Returns the vendor string of the sensor. + * @see Builder#setVendor + */ + @Nullable + public String getVendor() { + return mVendor; + } + + /** + * Returns the callback to get notified about changes in the sensor listeners. + * @hide + */ + @Nullable + public IVirtualSensorStateChangeCallback getStateChangeCallback() { + return mStateChangeCallback; + } + + /** + * Builder for {@link VirtualSensorConfig}. + */ + public static final class Builder { + + private final int mType; + @NonNull + private final String mName; + @Nullable + private String mVendor; + @Nullable + private IVirtualSensorStateChangeCallback mStateChangeCallback; + + private static class SensorStateChangeCallbackDelegate + extends IVirtualSensorStateChangeCallback.Stub { + @NonNull + private final Executor mExecutor; + @NonNull + private final VirtualSensor.SensorStateChangeCallback mCallback; + + SensorStateChangeCallbackDelegate(@NonNull @CallbackExecutor Executor executor, + @NonNull VirtualSensor.SensorStateChangeCallback callback) { + mCallback = callback; + mExecutor = executor; + } + @Override + public void onStateChanged(boolean enabled, int samplingPeriodMicros, + int batchReportLatencyMicros) { + final Duration samplingPeriod = + Duration.ofNanos(MICROSECONDS.toNanos(samplingPeriodMicros)); + final Duration batchReportingLatency = + Duration.ofNanos(MICROSECONDS.toNanos(batchReportLatencyMicros)); + mExecutor.execute(() -> mCallback.onStateChanged( + enabled, samplingPeriod, batchReportingLatency)); + } + } + + /** + * Creates a new builder. + * + * @param type The + * type of the sensor. + * @param name The name of the sensor. Must be unique among all sensors with the same type + * that belong to the same virtual device. + */ + public Builder(int type, @NonNull String name) { + mType = type; + mName = Objects.requireNonNull(name); + } + + /** + * Creates a new {@link VirtualSensorConfig}. + */ + @NonNull + public VirtualSensorConfig build() { + return new VirtualSensorConfig(mType, mName, mVendor, mStateChangeCallback); + } + + /** + * Sets the vendor string of the sensor. + */ + @NonNull + public VirtualSensorConfig.Builder setVendor(@Nullable String vendor) { + mVendor = vendor; + return this; + } + + /** + * Sets the callback to get notified about changes in the sensor listeners. + * + * @param executor The executor where the callback is executed on. + * @param callback The callback to get notified when the state of the sensor + * listeners has changed, see {@link VirtualSensor.SensorStateChangeCallback} + */ + @SuppressLint("MissingGetterMatchingBuilder") + @NonNull + public VirtualSensorConfig.Builder setStateChangeCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull VirtualSensor.SensorStateChangeCallback callback) { + mStateChangeCallback = new SensorStateChangeCallbackDelegate( + Objects.requireNonNull(executor), + Objects.requireNonNull(callback)); + return this; + } + } + + @NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator<>() { + public VirtualSensorConfig createFromParcel(Parcel source) { + return new VirtualSensorConfig(source); + } + + public VirtualSensorConfig[] newArray(int size) { + return new VirtualSensorConfig[size]; + } + }; +} diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorEvent.aidl b/core/java/android/companion/virtual/sensor/VirtualSensorEvent.aidl new file mode 100644 index 0000000000000..99439465a48fb --- /dev/null +++ b/core/java/android/companion/virtual/sensor/VirtualSensorEvent.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 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.companion.virtual.sensor; + +parcelable VirtualSensorEvent; \ No newline at end of file diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorEvent.java b/core/java/android/companion/virtual/sensor/VirtualSensorEvent.java new file mode 100644 index 0000000000000..8f8860ed5e6e0 --- /dev/null +++ b/core/java/android/companion/virtual/sensor/VirtualSensorEvent.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2022 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.companion.virtual.sensor; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; + + +/** + * A sensor event that originated from a virtual device's sensor. + * + * @hide + */ +@SystemApi +public final class VirtualSensorEvent implements Parcelable { + + @NonNull + private float[] mValues; + private long mTimestampNanos; + + private VirtualSensorEvent(@NonNull float[] values, long timestampNanos) { + mValues = values; + mTimestampNanos = timestampNanos; + } + + private VirtualSensorEvent(@NonNull Parcel parcel) { + final int valuesLength = parcel.readInt(); + mValues = new float[valuesLength]; + parcel.readFloatArray(mValues); + mTimestampNanos = parcel.readLong(); + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int parcelableFlags) { + parcel.writeInt(mValues.length); + parcel.writeFloatArray(mValues); + parcel.writeLong(mTimestampNanos); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Returns the values of this sensor event. The length and contents depend on the + * sensor type. + * @see android.hardware.SensorEvent#values + */ + @NonNull + public float[] getValues() { + return mValues; + } + + /** + * The time in nanoseconds at which the event happened. For a given sensor, each new sensor + * event should be monotonically increasing. + * + * @see Builder#setTimestampNanos(long) + */ + public long getTimestampNanos() { + return mTimestampNanos; + } + + /** + * Builder for {@link VirtualSensorEvent}. + */ + public static final class Builder { + + @NonNull + private float[] mValues; + private long mTimestampNanos = 0; + + /** + * Creates a new builder. + * @param values the values of the sensor event. @see android.hardware.SensorEvent#values + */ + public Builder(@NonNull float[] values) { + mValues = values; + } + + /** + * Creates a new {@link VirtualSensorEvent}. + */ + @NonNull + public VirtualSensorEvent build() { + if (mValues == null || mValues.length == 0) { + throw new IllegalArgumentException( + "Cannot build virtual sensor event with no values."); + } + if (mTimestampNanos <= 0) { + mTimestampNanos = SystemClock.elapsedRealtimeNanos(); + } + return new VirtualSensorEvent(mValues, mTimestampNanos); + } + + /** + * Sets the timestamp of this event. For a given sensor, each new sensor event should be + * monotonically increasing using the same time base as + * {@link android.os.SystemClock#elapsedRealtimeNanos()}. + * + * If not explicitly set, the current timestamp is used for the sensor event. + * + * @see android.hardware.SensorEvent#timestamp + */ + @NonNull + public Builder setTimestampNanos(long timestampNanos) { + mTimestampNanos = timestampNanos; + return this; + } + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator<>() { + public VirtualSensorEvent createFromParcel(Parcel source) { + return new VirtualSensorEvent(source); + } + + public VirtualSensorEvent[] newArray(int size) { + return new VirtualSensorEvent[size]; + } + }; +} diff --git a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java new file mode 100644 index 0000000000000..11afd045f364c --- /dev/null +++ b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022 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.companion.virtual.sensor; + +import static android.hardware.Sensor.TYPE_ACCELEROMETER; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.os.Parcel; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.os.BackgroundThread; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.time.Duration; + +@RunWith(AndroidJUnit4.class) +public class VirtualSensorConfigTest { + + private static final String SENSOR_NAME = "VirtualSensorName"; + private static final String SENSOR_VENDOR = "VirtualSensorVendor"; + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private VirtualSensor.SensorStateChangeCallback mSensorCallback; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void parcelAndUnparcel_matches() { + final VirtualSensorConfig originalConfig = + new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME) + .setVendor(SENSOR_VENDOR) + .setStateChangeCallback(BackgroundThread.getExecutor(), mSensorCallback) + .build(); + final Parcel parcel = Parcel.obtain(); + originalConfig.writeToParcel(parcel, /* flags= */ 0); + parcel.setDataPosition(0); + final VirtualSensorConfig recreatedConfig = + VirtualSensorConfig.CREATOR.createFromParcel(parcel); + assertThat(recreatedConfig.getType()).isEqualTo(originalConfig.getType()); + assertThat(recreatedConfig.getName()).isEqualTo(originalConfig.getName()); + assertThat(recreatedConfig.getVendor()).isEqualTo(originalConfig.getVendor()); + assertThat(recreatedConfig.getStateChangeCallback()).isNotNull(); + } + + @Test + public void sensorConfig_onlyRequiredFields() { + final VirtualSensorConfig config = + new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME).build(); + assertThat(config.getVendor()).isNull(); + assertThat(config.getStateChangeCallback()).isNull(); + } + + @Test + public void sensorConfig_sensorCallbackInvocation() throws Exception { + final VirtualSensorConfig config = + new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME) + .setStateChangeCallback(BackgroundThread.getExecutor(), mSensorCallback) + .build(); + + final Duration samplingPeriod = Duration.ofMillis(123); + final Duration batchLatency = Duration.ofMillis(456); + + config.getStateChangeCallback().onStateChanged(true, + (int) MILLISECONDS.toMicros(samplingPeriod.toMillis()), + (int) MILLISECONDS.toMicros(batchLatency.toMillis())); + + verify(mSensorCallback, timeout(1000)).onStateChanged(true, samplingPeriod, batchLatency); + } +} diff --git a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java new file mode 100644 index 0000000000000..a9583fdc2e2d7 --- /dev/null +++ b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2022 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.companion.virtual.sensor; + +import static com.google.common.truth.Truth.assertThat; + +import static org.testng.Assert.assertThrows; + +import android.os.Parcel; +import android.os.SystemClock; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class VirtualSensorEventTest { + + private static final long TIMESTAMP_NANOS = SystemClock.elapsedRealtimeNanos(); + private static final float[] SENSOR_VALUES = new float[] {1.2f, 3.4f, 5.6f}; + + @Test + public void parcelAndUnparcel_matches() { + final VirtualSensorEvent originalEvent = new VirtualSensorEvent.Builder(SENSOR_VALUES) + .setTimestampNanos(TIMESTAMP_NANOS) + .build(); + final Parcel parcel = Parcel.obtain(); + originalEvent.writeToParcel(parcel, /* flags= */ 0); + parcel.setDataPosition(0); + final VirtualSensorEvent recreatedEvent = + VirtualSensorEvent.CREATOR.createFromParcel(parcel); + assertThat(recreatedEvent.getValues()).isEqualTo(originalEvent.getValues()); + assertThat(recreatedEvent.getTimestampNanos()).isEqualTo(originalEvent.getTimestampNanos()); + } + + @Test + public void sensorEvent_nullValues() { + assertThrows( + IllegalArgumentException.class, () -> new VirtualSensorEvent.Builder(null).build()); + } + + @Test + public void sensorEvent_noValues() { + assertThrows( + IllegalArgumentException.class, + () -> new VirtualSensorEvent.Builder(new float[0]).build()); + } + + @Test + public void sensorEvent_noTimestamp_usesCurrentTime() { + final VirtualSensorEvent event = new VirtualSensorEvent.Builder(SENSOR_VALUES).build(); + assertThat(event.getValues()).isEqualTo(SENSOR_VALUES); + assertThat(TIMESTAMP_NANOS).isLessThan(event.getTimestampNanos()); + assertThat(event.getTimestampNanos()).isLessThan(SystemClock.elapsedRealtimeNanos()); + } + + @Test + public void sensorEvent_created() { + final VirtualSensorEvent event = new VirtualSensorEvent.Builder(SENSOR_VALUES) + .setTimestampNanos(TIMESTAMP_NANOS) + .build(); + assertThat(event.getTimestampNanos()).isEqualTo(TIMESTAMP_NANOS); + assertThat(event.getValues()).isEqualTo(SENSOR_VALUES); + } +} diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 0def25d322efb..019f582d4790e 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -38,6 +38,8 @@ import android.companion.virtual.VirtualDeviceManager.ActivityListener; import android.companion.virtual.VirtualDeviceParams; import android.companion.virtual.audio.IAudioConfigChangedCallback; import android.companion.virtual.audio.IAudioRoutingCallback; +import android.companion.virtual.sensor.VirtualSensorConfig; +import android.companion.virtual.sensor.VirtualSensorEvent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -613,6 +615,21 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } } + @Override // Binder call + public void createVirtualSensor( + @NonNull IBinder deviceToken, + @NonNull VirtualSensorConfig config) { + } + + @Override // Binder call + public void unregisterSensor(IBinder token) { + } + + @Override // Binder call + public boolean sendSensorEvent(IBinder token, VirtualSensorEvent event) { + return true; + } + @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { fout.println(" VirtualDevice: "); diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java index 036b6df92ef99..a226ebcdc8f22 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java @@ -16,9 +16,14 @@ package com.android.server.companion.virtual; +import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; +import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS; +import static android.hardware.Sensor.TYPE_ACCELEROMETER; + import static com.google.common.truth.Truth.assertThat; import android.companion.virtual.VirtualDeviceParams; +import android.companion.virtual.sensor.VirtualSensorConfig; import android.os.Parcel; import android.os.UserHandle; @@ -27,18 +32,25 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.List; import java.util.Set; @RunWith(AndroidJUnit4.class) public class VirtualDeviceParamsTest { + private static final String SENSOR_NAME = "VirtualSensorName"; + private static final String SENSOR_VENDOR = "VirtualSensorVendor"; + @Test public void parcelable_shouldRecreateSuccessfully() { VirtualDeviceParams originalParams = new VirtualDeviceParams.Builder() .setLockState(VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED) .setUsersWithMatchingAccounts(Set.of(UserHandle.of(123), UserHandle.of(456))) - .addDevicePolicy(VirtualDeviceParams.POLICY_TYPE_SENSORS, - VirtualDeviceParams.DEVICE_POLICY_CUSTOM) + .addDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM) + .addVirtualSensorConfig( + new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME) + .setVendor(SENSOR_VENDOR) + .build()) .build(); Parcel parcel = Parcel.obtain(); originalParams.writeToParcel(parcel, 0); @@ -49,7 +61,14 @@ public class VirtualDeviceParamsTest { assertThat(params.getLockState()).isEqualTo(VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED); assertThat(params.getUsersWithMatchingAccounts()) .containsExactly(UserHandle.of(123), UserHandle.of(456)); - assertThat(params.getDevicePolicy(VirtualDeviceParams.POLICY_TYPE_SENSORS)) - .isEqualTo(VirtualDeviceParams.DEVICE_POLICY_CUSTOM); + assertThat(params.getDevicePolicy(POLICY_TYPE_SENSORS)).isEqualTo(DEVICE_POLICY_CUSTOM); + + List sensorConfigs = params.getVirtualSensorConfigs(); + assertThat(sensorConfigs).hasSize(1); + VirtualSensorConfig sensorConfig = sensorConfigs.get(0); + assertThat(sensorConfig.getType()).isEqualTo(TYPE_ACCELEROMETER); + assertThat(sensorConfig.getName()).isEqualTo(SENSOR_NAME); + assertThat(sensorConfig.getVendor()).isEqualTo(SENSOR_VENDOR); + assertThat(sensorConfig.getStateChangeCallback()).isNull(); } }