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
This commit is contained in:
Vladimir Komsiyski
2022-10-21 16:11:51 +02:00
parent b6159f3186
commit ca9308224a
14 changed files with 938 additions and 31 deletions

View File

@@ -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<android.os.UserHandle> getUsersWithMatchingAccounts();
method @NonNull public java.util.List<android.companion.virtual.sensor.VirtualSensorConfig> 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<android.content.ComponentName>);
method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAllowedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>);
@@ -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<android.companion.virtual.sensor.VirtualSensorConfig> 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<android.companion.virtual.sensor.VirtualSensorEvent> 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 {

View File

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

View File

@@ -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<VirtualSensor> 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<VirtualSensorConfig> 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.

View File

@@ -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<VirtualSensorConfig> mVirtualSensorConfigs;
private VirtualDeviceParams(
@LockState int lockState,
@@ -169,24 +172,22 @@ public final class VirtualDeviceParams implements Parcelable {
@NonNull Set<ComponentName> 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<VirtualSensorConfig> 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<VirtualSensorConfig> 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<VirtualSensorConfig> 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<UserHandle> 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<ComponentName> 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<ComponentName> 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<ComponentName> 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<ComponentName> 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<Set<String>> sensorNameByType = new SparseArray();
for (int i = 0; i < mVirtualSensorConfigs.size(); ++i) {
VirtualSensorConfig config = mVirtualSensorConfigs.get(i);
Set<String> 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);
}
}
}

View File

@@ -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);
}

View File

@@ -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
* <a href="https://source.android.com/devices/sensors/sensor-types">type</a> 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();
}
}
}

View File

@@ -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;

View File

@@ -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
* <a href="https://source.android.com/devices/sensors/sensor-types">type</a> 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
* <a href="https://source.android.com/devices/sensors/sensor-types">type</a> 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<VirtualSensorConfig> CREATOR =
new Parcelable.Creator<>() {
public VirtualSensorConfig createFromParcel(Parcel source) {
return new VirtualSensorConfig(source);
}
public VirtualSensorConfig[] newArray(int size) {
return new VirtualSensorConfig[size];
}
};
}

View File

@@ -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;

View File

@@ -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
* <a href="https://source.android.com/devices/sensors/sensor-types">sensor type</a>.
* @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<VirtualSensorEvent> CREATOR =
new Parcelable.Creator<>() {
public VirtualSensorEvent createFromParcel(Parcel source) {
return new VirtualSensorEvent(source);
}
public VirtualSensorEvent[] newArray(int size) {
return new VirtualSensorEvent[size];
}
};
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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: ");

View File

@@ -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<VirtualSensorConfig> 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();
}
}