From ca9308224a37a6dd38690d436f4342044e3ce81a Mon Sep 17 00:00:00 2001 From: Vladimir Komsiyski Date: Fri, 21 Oct 2022 16:11:51 +0200 Subject: [PATCH 1/3] Virtual sensor API. Add VirtualSensor, VirtualSensorEvent and a callback to notify the sensor owner for any changes in the registered sensor listeners. The virtual device sensors will registered with the sensor framework as runtime sensors. They must remain valid for the entire lifetime of the virtual device, so they are created when it is created and only unregistered when it is closed. The virtual device owners can access them from the device itself after creation. This CL is only adding the API, the sensor registration and lifetime management is added in a follow-up CL. Bug: 237278244 Test: atest VirtualSensorConfigTest Test: atest VirtualSensorEventTest Test: atest VirtualDeviceParamsTest Change-Id: I8bb458dacba1bb65b00e288734ce41186c4fe624 --- core/api/system-current.txt | 47 ++++ .../companion/virtual/IVirtualDevice.aidl | 21 ++ .../virtual/VirtualDeviceManager.java | 50 +++++ .../virtual/VirtualDeviceParams.java | 103 ++++++--- .../IVirtualSensorStateChangeCallback.aidl | 35 +++ .../virtual/sensor/VirtualSensor.java | 97 ++++++++ .../virtual/sensor/VirtualSensorConfig.aidl | 19 ++ .../virtual/sensor/VirtualSensorConfig.java | 210 ++++++++++++++++++ .../virtual/sensor/VirtualSensorEvent.aidl | 19 ++ .../virtual/sensor/VirtualSensorEvent.java | 140 ++++++++++++ .../sensor/VirtualSensorConfigTest.java | 104 +++++++++ .../sensor/VirtualSensorEventTest.java | 80 +++++++ .../companion/virtual/VirtualDeviceImpl.java | 17 ++ .../virtual/VirtualDeviceParamsTest.java | 27 ++- 14 files changed, 938 insertions(+), 31 deletions(-) create mode 100644 core/java/android/companion/virtual/sensor/IVirtualSensorStateChangeCallback.aidl create mode 100644 core/java/android/companion/virtual/sensor/VirtualSensor.java create mode 100644 core/java/android/companion/virtual/sensor/VirtualSensorConfig.aidl create mode 100644 core/java/android/companion/virtual/sensor/VirtualSensorConfig.java create mode 100644 core/java/android/companion/virtual/sensor/VirtualSensorEvent.aidl create mode 100644 core/java/android/companion/virtual/sensor/VirtualSensorEvent.java create mode 100644 core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java create mode 100644 core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java 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(); } } From 30ea428016d00f2fcd41e7cb4150af2100abd5ad Mon Sep 17 00:00:00 2001 From: Vladimir Komsiyski Date: Sat, 22 Oct 2022 16:13:16 +0200 Subject: [PATCH 2/3] Virtual sensors lifecycle management. Add a SensorController between virtual devices and the JNI layer of the sensor service. The virtual sensors are registered with the sensor framework as runtime sensors when the virtual device is created, and unregistered when the device is closed. Bug: 237278244 Test: atest SensorControllerTest Test: atest VirtualDeviceManagerServiceTest Change-Id: I474b64381ccaf6fd6b0055f7344ebe6d8d5d6e3f --- .../companion/virtual/SensorController.java | 235 ++++++++++++++++++ .../companion/virtual/VirtualDeviceImpl.java | 105 +++++--- .../server/sensors/SensorManagerInternal.java | 50 ++++ .../android/server/sensors/SensorService.java | 42 ++++ ...om_android_server_sensor_SensorService.cpp | 187 +++++++++++++- services/proguard.flags | 1 + .../virtual/SensorControllerTest.java | 140 +++++++++++ .../VirtualDeviceManagerServiceTest.java | 49 +++- 8 files changed, 756 insertions(+), 53 deletions(-) create mode 100644 services/companion/java/com/android/server/companion/virtual/SensorController.java create mode 100644 services/tests/servicestests/src/com/android/server/companion/virtual/SensorControllerTest.java diff --git a/services/companion/java/com/android/server/companion/virtual/SensorController.java b/services/companion/java/com/android/server/companion/virtual/SensorController.java new file mode 100644 index 0000000000000..ec7e993ec30ed --- /dev/null +++ b/services/companion/java/com/android/server/companion/virtual/SensorController.java @@ -0,0 +1,235 @@ +/* + * 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 com.android.server.companion.virtual; + +import android.annotation.NonNull; +import android.companion.virtual.sensor.IVirtualSensorStateChangeCallback; +import android.companion.virtual.sensor.VirtualSensorConfig; +import android.companion.virtual.sensor.VirtualSensorEvent; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.LocalServices; +import com.android.server.sensors.SensorManagerInternal; + +import java.io.PrintWriter; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; + +/** Controls virtual sensors, including their lifecycle and sensor event dispatch. */ +public class SensorController { + + private static final String TAG = "SensorController"; + + private final Object mLock; + private final int mVirtualDeviceId; + @GuardedBy("mLock") + private final Map mSensorDescriptors = new ArrayMap<>(); + + private final SensorManagerInternal mSensorManagerInternal; + + public SensorController(@NonNull Object lock, int virtualDeviceId) { + mLock = lock; + mVirtualDeviceId = virtualDeviceId; + mSensorManagerInternal = LocalServices.getService(SensorManagerInternal.class); + } + + void close() { + synchronized (mLock) { + final Iterator> iterator = + mSensorDescriptors.entrySet().iterator(); + if (iterator.hasNext()) { + final Map.Entry entry = iterator.next(); + final IBinder token = entry.getKey(); + final SensorDescriptor sensorDescriptor = entry.getValue(); + iterator.remove(); + closeSensorDescriptorLocked(token, sensorDescriptor); + } + } + } + + void createSensor(@NonNull IBinder deviceToken, @NonNull VirtualSensorConfig config) { + Objects.requireNonNull(deviceToken); + Objects.requireNonNull(config); + try { + createSensorInternal(deviceToken, config); + } catch (SensorCreationException e) { + throw new RuntimeException( + "Failed to create virtual sensor '" + config.getName() + "'.", e); + } + } + + private void createSensorInternal(IBinder deviceToken, VirtualSensorConfig config) + throws SensorCreationException { + final SensorManagerInternal.RuntimeSensorStateChangeCallback runtimeSensorCallback = + (enabled, samplingPeriodMicros, batchReportLatencyMicros) -> { + IVirtualSensorStateChangeCallback callback = config.getStateChangeCallback(); + if (callback != null) { + try { + callback.onStateChanged( + enabled, samplingPeriodMicros, batchReportLatencyMicros); + } catch (RemoteException e) { + throw new RuntimeException("Failed to call sensor callback.", e); + } + } + }; + + final int handle = mSensorManagerInternal.createRuntimeSensor(mVirtualDeviceId, + config.getType(), config.getName(), + config.getVendor() == null ? "" : config.getVendor(), + runtimeSensorCallback); + if (handle <= 0) { + throw new SensorCreationException("Received an invalid virtual sensor handle."); + } + + // The handle is valid from here, so ensure that all failures clean it up. + final BinderDeathRecipient binderDeathRecipient; + try { + binderDeathRecipient = new BinderDeathRecipient(deviceToken); + deviceToken.linkToDeath(binderDeathRecipient, /* flags= */ 0); + } catch (RemoteException e) { + mSensorManagerInternal.removeRuntimeSensor(handle); + throw new SensorCreationException("Client died before sensor could be created.", e); + } + + synchronized (mLock) { + SensorDescriptor sensorDescriptor = new SensorDescriptor( + handle, config.getType(), config.getName(), binderDeathRecipient); + mSensorDescriptors.put(deviceToken, sensorDescriptor); + } + } + + boolean sendSensorEvent(@NonNull IBinder token, @NonNull VirtualSensorEvent event) { + Objects.requireNonNull(token); + Objects.requireNonNull(event); + synchronized (mLock) { + final SensorDescriptor sensorDescriptor = mSensorDescriptors.get(token); + if (sensorDescriptor == null) { + throw new IllegalArgumentException("Could not send sensor event for given token"); + } + return mSensorManagerInternal.sendSensorEvent( + sensorDescriptor.getHandle(), sensorDescriptor.getType(), + event.getTimestampNanos(), event.getValues()); + } + } + + void unregisterSensor(@NonNull IBinder token) { + Objects.requireNonNull(token); + synchronized (mLock) { + final SensorDescriptor sensorDescriptor = mSensorDescriptors.remove(token); + if (sensorDescriptor == null) { + throw new IllegalArgumentException("Could not unregister sensor for given token"); + } + closeSensorDescriptorLocked(token, sensorDescriptor); + } + } + + @GuardedBy("mLock") + private void closeSensorDescriptorLocked(IBinder token, SensorDescriptor sensorDescriptor) { + token.unlinkToDeath(sensorDescriptor.getDeathRecipient(), /* flags= */ 0); + final int handle = sensorDescriptor.getHandle(); + mSensorManagerInternal.removeRuntimeSensor(handle); + } + + + void dump(@NonNull PrintWriter fout) { + fout.println(" SensorController: "); + synchronized (mLock) { + fout.println(" Active descriptors: "); + for (SensorDescriptor sensorDescriptor : mSensorDescriptors.values()) { + fout.println(" handle: " + sensorDescriptor.getHandle()); + fout.println(" type: " + sensorDescriptor.getType()); + fout.println(" name: " + sensorDescriptor.getName()); + } + } + } + + @VisibleForTesting + void addSensorForTesting(IBinder deviceToken, int handle, int type, String name) { + synchronized (mLock) { + mSensorDescriptors.put(deviceToken, + new SensorDescriptor(handle, type, name, () -> {})); + } + } + + @VisibleForTesting + Map getSensorDescriptors() { + synchronized (mLock) { + return mSensorDescriptors; + } + } + + @VisibleForTesting + static final class SensorDescriptor { + + private final int mHandle; + private final IBinder.DeathRecipient mDeathRecipient; + private final int mType; + private final String mName; + + SensorDescriptor(int handle, int type, String name, IBinder.DeathRecipient deathRecipient) { + mHandle = handle; + mDeathRecipient = deathRecipient; + mType = type; + mName = name; + } + public int getHandle() { + return mHandle; + } + public int getType() { + return mType; + } + public String getName() { + return mName; + } + public IBinder.DeathRecipient getDeathRecipient() { + return mDeathRecipient; + } + } + + private final class BinderDeathRecipient implements IBinder.DeathRecipient { + private final IBinder mDeviceToken; + + BinderDeathRecipient(IBinder deviceToken) { + mDeviceToken = deviceToken; + } + + @Override + public void binderDied() { + // All callers are expected to call {@link VirtualDevice#unregisterSensor} before + // quitting, which removes this death recipient. If this is invoked, the remote end + // died, or they disposed of the object without properly unregistering. + Slog.e(TAG, "Virtual sensor controller binder died"); + unregisterSensor(mDeviceToken); + } + } + + /** An internal exception that is thrown to indicate an error when opening a virtual sensor. */ + private static class SensorCreationException extends Exception { + SensorCreationException(String message) { + super(message); + } + SensorCreationException(String message, Exception cause) { + super(message, cause); + } + } +} 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 019f582d4790e..798ae0ea32308 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -77,6 +77,7 @@ import com.android.server.companion.virtual.audio.VirtualAudioController; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.Consumer; @@ -99,6 +100,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private final int mOwnerUid; private final int mDeviceId; private final InputController mInputController; + private final SensorController mSensorController; private VirtualAudioController mVirtualAudioController; @VisibleForTesting final Set mVirtualDisplayIds = new ArraySet<>(); @@ -161,6 +163,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub ownerUid, deviceId, /* inputController= */ null, + /* sensorController= */ null, listener, pendingTrampolineCallback, activityListener, @@ -176,6 +179,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub int ownerUid, int deviceId, InputController inputController, + SensorController sensorController, OnDeviceCloseListener listener, PendingTrampolineCallback pendingTrampolineCallback, IVirtualDeviceActivityListener activityListener, @@ -199,6 +203,11 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } else { mInputController = inputController; } + if (sensorController == null) { + mSensorController = new SensorController(mVirtualDeviceLock, mDeviceId); + } else { + mSensorController = sensorController; + } mListener = listener; try { token.linkToDeath(this, 0); @@ -320,11 +329,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub mListener.onClose(mAssociationInfo.getId()); mAppToken.unlinkToDeath(this, 0); - final long token = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { mInputController.close(); + mSensorController.close(); } finally { - Binder.restoreCallingIdentity(token); + Binder.restoreCallingIdentity(ident); } } @@ -404,12 +414,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub + "this virtual device"); } } - final long token = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { mInputController.createDpad(deviceName, vendorId, productId, deviceToken, displayId); } finally { - Binder.restoreCallingIdentity(token); + Binder.restoreCallingIdentity(ident); } } @@ -431,12 +441,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub + "this virtual device"); } } - final long token = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { mInputController.createKeyboard(deviceName, vendorId, productId, deviceToken, displayId); } finally { - Binder.restoreCallingIdentity(token); + Binder.restoreCallingIdentity(ident); } } @@ -458,11 +468,11 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub + "virtual device"); } } - final long token = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { mInputController.createMouse(deviceName, vendorId, productId, deviceToken, displayId); } finally { - Binder.restoreCallingIdentity(token); + Binder.restoreCallingIdentity(ident); } } @@ -492,12 +502,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub + screenSize); } - final long token = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { mInputController.createTouchscreen(deviceName, vendorId, productId, deviceToken, displayId, screenSize); } finally { - Binder.restoreCallingIdentity(token); + Binder.restoreCallingIdentity(ident); } } @@ -507,92 +517,92 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub android.Manifest.permission.CREATE_VIRTUAL_DEVICE, "Permission required to unregister this input device"); - final long binderToken = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { mInputController.unregisterInputDevice(token); } finally { - Binder.restoreCallingIdentity(binderToken); + Binder.restoreCallingIdentity(ident); } } @Override // Binder call public int getInputDeviceId(IBinder token) { - final long binderToken = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { return mInputController.getInputDeviceId(token); } finally { - Binder.restoreCallingIdentity(binderToken); + Binder.restoreCallingIdentity(ident); } } @Override // Binder call public boolean sendDpadKeyEvent(IBinder token, VirtualKeyEvent event) { - final long binderToken = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { return mInputController.sendDpadKeyEvent(token, event); } finally { - Binder.restoreCallingIdentity(binderToken); + Binder.restoreCallingIdentity(ident); } } @Override // Binder call public boolean sendKeyEvent(IBinder token, VirtualKeyEvent event) { - final long binderToken = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { return mInputController.sendKeyEvent(token, event); } finally { - Binder.restoreCallingIdentity(binderToken); + Binder.restoreCallingIdentity(ident); } } @Override // Binder call public boolean sendButtonEvent(IBinder token, VirtualMouseButtonEvent event) { - final long binderToken = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { return mInputController.sendButtonEvent(token, event); } finally { - Binder.restoreCallingIdentity(binderToken); + Binder.restoreCallingIdentity(ident); } } @Override // Binder call public boolean sendTouchEvent(IBinder token, VirtualTouchEvent event) { - final long binderToken = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { return mInputController.sendTouchEvent(token, event); } finally { - Binder.restoreCallingIdentity(binderToken); + Binder.restoreCallingIdentity(ident); } } @Override // Binder call public boolean sendRelativeEvent(IBinder token, VirtualMouseRelativeEvent event) { - final long binderToken = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { return mInputController.sendRelativeEvent(token, event); } finally { - Binder.restoreCallingIdentity(binderToken); + Binder.restoreCallingIdentity(ident); } } @Override // Binder call public boolean sendScrollEvent(IBinder token, VirtualMouseScrollEvent event) { - final long binderToken = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { return mInputController.sendScrollEvent(token, event); } finally { - Binder.restoreCallingIdentity(binderToken); + Binder.restoreCallingIdentity(ident); } } @Override // Binder call public PointF getCursorPosition(IBinder token) { - final long binderToken = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { return mInputController.getCursorPosition(token); } finally { - Binder.restoreCallingIdentity(binderToken); + Binder.restoreCallingIdentity(ident); } } @@ -602,7 +612,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub android.Manifest.permission.CREATE_VIRTUAL_DEVICE, "Permission required to unregister this input device"); - final long binderToken = Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); try { synchronized (mVirtualDeviceLock) { mDefaultShowPointerIcon = showPointerIcon; @@ -611,7 +621,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } } } finally { - Binder.restoreCallingIdentity(binderToken); + Binder.restoreCallingIdentity(ident); } } @@ -619,15 +629,43 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub public void createVirtualSensor( @NonNull IBinder deviceToken, @NonNull VirtualSensorConfig config) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CREATE_VIRTUAL_DEVICE, + "Permission required to create a virtual sensor"); + Objects.requireNonNull(config); + Objects.requireNonNull(deviceToken); + final long ident = Binder.clearCallingIdentity(); + try { + mSensorController.createSensor(deviceToken, config); + } finally { + Binder.restoreCallingIdentity(ident); + } } @Override // Binder call - public void unregisterSensor(IBinder token) { + public void unregisterSensor(@NonNull IBinder token) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CREATE_VIRTUAL_DEVICE, + "Permission required to unregister a virtual sensor"); + final long ident = Binder.clearCallingIdentity(); + try { + mSensorController.unregisterSensor(token); + } finally { + Binder.restoreCallingIdentity(ident); + } } @Override // Binder call - public boolean sendSensorEvent(IBinder token, VirtualSensorEvent event) { - return true; + public boolean sendSensorEvent(@NonNull IBinder token, @NonNull VirtualSensorEvent event) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CREATE_VIRTUAL_DEVICE, + "Permission required to send a virtual sensor event"); + final long ident = Binder.clearCallingIdentity(); + try { + return mSensorController.sendSensorEvent(token, event); + } finally { + Binder.restoreCallingIdentity(ident); + } } @Override @@ -643,6 +681,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub fout.println(" mDefaultShowPointerIcon: " + mDefaultShowPointerIcon); } mInputController.dump(fout); + mSensorController.dump(fout); } GenericWindowPolicyController createWindowPolicyController() { diff --git a/services/core/java/com/android/server/sensors/SensorManagerInternal.java b/services/core/java/com/android/server/sensors/SensorManagerInternal.java index fbb6644934f1f..f17e5e73ceb01 100644 --- a/services/core/java/com/android/server/sensors/SensorManagerInternal.java +++ b/services/core/java/com/android/server/sensors/SensorManagerInternal.java @@ -42,6 +42,43 @@ public abstract class SensorManagerInternal { */ public abstract void removeProximityActiveListener(@NonNull ProximityActiveListener listener); + /** + * Creates a sensor that is registered at runtime by the system with the sensor service. + * + * The runtime sensors created here are different from the + * + * dynamic sensor support in the HAL. These sensors have no HAL dependency and correspond to + * sensors that belong to an external (virtual) device. + * + * @param deviceId The identifier of the device this sensor is associated with. + * @param type The generic type of the sensor. + * @param name The name of the sensor. + * @param vendor The vendor string of the sensor. + * @param callback The callback to get notified when the sensor listeners have changed. + * @return The sensor handle. + */ + public abstract int createRuntimeSensor(int deviceId, int type, @NonNull String name, + @NonNull String vendor, @NonNull RuntimeSensorStateChangeCallback callback); + + /** + * Unregisters the sensor with the given handle from the framework. + */ + public abstract void removeRuntimeSensor(int handle); + + /** + * Sends an event for the runtime sensor with the given handle to the framework. + * + * Only relevant for sending runtime sensor events. @see #createRuntimeSensor. + * + * @param handle The sensor handle. + * @param type The type of the sensor. + * @param timestampNanos When the event occurred. + * @param values The values of the event. + * @return Whether the event injection was successful. + */ + public abstract boolean sendSensorEvent(int handle, int type, long timestampNanos, + @NonNull float[] values); + /** * Listener for proximity sensor state changes. */ @@ -52,4 +89,17 @@ public abstract class SensorManagerInternal { */ void onProximityActive(boolean isActive); } + + /** + * Callback for runtime sensor state changes. Only relevant to sensors created via + * {@link #createRuntimeSensor}, i.e. the dynamic sensors created via the dynamic sensor HAL are + * not covered. + */ + public interface RuntimeSensorStateChangeCallback { + /** + * Invoked when the listeners of the runtime sensor have changed. + */ + void onStateChanged(boolean enabled, int samplingPeriodMicros, + int batchReportLatencyMicros); + } } diff --git a/services/core/java/com/android/server/sensors/SensorService.java b/services/core/java/com/android/server/sensors/SensorService.java index 8fe2d52f71602..d8e3bddd64320 100644 --- a/services/core/java/com/android/server/sensors/SensorService.java +++ b/services/core/java/com/android/server/sensors/SensorService.java @@ -29,7 +29,9 @@ import com.android.server.SystemServerInitThreadPool; import com.android.server.SystemService; import com.android.server.utils.TimingsTraceAndSlog; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.Future; @@ -40,6 +42,8 @@ public class SensorService extends SystemService { private final ArrayMap mProximityListeners = new ArrayMap<>(); @GuardedBy("mLock") + private final Set mRuntimeSensorHandles = new HashSet<>(); + @GuardedBy("mLock") private Future mSensorServiceStart; @GuardedBy("mLock") private long mPtr; @@ -51,6 +55,12 @@ public class SensorService extends SystemService { private static native void registerProximityActiveListenerNative(long ptr); private static native void unregisterProximityActiveListenerNative(long ptr); + private static native int registerRuntimeSensorNative(long ptr, int deviceId, int type, + String name, String vendor, + SensorManagerInternal.RuntimeSensorStateChangeCallback callback); + private static native void unregisterRuntimeSensorNative(long ptr, int handle); + private static native boolean sendRuntimeSensorEventNative(long ptr, int handle, int type, + long timestampNanos, float[] values); public SensorService(Context ctx) { super(ctx); @@ -84,6 +94,38 @@ public class SensorService extends SystemService { } class LocalService extends SensorManagerInternal { + @Override + public int createRuntimeSensor(int deviceId, int type, @NonNull String name, + @NonNull String vendor, @NonNull RuntimeSensorStateChangeCallback callback) { + synchronized (mLock) { + int handle = registerRuntimeSensorNative(mPtr, deviceId, type, name, vendor, + callback); + mRuntimeSensorHandles.add(handle); + return handle; + } + } + + @Override + public void removeRuntimeSensor(int handle) { + synchronized (mLock) { + if (mRuntimeSensorHandles.contains(handle)) { + mRuntimeSensorHandles.remove(handle); + unregisterRuntimeSensorNative(mPtr, handle); + } + } + } + + @Override + public boolean sendSensorEvent(int handle, int type, long timestampNanos, + @NonNull float[] values) { + synchronized (mLock) { + if (!mRuntimeSensorHandles.contains(handle)) { + return false; + } + return sendRuntimeSensorEventNative(mPtr, handle, type, timestampNanos, values); + } + } + @Override public void addProximityActiveListener(@NonNull Executor executor, @NonNull ProximityActiveListener listener) { diff --git a/services/core/jni/com_android_server_sensor_SensorService.cpp b/services/core/jni/com_android_server_sensor_SensorService.cpp index 63b7dfbc2a3b3..10d8b42c2979c 100644 --- a/services/core/jni/com_android_server_sensor_SensorService.cpp +++ b/services/core/jni/com_android_server_sensor_SensorService.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -30,10 +31,14 @@ #define PROXIMITY_ACTIVE_CLASS \ "com/android/server/sensors/SensorManagerInternal$ProximityActiveListener" +#define RUNTIME_SENSOR_CALLBACK_CLASS \ + "com/android/server/sensors/SensorManagerInternal$RuntimeSensorStateChangeCallback" + namespace android { static JavaVM* sJvm = nullptr; static jmethodID sMethodIdOnProximityActive; +static jmethodID sMethodIdOnStateChanged; class NativeSensorService { public: @@ -41,6 +46,11 @@ public: void registerProximityActiveListener(); void unregisterProximityActiveListener(); + jint registerRuntimeSensor(JNIEnv* env, jint deviceId, jint type, jstring name, jstring vendor, + jobject callback); + void unregisterRuntimeSensor(jint handle); + jboolean sendRuntimeSensorEvent(JNIEnv* env, jint handle, jint type, jlong timestamp, + jfloatArray values); private: sp mService; @@ -56,6 +66,18 @@ private: jobject mListener; }; sp mProximityActiveListenerDelegate; + + class RuntimeSensorCallbackDelegate : public SensorService::RuntimeSensorStateChangeCallback { + public: + RuntimeSensorCallbackDelegate(JNIEnv* env, jobject callback); + ~RuntimeSensorCallbackDelegate(); + + void onStateChanged(bool enabled, int64_t samplingPeriodNs, + int64_t batchReportLatencyNs) override; + + private: + jobject mCallback; + }; }; NativeSensorService::NativeSensorService(JNIEnv* env, jobject listener) @@ -85,6 +107,109 @@ void NativeSensorService::unregisterProximityActiveListener() { mService->removeProximityActiveListener(mProximityActiveListenerDelegate); } +jint NativeSensorService::registerRuntimeSensor(JNIEnv* env, jint deviceId, jint type, jstring name, + jstring vendor, jobject callback) { + if (mService == nullptr) { + ALOGD("Dropping registerRuntimeSensor, sensor service not available."); + return -1; + } + + sensor_t sensor{ + .name = env->GetStringUTFChars(name, 0), + .vendor = env->GetStringUTFChars(vendor, 0), + .version = sizeof(sensor_t), + .type = type, + }; + + sp callbackDelegate( + new RuntimeSensorCallbackDelegate(env, callback)); + return mService->registerRuntimeSensor(sensor, deviceId, callbackDelegate); +} + +void NativeSensorService::unregisterRuntimeSensor(jint handle) { + if (mService == nullptr) { + ALOGD("Dropping unregisterProximityActiveListener, sensor service not available."); + return; + } + + mService->unregisterRuntimeSensor(handle); +} + +jboolean NativeSensorService::sendRuntimeSensorEvent(JNIEnv* env, jint handle, jint type, + jlong timestamp, jfloatArray values) { + if (mService == nullptr) { + ALOGD("Dropping sendRuntimeSensorEvent, sensor service not available."); + return false; + } + if (values == nullptr) { + ALOGD("Dropping sendRuntimeSensorEvent, no values."); + return false; + } + + sensors_event_t event{ + .version = sizeof(sensors_event_t), + .timestamp = timestamp, + .sensor = handle, + .type = type, + }; + + int valuesLength = env->GetArrayLength(values); + jfloat* sensorValues = env->GetFloatArrayElements(values, nullptr); + + switch (type) { + case SENSOR_TYPE_ACCELEROMETER: + case SENSOR_TYPE_MAGNETIC_FIELD: + case SENSOR_TYPE_ORIENTATION: + case SENSOR_TYPE_GYROSCOPE: + case SENSOR_TYPE_GRAVITY: + case SENSOR_TYPE_LINEAR_ACCELERATION: { + if (valuesLength != 3) { + ALOGD("Dropping sendRuntimeSensorEvent, wrong number of values."); + return false; + } + event.acceleration.x = sensorValues[0]; + event.acceleration.y = sensorValues[1]; + event.acceleration.z = sensorValues[2]; + break; + } + case SENSOR_TYPE_DEVICE_ORIENTATION: + case SENSOR_TYPE_LIGHT: + case SENSOR_TYPE_PRESSURE: + case SENSOR_TYPE_TEMPERATURE: + case SENSOR_TYPE_PROXIMITY: + case SENSOR_TYPE_RELATIVE_HUMIDITY: + case SENSOR_TYPE_AMBIENT_TEMPERATURE: + case SENSOR_TYPE_SIGNIFICANT_MOTION: + case SENSOR_TYPE_STEP_DETECTOR: + case SENSOR_TYPE_TILT_DETECTOR: + case SENSOR_TYPE_WAKE_GESTURE: + case SENSOR_TYPE_GLANCE_GESTURE: + case SENSOR_TYPE_PICK_UP_GESTURE: + case SENSOR_TYPE_WRIST_TILT_GESTURE: + case SENSOR_TYPE_STATIONARY_DETECT: + case SENSOR_TYPE_MOTION_DETECT: + case SENSOR_TYPE_HEART_BEAT: + case SENSOR_TYPE_LOW_LATENCY_OFFBODY_DETECT: { + if (valuesLength != 1) { + ALOGD("Dropping sendRuntimeSensorEvent, wrong number of values."); + return false; + } + event.data[0] = sensorValues[0]; + break; + } + default: { + if (valuesLength > 16) { + ALOGD("Dropping sendRuntimeSensorEvent, number of values exceeds the maximum."); + return false; + } + memcpy(event.data, sensorValues, valuesLength * sizeof(float)); + } + } + + status_t err = mService->sendRuntimeSensorEvent(event); + return err == OK; +} + NativeSensorService::ProximityActiveListenerDelegate::ProximityActiveListenerDelegate( JNIEnv* env, jobject listener) : mListener(env->NewGlobalRef(listener)) {} @@ -98,6 +223,22 @@ void NativeSensorService::ProximityActiveListenerDelegate::onProximityActive(boo jniEnv->CallVoidMethod(mListener, sMethodIdOnProximityActive, static_cast(isActive)); } +NativeSensorService::RuntimeSensorCallbackDelegate::RuntimeSensorCallbackDelegate(JNIEnv* env, + jobject callback) + : mCallback(env->NewGlobalRef(callback)) {} + +NativeSensorService::RuntimeSensorCallbackDelegate::~RuntimeSensorCallbackDelegate() { + AndroidRuntime::getJNIEnv()->DeleteGlobalRef(mCallback); +} + +void NativeSensorService::RuntimeSensorCallbackDelegate::onStateChanged( + bool enabled, int64_t samplingPeriodNs, int64_t batchReportLatencyNs) { + auto jniEnv = GetOrAttachJNIEnvironment(sJvm); + jniEnv->CallVoidMethod(mCallback, sMethodIdOnStateChanged, static_cast(enabled), + static_cast(ns2us(samplingPeriodNs)), + static_cast(ns2us(batchReportLatencyNs))); +} + static jlong startSensorServiceNative(JNIEnv* env, jclass, jobject listener) { NativeSensorService* service = new NativeSensorService(env, listener); return reinterpret_cast(service); @@ -113,26 +254,46 @@ static void unregisterProximityActiveListenerNative(JNIEnv* env, jclass, jlong p service->unregisterProximityActiveListener(); } -static const JNINativeMethod methods[] = { - { - "startSensorServiceNative", "(L" PROXIMITY_ACTIVE_CLASS ";)J", - reinterpret_cast(startSensorServiceNative) - }, - { - "registerProximityActiveListenerNative", "(J)V", - reinterpret_cast(registerProximityActiveListenerNative) - }, - { - "unregisterProximityActiveListenerNative", "(J)V", - reinterpret_cast(unregisterProximityActiveListenerNative) - }, +static jint registerRuntimeSensorNative(JNIEnv* env, jclass, jlong ptr, jint deviceId, jint type, + jstring name, jstring vendor, jobject callback) { + auto* service = reinterpret_cast(ptr); + return service->registerRuntimeSensor(env, deviceId, type, name, vendor, callback); +} +static void unregisterRuntimeSensorNative(JNIEnv* env, jclass, jlong ptr, jint handle) { + auto* service = reinterpret_cast(ptr); + service->unregisterRuntimeSensor(handle); +} + +static jboolean sendRuntimeSensorEventNative(JNIEnv* env, jclass, jlong ptr, jint handle, jint type, + jlong timestamp, jfloatArray values) { + auto* service = reinterpret_cast(ptr); + return service->sendRuntimeSensorEvent(env, handle, type, timestamp, values); +} + +static const JNINativeMethod methods[] = { + {"startSensorServiceNative", "(L" PROXIMITY_ACTIVE_CLASS ";)J", + reinterpret_cast(startSensorServiceNative)}, + {"registerProximityActiveListenerNative", "(J)V", + reinterpret_cast(registerProximityActiveListenerNative)}, + {"unregisterProximityActiveListenerNative", "(J)V", + reinterpret_cast(unregisterProximityActiveListenerNative)}, + {"registerRuntimeSensorNative", + "(JIILjava/lang/String;Ljava/lang/String;L" RUNTIME_SENSOR_CALLBACK_CLASS ";)I", + reinterpret_cast(registerRuntimeSensorNative)}, + {"unregisterRuntimeSensorNative", "(JI)V", + reinterpret_cast(unregisterRuntimeSensorNative)}, + {"sendRuntimeSensorEventNative", "(JIIJ[F)Z", + reinterpret_cast(sendRuntimeSensorEventNative)}, }; int register_android_server_sensor_SensorService(JavaVM* vm, JNIEnv* env) { sJvm = vm; jclass listenerClass = FindClassOrDie(env, PROXIMITY_ACTIVE_CLASS); sMethodIdOnProximityActive = GetMethodIDOrDie(env, listenerClass, "onProximityActive", "(Z)V"); + jclass runtimeSensorCallbackClass = FindClassOrDie(env, RUNTIME_SENSOR_CALLBACK_CLASS); + sMethodIdOnStateChanged = + GetMethodIDOrDie(env, runtimeSensorCallbackClass, "onStateChanged", "(ZII)V"); return jniRegisterNativeMethods(env, "com/android/server/sensors/SensorService", methods, NELEM(methods)); } diff --git a/services/proguard.flags b/services/proguard.flags index 27fe5056b8b4c..6cdf11c3c6855 100644 --- a/services/proguard.flags +++ b/services/proguard.flags @@ -88,6 +88,7 @@ -keep,allowoptimization,allowaccessmodification class com.android.server.location.gnss.GnssPowerStats { *; } -keep,allowoptimization,allowaccessmodification class com.android.server.location.gnss.hal.GnssNative { *; } -keep,allowoptimization,allowaccessmodification class com.android.server.pm.PackageManagerShellCommandDataLoader { *; } +-keep,allowoptimization,allowaccessmodification class com.android.server.sensors.SensorManagerInternal$RuntimeSensorStateChangeCallback { *; } -keep,allowoptimization,allowaccessmodification class com.android.server.sensors.SensorManagerInternal$ProximityActiveListener { *; } -keep,allowoptimization,allowaccessmodification class com.android.server.sensors.SensorService { *; } -keep,allowoptimization,allowaccessmodification class com.android.server.soundtrigger_middleware.SoundTriggerMiddlewareImpl$AudioSessionProvider$AudioSession { *; } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/SensorControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/SensorControllerTest.java new file mode 100644 index 0000000000000..ef8a49f95a496 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/SensorControllerTest.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 com.android.server.companion.virtual; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import android.companion.virtual.sensor.VirtualSensorConfig; +import android.companion.virtual.sensor.VirtualSensorEvent; +import android.hardware.Sensor; +import android.os.Binder; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import com.android.server.LocalServices; +import com.android.server.sensors.SensorManagerInternal; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@Presubmit +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class SensorControllerTest { + + private static final int VIRTUAL_DEVICE_ID = 42; + private static final String VIRTUAL_SENSOR_NAME = "VirtualAccelerometer"; + private static final int SENSOR_HANDLE = 7; + + @Mock + private SensorManagerInternal mSensorManagerInternalMock; + private SensorController mSensorController; + private VirtualSensorEvent mSensorEvent; + private VirtualSensorConfig mVirtualSensorConfig; + private IBinder mSensorToken; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + LocalServices.removeServiceForTest(SensorManagerInternal.class); + LocalServices.addService(SensorManagerInternal.class, mSensorManagerInternalMock); + + mSensorController = new SensorController(new Object(), VIRTUAL_DEVICE_ID); + mSensorEvent = new VirtualSensorEvent.Builder(new float[] { 1f, 2f, 3f}).build(); + mVirtualSensorConfig = + new VirtualSensorConfig.Builder(Sensor.TYPE_ACCELEROMETER, VIRTUAL_SENSOR_NAME) + .build(); + mSensorToken = new Binder("sensorToken"); + } + + @Test + public void createSensor_invalidHandle_throwsException() { + doReturn(/* handle= */0).when(mSensorManagerInternalMock).createRuntimeSensor( + anyInt(), anyInt(), anyString(), anyString(), any()); + + Throwable thrown = assertThrows( + RuntimeException.class, + () -> mSensorController.createSensor(mSensorToken, mVirtualSensorConfig)); + + assertThat(thrown.getCause().getMessage()) + .contains("Received an invalid virtual sensor handle"); + } + + @Test + public void createSensor_success() { + doCreateSensorSuccessfully(); + + assertThat(mSensorController.getSensorDescriptors()).isNotEmpty(); + } + + @Test + public void sendSensorEvent_invalidToken_throwsException() { + doCreateSensorSuccessfully(); + + assertThrows( + IllegalArgumentException.class, + () -> mSensorController.sendSensorEvent( + new Binder("invalidSensorToken"), mSensorEvent)); + } + + @Test + public void sendSensorEvent_success() { + doCreateSensorSuccessfully(); + + mSensorController.sendSensorEvent(mSensorToken, mSensorEvent); + verify(mSensorManagerInternalMock).sendSensorEvent( + SENSOR_HANDLE, Sensor.TYPE_ACCELEROMETER, mSensorEvent.getTimestampNanos(), + mSensorEvent.getValues()); + } + + @Test + public void unregisterSensor_invalidToken_throwsException() { + doCreateSensorSuccessfully(); + + assertThrows( + IllegalArgumentException.class, + () -> mSensorController.unregisterSensor(new Binder("invalidSensorToken"))); + } + + @Test + public void unregisterSensor_success() { + doCreateSensorSuccessfully(); + + mSensorController.unregisterSensor(mSensorToken); + verify(mSensorManagerInternalMock).removeRuntimeSensor(SENSOR_HANDLE); + assertThat(mSensorController.getSensorDescriptors()).isEmpty(); + } + + private void doCreateSensorSuccessfully() { + doReturn(SENSOR_HANDLE).when(mSensorManagerInternalMock).createRuntimeSensor( + anyInt(), anyInt(), anyString(), anyString(), any()); + mSensorController.createSensor(mSensorToken, mVirtualSensorConfig); + } +} diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 09dc367cceb4a..41700ab1f04e6 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -51,6 +51,7 @@ import android.companion.virtual.VirtualDeviceManager; 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.content.ComponentName; import android.content.Context; import android.content.ContextWrapper; @@ -58,6 +59,7 @@ import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.graphics.Point; +import android.hardware.Sensor; import android.hardware.display.DisplayManagerInternal; import android.hardware.input.IInputManager; import android.hardware.input.VirtualKeyEvent; @@ -88,6 +90,7 @@ import androidx.test.InstrumentationRegistry; import com.android.internal.app.BlockedAppStreamingActivity; import com.android.server.LocalServices; import com.android.server.input.InputManagerInternal; +import com.android.server.sensors.SensorManagerInternal; import org.junit.Before; import org.junit.Test; @@ -125,16 +128,19 @@ public class VirtualDeviceManagerServiceTest { private static final int VENDOR_ID = 5; private static final String UNIQUE_ID = "uniqueid"; private static final String PHYS = "phys"; - private static final int DEVICE_ID = 42; + private static final int DEVICE_ID = 53; private static final int HEIGHT = 1800; private static final int WIDTH = 900; + private static final int SENSOR_HANDLE = 64; private static final Binder BINDER = new Binder("binder"); private static final int FLAG_CANNOT_DISPLAY_ON_REMOTE_DEVICES = 0x00000; + private static final int VIRTUAL_DEVICE_ID = 42; private Context mContext; private InputManagerMockHelper mInputManagerMockHelper; private VirtualDeviceImpl mDeviceImpl; private InputController mInputController; + private SensorController mSensorController; private AssociationInfo mAssociationInfo; private VirtualDeviceManagerService mVdms; private VirtualDeviceManagerInternal mLocalService; @@ -149,6 +155,8 @@ public class VirtualDeviceManagerServiceTest { @Mock private InputManagerInternal mInputManagerInternalMock; @Mock + private SensorManagerInternal mSensorManagerInternalMock; + @Mock private IVirtualDeviceActivityListener mActivityListener; @Mock private Consumer> mRunningAppsChangedCallback; @@ -205,6 +213,9 @@ public class VirtualDeviceManagerServiceTest { LocalServices.removeServiceForTest(InputManagerInternal.class); LocalServices.addService(InputManagerInternal.class, mInputManagerInternalMock); + LocalServices.removeServiceForTest(SensorManagerInternal.class); + LocalServices.addService(SensorManagerInternal.class, mSensorManagerInternalMock); + final DisplayInfo displayInfo = new DisplayInfo(); displayInfo.uniqueId = UNIQUE_ID; doReturn(displayInfo).when(mDisplayManagerInternalMock).getDisplayInfo(anyInt()); @@ -229,6 +240,7 @@ public class VirtualDeviceManagerServiceTest { mInputController = new InputController(new Object(), mNativeWrapperMock, new Handler(TestableLooper.get(this).getLooper()), mContext.getSystemService(WindowManager.class), threadVerifier); + mSensorController = new SensorController(new Object(), VIRTUAL_DEVICE_ID); mAssociationInfo = new AssociationInfo(1, 0, null, MacAddress.BROADCAST_ADDRESS, "", null, null, true, false, false, 0, 0); @@ -241,9 +253,9 @@ public class VirtualDeviceManagerServiceTest { .setBlockedActivities(getBlockedActivities()) .build(); mDeviceImpl = new VirtualDeviceImpl(mContext, - mAssociationInfo, new Binder(), /* ownerUid */ 0, /* uniqueId */ 1, - mInputController, (int associationId) -> {}, mPendingTrampolineCallback, - mActivityListener, mRunningAppsChangedCallback, params); + mAssociationInfo, new Binder(), /* ownerUid */ 0, VIRTUAL_DEVICE_ID, + mInputController, mSensorController, (int associationId) -> {}, + mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback, params); mVdms.addVirtualDevice(mDeviceImpl); } @@ -285,9 +297,9 @@ public class VirtualDeviceManagerServiceTest { .addDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM) .build(); mDeviceImpl = new VirtualDeviceImpl(mContext, - mAssociationInfo, new Binder(), /* ownerUid */ 0, /* uniqueId */ 1, - mInputController, (int associationId) -> {}, mPendingTrampolineCallback, - mActivityListener, mRunningAppsChangedCallback, params); + mAssociationInfo, new Binder(), /* ownerUid */ 0, VIRTUAL_DEVICE_ID, + mInputController, mSensorController, (int associationId) -> {}, + mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback, params); mVdms.addVirtualDevice(mDeviceImpl); assertThat( @@ -551,6 +563,18 @@ public class VirtualDeviceManagerServiceTest { VENDOR_ID, PRODUCT_ID, BINDER, new Point(WIDTH, HEIGHT))); } + @Test + public void createVirtualSensor_noPermission_failsSecurityException() { + doCallRealMethod().when(mContext).enforceCallingOrSelfPermission( + eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); + assertThrows( + SecurityException.class, + () -> mDeviceImpl.createVirtualSensor( + BINDER, + new VirtualSensorConfig.Builder( + Sensor.TYPE_ACCELEROMETER, DEVICE_NAME).build())); + } + @Test public void onAudioSessionStarting_noPermission_failsSecurityException() { mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID); @@ -654,6 +678,17 @@ public class VirtualDeviceManagerServiceTest { assertThat(mDeviceImpl.getVirtualAudioControllerForTesting()).isNull(); } + @Test + public void close_cleanSensorController() { + mSensorController.addSensorForTesting( + BINDER, SENSOR_HANDLE, Sensor.TYPE_ACCELEROMETER, DEVICE_NAME); + + mDeviceImpl.close(); + + assertThat(mSensorController.getSensorDescriptors()).isEmpty(); + verify(mSensorManagerInternalMock).removeRuntimeSensor(SENSOR_HANDLE); + } + @Test public void sendKeyEvent_noFd() { assertThrows( From c0acd6cd17a41c3a4717cd0f927f18de06c91d5e Mon Sep 17 00:00:00 2001 From: Vladimir Komsiyski Date: Sun, 23 Oct 2022 18:34:40 +0200 Subject: [PATCH 3/3] Adjust the behavior of SensorManager for virtual device sensors. The virtual device sensors are handled as runtime sensors internally. They are returned from getSensorsList *iff* the current context in SensorManager is associated with a virtual device, which has a custom sensors policy. The SystemSensorManager adds a new implementation of getSensorList in order to avoid the cache the per-device sensor lists separately. The virtual device sensors do not trigger DynamicSensorCallbacks and are removed from the system when the virtual device is closed. The virtual device broadcasts the fact that it's being closed so the SensorManager can clean up any cached lists associated with that device. Bug: 237278244 Test: atest cts/tests/sensor Change-Id: I63938377f4eedca32fba14d8e15e09be1b85e460 --- .../virtual/VirtualDeviceManager.java | 21 ++++ .../android/hardware/SystemSensorManager.java | 112 +++++++++++++++++- core/jni/android_hardware_SensorManager.cpp | 57 ++++----- .../virtual/VirtualDeviceManagerService.java | 18 ++- 4 files changed, 178 insertions(+), 30 deletions(-) diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index fbb4bd0bdb3f8..08002da41cfc3 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -22,6 +22,7 @@ import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.annotation.SystemService; import android.app.PendingIntent; @@ -91,6 +92,26 @@ public final class VirtualDeviceManager { */ public static final int INVALID_DEVICE_ID = -1; + /** + * Broadcast Action: A Virtual Device was removed. + * + *

This is a protected intent that can only be sent by the system.

+ * + * @hide + */ + @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_VIRTUAL_DEVICE_REMOVED = + "android.companion.virtual.action.VIRTUAL_DEVICE_REMOVED"; + + /** + * Int intent extra to be used with {@link #ACTION_VIRTUAL_DEVICE_REMOVED}. + * Contains the identifier of the virtual device, which was removed. + * + * @hide + */ + public static final String EXTRA_VIRTUAL_DEVICE_ID = + "android.companion.virtual.extra.VIRTUAL_DEVICE_ID"; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java index 1c4898a4c8d0f..18118f5bfe29a 100644 --- a/core/java/android/hardware/SystemSensorManager.java +++ b/core/java/android/hardware/SystemSensorManager.java @@ -16,8 +16,14 @@ package android.hardware; +import static android.companion.virtual.VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED; +import static android.companion.virtual.VirtualDeviceManager.DEFAULT_DEVICE_ID; +import static android.companion.virtual.VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID; +import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; +import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import android.companion.virtual.VirtualDeviceManager; import android.compat.Compatibility; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledAfter; @@ -45,6 +51,7 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -80,6 +87,8 @@ public class SystemSensorManager extends SensorManager { private static native boolean nativeGetSensorAtIndex(long nativeInstance, Sensor sensor, int index); private static native void nativeGetDynamicSensors(long nativeInstance, List list); + private static native void nativeGetRuntimeSensors( + long nativeInstance, int deviceId, List list); private static native boolean nativeIsDataInjectionEnabled(long nativeInstance); private static native int nativeCreateDirectChannel( @@ -100,6 +109,10 @@ public class SystemSensorManager extends SensorManager { private final ArrayList mFullSensorsList = new ArrayList<>(); private List mFullDynamicSensorsList = new ArrayList<>(); + private final SparseArray> mFullRuntimeSensorListByDevice = new SparseArray<>(); + private final SparseArray>> mRuntimeSensorListByDeviceByType = + new SparseArray<>(); + private boolean mDynamicSensorListDirty = true; private final HashMap mHandleToSensor = new HashMap<>(); @@ -114,6 +127,7 @@ public class SystemSensorManager extends SensorManager { private HashMap mDynamicSensorCallbacks = new HashMap<>(); private BroadcastReceiver mDynamicSensorBroadcastReceiver; + private BroadcastReceiver mRuntimeSensorBroadcastReceiver; // Looper associated with the context in which this instance was created. private final Looper mMainLooper; @@ -121,6 +135,7 @@ public class SystemSensorManager extends SensorManager { private final boolean mIsPackageDebuggable; private final Context mContext; private final long mNativeInstance; + private final VirtualDeviceManager mVdm; private Optional mHasHighSamplingRateSensorsPermission = Optional.empty(); @@ -139,6 +154,7 @@ public class SystemSensorManager extends SensorManager { mContext = context; mNativeInstance = nativeCreate(context.getOpPackageName()); mIsPackageDebuggable = (0 != (appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE)); + mVdm = mContext.getSystemService(VirtualDeviceManager.class); // initialize the sensor list for (int index = 0;; ++index) { @@ -147,12 +163,63 @@ public class SystemSensorManager extends SensorManager { mFullSensorsList.add(sensor); mHandleToSensor.put(sensor.getHandle(), sensor); } + + } + + /** @hide */ + @Override + public List getSensorList(int type) { + final int deviceId = mContext.getDeviceId(); + if (deviceId == DEFAULT_DEVICE_ID || mVdm == null + || mVdm.getDevicePolicy(deviceId, POLICY_TYPE_SENSORS) == DEVICE_POLICY_DEFAULT) { + return super.getSensorList(type); + } + + // Cache the per-device lists on demand. + List list; + synchronized (mFullRuntimeSensorListByDevice) { + List fullList = mFullRuntimeSensorListByDevice.get(deviceId); + if (fullList == null) { + fullList = createRuntimeSensorListLocked(deviceId); + } + SparseArray> deviceSensorListByType = + mRuntimeSensorListByDeviceByType.get(deviceId); + list = deviceSensorListByType.get(type); + if (list == null) { + if (type == Sensor.TYPE_ALL) { + list = fullList; + } else { + list = new ArrayList<>(); + for (Sensor i : fullList) { + if (i.getType() == type) { + list.add(i); + } + } + } + list = Collections.unmodifiableList(list); + deviceSensorListByType.append(type, list); + } + } + return list; } /** @hide */ @Override protected List getFullSensorList() { - return mFullSensorsList; + final int deviceId = mContext.getDeviceId(); + if (deviceId == DEFAULT_DEVICE_ID || mVdm == null + || mVdm.getDevicePolicy(deviceId, POLICY_TYPE_SENSORS) == DEVICE_POLICY_DEFAULT) { + return mFullSensorsList; + } + + List fullList; + synchronized (mFullRuntimeSensorListByDevice) { + fullList = mFullRuntimeSensorListByDevice.get(deviceId); + if (fullList == null) { + fullList = createRuntimeSensorListLocked(deviceId); + } + } + return fullList; } /** @hide */ @@ -446,12 +513,53 @@ public class SystemSensorManager extends SensorManager { } } + private List createRuntimeSensorListLocked(int deviceId) { + setupRuntimeSensorBroadcastReceiver(); + List list = new ArrayList<>(); + nativeGetRuntimeSensors(mNativeInstance, deviceId, list); + mFullRuntimeSensorListByDevice.put(deviceId, list); + mRuntimeSensorListByDeviceByType.put(deviceId, new SparseArray<>()); + for (Sensor s : list) { + mHandleToSensor.put(s.getHandle(), s); + } + return list; + } + + private void setupRuntimeSensorBroadcastReceiver() { + if (mRuntimeSensorBroadcastReceiver == null) { + mRuntimeSensorBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(ACTION_VIRTUAL_DEVICE_REMOVED)) { + synchronized (mFullRuntimeSensorListByDevice) { + final int deviceId = intent.getIntExtra( + EXTRA_VIRTUAL_DEVICE_ID, DEFAULT_DEVICE_ID); + List removedSensors = + mFullRuntimeSensorListByDevice.removeReturnOld(deviceId); + if (removedSensors != null) { + for (Sensor s : removedSensors) { + cleanupSensorConnection(s); + } + } + mRuntimeSensorListByDeviceByType.remove(deviceId); + } + } + } + }; + + IntentFilter filter = new IntentFilter("virtual_device_removed"); + filter.addAction(ACTION_VIRTUAL_DEVICE_REMOVED); + mContext.registerReceiver(mRuntimeSensorBroadcastReceiver, filter, + Context.RECEIVER_NOT_EXPORTED); + } + } + private void setupDynamicSensorBroadcastReceiver() { if (mDynamicSensorBroadcastReceiver == null) { mDynamicSensorBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (intent.getAction() == Intent.ACTION_DYNAMIC_SENSOR_CHANGED) { + if (intent.getAction().equals(Intent.ACTION_DYNAMIC_SENSOR_CHANGED)) { if (DEBUG_DYNAMIC_SENSOR) { Log.i(TAG, "DYNS received DYNAMIC_SENSOR_CHANED broadcast"); } diff --git a/core/jni/android_hardware_SensorManager.cpp b/core/jni/android_hardware_SensorManager.cpp index cb97698fefea5..939a0e4119139 100644 --- a/core/jni/android_hardware_SensorManager.cpp +++ b/core/jni/android_hardware_SensorManager.cpp @@ -243,6 +243,23 @@ nativeGetDynamicSensors(JNIEnv *env, jclass clazz, jlong sensorManager, jobject } } +static void nativeGetRuntimeSensors(JNIEnv *env, jclass clazz, jlong sensorManager, jint deviceId, + jobject sensorList) { + SensorManager *mgr = reinterpret_cast(sensorManager); + const ListOffsets &listOffsets(gListOffsets); + + Vector nativeList; + + mgr->getRuntimeSensorList(deviceId, nativeList); + + ALOGI("DYNS native SensorManager.getRuntimeSensorList return %zu sensors", nativeList.size()); + for (size_t i = 0; i < nativeList.size(); ++i) { + jobject sensor = translateNativeSensorToJavaSensor(env, NULL, nativeList[i]); + // add to list + env->CallBooleanMethod(sensorList, listOffsets.add, sensor); + } +} + static jboolean nativeIsDataInjectionEnabled(JNIEnv *_env, jclass _this, jlong sensorManager) { SensorManager* mgr = reinterpret_cast(sensorManager); return mgr->isDataInjectionEnabled(); @@ -503,40 +520,26 @@ static jint nativeInjectSensorData(JNIEnv *env, jclass clazz, jlong eventQ, jint //---------------------------------------------------------------------------- static const JNINativeMethod gSystemSensorManagerMethods[] = { - {"nativeClassInit", - "()V", - (void*)nativeClassInit }, - {"nativeCreate", - "(Ljava/lang/String;)J", - (void*)nativeCreate }, + {"nativeClassInit", "()V", (void *)nativeClassInit}, + {"nativeCreate", "(Ljava/lang/String;)J", (void *)nativeCreate}, - {"nativeGetSensorAtIndex", - "(JLandroid/hardware/Sensor;I)Z", - (void*)nativeGetSensorAtIndex }, + {"nativeGetSensorAtIndex", "(JLandroid/hardware/Sensor;I)Z", + (void *)nativeGetSensorAtIndex}, - {"nativeGetDynamicSensors", - "(JLjava/util/List;)V", - (void*)nativeGetDynamicSensors }, + {"nativeGetDynamicSensors", "(JLjava/util/List;)V", (void *)nativeGetDynamicSensors}, - {"nativeIsDataInjectionEnabled", - "(J)Z", - (void*)nativeIsDataInjectionEnabled }, + {"nativeGetRuntimeSensors", "(JILjava/util/List;)V", (void *)nativeGetRuntimeSensors}, - {"nativeCreateDirectChannel", - "(JJIILandroid/hardware/HardwareBuffer;)I", - (void*)nativeCreateDirectChannel }, + {"nativeIsDataInjectionEnabled", "(J)Z", (void *)nativeIsDataInjectionEnabled}, - {"nativeDestroyDirectChannel", - "(JI)V", - (void*)nativeDestroyDirectChannel }, + {"nativeCreateDirectChannel", "(JJIILandroid/hardware/HardwareBuffer;)I", + (void *)nativeCreateDirectChannel}, - {"nativeConfigDirectChannel", - "(JIII)I", - (void*)nativeConfigDirectChannel }, + {"nativeDestroyDirectChannel", "(JI)V", (void *)nativeDestroyDirectChannel}, - {"nativeSetOperationParameter", - "(JII[F[I)I", - (void*)nativeSetOperationParameter }, + {"nativeConfigDirectChannel", "(JIII)I", (void *)nativeConfigDirectChannel}, + + {"nativeSetOperationParameter", "(JII[F[I)I", (void *)nativeSetOperationParameter}, }; static const JNINativeMethod gBaseEventQueueMethods[] = { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index a8797a05ed240..ea957044bebe2 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -32,6 +32,7 @@ import android.companion.virtual.VirtualDevice; import android.companion.virtual.VirtualDeviceManager; import android.companion.virtual.VirtualDeviceParams; import android.content.Context; +import android.content.Intent; import android.hardware.display.DisplayManagerInternal; import android.hardware.display.IVirtualDisplayCallback; import android.hardware.display.VirtualDisplayConfig; @@ -280,7 +281,22 @@ public class VirtualDeviceManagerService extends SystemService { @Override public void onClose(int associationId) { synchronized (mVirtualDeviceManagerLock) { - mVirtualDevices.remove(associationId); + VirtualDeviceImpl removedDevice = + mVirtualDevices.removeReturnOld(associationId); + if (removedDevice != null) { + Intent i = new Intent( + VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED); + i.putExtra( + VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID, + removedDevice.getDeviceId()); + i.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + final long identity = Binder.clearCallingIdentity(); + try { + getContext().sendBroadcastAsUser(i, UserHandle.ALL); + } finally { + Binder.restoreCallingIdentity(identity); + } + } mAppsOnVirtualDevices.remove(associationId); if (cameraAccessController != null) { cameraAccessController.stopObservingIfNeeded();