Merge "Direct connection API for runtime sensors." into udc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
24618718bc
@@ -3347,10 +3347,15 @@ package android.companion.virtual.sensor {
|
||||
|
||||
public interface VirtualSensorCallback {
|
||||
method public void onConfigurationChanged(@NonNull android.companion.virtual.sensor.VirtualSensor, boolean, @NonNull java.time.Duration, @NonNull java.time.Duration);
|
||||
method public default void onDirectChannelConfigured(@IntRange(from=1) int, @NonNull android.companion.virtual.sensor.VirtualSensor, int, @IntRange(from=1) int);
|
||||
method public default void onDirectChannelCreated(@IntRange(from=1) int, @NonNull android.os.SharedMemory);
|
||||
method public default void onDirectChannelDestroyed(@IntRange(from=1) int);
|
||||
}
|
||||
|
||||
public final class VirtualSensorConfig implements android.os.Parcelable {
|
||||
method public int describeContents();
|
||||
method public int getDirectChannelTypesSupported();
|
||||
method public int getHighestDirectReportRateLevel();
|
||||
method @NonNull public String getName();
|
||||
method public int getType();
|
||||
method @Nullable public String getVendor();
|
||||
@@ -3361,6 +3366,8 @@ package android.companion.virtual.sensor {
|
||||
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 setDirectChannelTypesSupported(int);
|
||||
method @NonNull public android.companion.virtual.sensor.VirtualSensorConfig.Builder setHighestDirectReportRateLevel(int);
|
||||
method @NonNull public android.companion.virtual.sensor.VirtualSensorConfig.Builder setVendor(@Nullable String);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import android.companion.virtual.sensor.VirtualSensorConfig;
|
||||
import android.content.ComponentName;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.SharedMemory;
|
||||
import android.os.UserHandle;
|
||||
import android.util.ArraySet;
|
||||
import android.util.SparseArray;
|
||||
@@ -577,6 +578,25 @@ public final class VirtualDeviceParams implements Parcelable {
|
||||
mExecutor.execute(() -> mCallback.onConfigurationChanged(
|
||||
sensor, enabled, samplingPeriod, batchReportingLatency));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDirectChannelCreated(int channelHandle,
|
||||
@NonNull SharedMemory sharedMemory) {
|
||||
mExecutor.execute(
|
||||
() -> mCallback.onDirectChannelCreated(channelHandle, sharedMemory));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDirectChannelDestroyed(int channelHandle) {
|
||||
mExecutor.execute(() -> mCallback.onDirectChannelDestroyed(channelHandle));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDirectChannelConfigured(int channelHandle, @NonNull VirtualSensor sensor,
|
||||
int rateLevel, int reportToken) {
|
||||
mExecutor.execute(() -> mCallback.onDirectChannelConfigured(
|
||||
channelHandle, sensor, rateLevel, reportToken));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package android.companion.virtual.sensor;
|
||||
|
||||
import android.companion.virtual.sensor.VirtualSensor;
|
||||
import android.os.SharedMemory;
|
||||
|
||||
/**
|
||||
* Interface for notifying the sensor owner about whether and how sensor events should be injected.
|
||||
@@ -36,4 +37,31 @@ oneway interface IVirtualSensorCallback {
|
||||
*/
|
||||
void onConfigurationChanged(in VirtualSensor sensor, boolean enabled, int samplingPeriodMicros,
|
||||
int batchReportLatencyMicros);
|
||||
|
||||
/**
|
||||
* Called when a sensor direct channel is created.
|
||||
*
|
||||
* @param channelHandle Identifier of the channel that was created.
|
||||
* @param sharedMemory The shared memory region for the direct sensor channel.
|
||||
*/
|
||||
void onDirectChannelCreated(int channelHandle, in SharedMemory sharedMemory);
|
||||
|
||||
/**
|
||||
* Called when a sensor direct channel is destroyed.
|
||||
*
|
||||
* @param channelHandle Identifier of the channel that was destroyed.
|
||||
*/
|
||||
void onDirectChannelDestroyed(int channelHandle);
|
||||
|
||||
/**
|
||||
* Called when a sensor direct channel is configured.
|
||||
*
|
||||
* @param channelHandle Identifier of the channel that was configured.
|
||||
* @param sensor The sensor, for which the channel was configured.
|
||||
* @param rateLevel The rate level used to configure the direct sensor channel.
|
||||
* @param reportToken A positive sensor report token, used to differentiate between events from
|
||||
* different sensors within the same channel.
|
||||
*/
|
||||
void onDirectChannelConfigured(int channelHandle, in VirtualSensor sensor, int rateLevel,
|
||||
int reportToken);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,13 @@
|
||||
package android.companion.virtual.sensor;
|
||||
|
||||
|
||||
import android.annotation.IntRange;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.SystemApi;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorDirectChannel;
|
||||
import android.os.MemoryFile;
|
||||
import android.os.SharedMemory;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@@ -50,4 +55,74 @@ public interface VirtualSensorCallback {
|
||||
*/
|
||||
void onConfigurationChanged(@NonNull VirtualSensor sensor, boolean enabled,
|
||||
@NonNull Duration samplingPeriod, @NonNull Duration batchReportLatency);
|
||||
|
||||
/**
|
||||
* Called when a {@link android.hardware.SensorDirectChannel} is created.
|
||||
*
|
||||
* <p>The {@link android.hardware.SensorManager} instance used to create the direct channel must
|
||||
* be associated with the virtual device.
|
||||
*
|
||||
* <p>A typical order of callback invocations is:
|
||||
* <ul>
|
||||
* <li>{@code onDirectChannelCreated} - the channel handle and the associated shared memory
|
||||
* should be stored by the virtual device</li>
|
||||
* <li>{@code onDirectChannelConfigured} with a positive {@code rateLevel} - the virtual
|
||||
* device should start writing to the shared memory for the associated channel with the
|
||||
* requested parameters.</li>
|
||||
* <li>{@code onDirectChannelConfigured} with a {@code rateLevel = RATE_STOP} - the virtual
|
||||
* device should stop writing to the shared memory for the associated channel.</li>
|
||||
* <li>{@code onDirectChannelDestroyed} - the shared memory associated with the channel
|
||||
* handle should be closed.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param channelHandle Identifier of the newly created channel.
|
||||
* @param sharedMemory writable shared memory region.
|
||||
*
|
||||
* @see android.hardware.SensorManager#createDirectChannel(MemoryFile)
|
||||
* @see #onDirectChannelConfigured
|
||||
* @see #onDirectChannelDestroyed
|
||||
*/
|
||||
default void onDirectChannelCreated(@IntRange(from = 1) int channelHandle,
|
||||
@NonNull SharedMemory sharedMemory) {}
|
||||
|
||||
/**
|
||||
* Called when a {@link android.hardware.SensorDirectChannel} is destroyed.
|
||||
*
|
||||
* <p>The virtual device must perform any clean-up and close the shared memory that was
|
||||
* received with the {@link #onDirectChannelCreated} callback and the corresponding
|
||||
* {@code channelHandle}.
|
||||
*
|
||||
* @param channelHandle Identifier of the channel that was destroyed.
|
||||
*
|
||||
* @see SensorDirectChannel#close()
|
||||
*/
|
||||
default void onDirectChannelDestroyed(@IntRange(from = 1) int channelHandle) {}
|
||||
|
||||
/**
|
||||
* Called when a {@link android.hardware.SensorDirectChannel} is configured.
|
||||
*
|
||||
* <p>Sensor events for the corresponding sensor should be written at the indicated rate to the
|
||||
* shared memory region that was received with the {@link #onDirectChannelCreated} callback and
|
||||
* the corresponding {@code channelHandle}. The events should be written in the correct format
|
||||
* and with the provided {@code reportToken} until the channel is reconfigured with
|
||||
* {@link SensorDirectChannel#RATE_STOP}.
|
||||
*
|
||||
* <p>The sensor must support direct channel in order for this callback to be invoked. Only
|
||||
* {@link MemoryFile} sensor direct channels are supported for virtual sensors.
|
||||
*
|
||||
* @param channelHandle Identifier of the channel that was configured.
|
||||
* @param sensor The sensor, for which the channel was configured.
|
||||
* @param rateLevel The rate level used to configure the direct sensor channel.
|
||||
* @param reportToken A positive sensor report token, used to differentiate between events from
|
||||
* different sensors within the same channel.
|
||||
*
|
||||
* @see VirtualSensorConfig.Builder#setHighestDirectReportRateLevel(int)
|
||||
* @see VirtualSensorConfig.Builder#setDirectChannelTypesSupported(int)
|
||||
* @see android.hardware.SensorManager#createDirectChannel(MemoryFile)
|
||||
* @see #onDirectChannelCreated
|
||||
* @see SensorDirectChannel#configure(Sensor, int)
|
||||
*/
|
||||
default void onDirectChannelConfigured(@IntRange(from = 1) int channelHandle,
|
||||
@NonNull VirtualSensor sensor, @SensorDirectChannel.RateLevel int rateLevel,
|
||||
@IntRange(from = 1) int reportToken) {}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,13 @@ import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.SystemApi;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorDirectChannel;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
/**
|
||||
* Configuration for creation of a virtual sensor.
|
||||
* @see VirtualSensor
|
||||
@@ -33,6 +35,14 @@ import java.util.Objects;
|
||||
*/
|
||||
@SystemApi
|
||||
public final class VirtualSensorConfig implements Parcelable {
|
||||
private static final String TAG = "VirtualSensorConfig";
|
||||
|
||||
// Mask for direct mode highest rate level, bit 7, 8, 9.
|
||||
private static final int DIRECT_REPORT_MASK = 0x380;
|
||||
private static final int DIRECT_REPORT_SHIFT = 7;
|
||||
|
||||
// Mask for supported direct channel, bit 10, 11
|
||||
private static final int DIRECT_CHANNEL_SHIFT = 10;
|
||||
|
||||
private final int mType;
|
||||
@NonNull
|
||||
@@ -40,16 +50,21 @@ public final class VirtualSensorConfig implements Parcelable {
|
||||
@Nullable
|
||||
private final String mVendor;
|
||||
|
||||
private VirtualSensorConfig(int type, @NonNull String name, @Nullable String vendor) {
|
||||
private final int mFlags;
|
||||
|
||||
private VirtualSensorConfig(int type, @NonNull String name, @Nullable String vendor,
|
||||
int flags) {
|
||||
mType = type;
|
||||
mName = name;
|
||||
mVendor = vendor;
|
||||
mFlags = flags;
|
||||
}
|
||||
|
||||
private VirtualSensorConfig(@NonNull Parcel parcel) {
|
||||
mType = parcel.readInt();
|
||||
mName = parcel.readString8();
|
||||
mVendor = parcel.readString8();
|
||||
mFlags = parcel.readInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,6 +77,7 @@ public final class VirtualSensorConfig implements Parcelable {
|
||||
parcel.writeInt(mType);
|
||||
parcel.writeString8(mName);
|
||||
parcel.writeString8(mVendor);
|
||||
parcel.writeInt(mFlags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,23 +107,65 @@ public final class VirtualSensorConfig implements Parcelable {
|
||||
return mVendor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the highest supported direct report mode rate level of the sensor.
|
||||
*
|
||||
* @see Sensor#getHighestDirectReportRateLevel()
|
||||
*/
|
||||
@SensorDirectChannel.RateLevel
|
||||
public int getHighestDirectReportRateLevel() {
|
||||
int rateLevel = ((mFlags & DIRECT_REPORT_MASK) >> DIRECT_REPORT_SHIFT);
|
||||
return rateLevel <= SensorDirectChannel.RATE_VERY_FAST
|
||||
? rateLevel : SensorDirectChannel.RATE_VERY_FAST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a combination of all supported direct channel types.
|
||||
*
|
||||
* @see Builder#setDirectChannelTypesSupported(int)
|
||||
* @see Sensor#isDirectChannelTypeSupported(int)
|
||||
*/
|
||||
public @SensorDirectChannel.MemoryType int getDirectChannelTypesSupported() {
|
||||
int memoryTypes = 0;
|
||||
if ((mFlags & (1 << DIRECT_CHANNEL_SHIFT)) > 0) {
|
||||
memoryTypes |= SensorDirectChannel.TYPE_MEMORY_FILE;
|
||||
}
|
||||
if ((mFlags & (1 << (DIRECT_CHANNEL_SHIFT + 1))) > 0) {
|
||||
memoryTypes |= SensorDirectChannel.TYPE_HARDWARE_BUFFER;
|
||||
}
|
||||
return memoryTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sensor flags.
|
||||
* @hide
|
||||
*/
|
||||
public int getFlags() {
|
||||
return mFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for {@link VirtualSensorConfig}.
|
||||
*/
|
||||
public static final class Builder {
|
||||
|
||||
private static final int FLAG_MEMORY_FILE_DIRECT_CHANNEL_SUPPORTED =
|
||||
1 << DIRECT_CHANNEL_SHIFT;
|
||||
private final int mType;
|
||||
@NonNull
|
||||
private final String mName;
|
||||
@Nullable
|
||||
private String mVendor;
|
||||
private int mFlags;
|
||||
@SensorDirectChannel.RateLevel
|
||||
int mHighestDirectReportRateLevel;
|
||||
|
||||
/**
|
||||
* Creates a new builder.
|
||||
*
|
||||
* @param type The type of the sensor, matching {@link Sensor#getType}.
|
||||
* @param name The name of the sensor. Must be unique among all sensors with the same type
|
||||
* that belong to the same virtual device.
|
||||
* that belong to the same virtual device.
|
||||
*/
|
||||
public Builder(int type, @NonNull String name) {
|
||||
mType = type;
|
||||
@@ -119,7 +177,19 @@ public final class VirtualSensorConfig implements Parcelable {
|
||||
*/
|
||||
@NonNull
|
||||
public VirtualSensorConfig build() {
|
||||
return new VirtualSensorConfig(mType, mName, mVendor);
|
||||
if (mHighestDirectReportRateLevel > 0) {
|
||||
if ((mFlags & FLAG_MEMORY_FILE_DIRECT_CHANNEL_SUPPORTED) == 0) {
|
||||
throw new IllegalArgumentException("Setting direct channel type is required "
|
||||
+ "for sensors with direct channel support.");
|
||||
}
|
||||
mFlags |= mHighestDirectReportRateLevel << DIRECT_REPORT_SHIFT;
|
||||
}
|
||||
if ((mFlags & FLAG_MEMORY_FILE_DIRECT_CHANNEL_SUPPORTED) > 0
|
||||
&& mHighestDirectReportRateLevel == 0) {
|
||||
throw new IllegalArgumentException("Highest direct report rate level is "
|
||||
+ "required for sensors with direct channel support.");
|
||||
}
|
||||
return new VirtualSensorConfig(mType, mName, mVendor, mFlags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,6 +200,44 @@ public final class VirtualSensorConfig implements Parcelable {
|
||||
mVendor = vendor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the highest supported rate level for direct sensor report.
|
||||
*
|
||||
* @see VirtualSensorConfig#getHighestDirectReportRateLevel()
|
||||
*/
|
||||
@NonNull
|
||||
public VirtualSensorConfig.Builder setHighestDirectReportRateLevel(
|
||||
@SensorDirectChannel.RateLevel int rateLevel) {
|
||||
mHighestDirectReportRateLevel = rateLevel;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether direct sensor channel of the given types is supported.
|
||||
*
|
||||
* @param memoryTypes A combination of {@link SensorDirectChannel.MemoryType} flags
|
||||
* indicating the types of shared memory supported for creating direct channels. Only
|
||||
* {@link SensorDirectChannel#TYPE_MEMORY_FILE} direct channels may be supported for virtual
|
||||
* sensors.
|
||||
* @throws IllegalArgumentException if {@link SensorDirectChannel#TYPE_HARDWARE_BUFFER} is
|
||||
* set to be supported.
|
||||
*/
|
||||
@NonNull
|
||||
public VirtualSensorConfig.Builder setDirectChannelTypesSupported(
|
||||
@SensorDirectChannel.MemoryType int memoryTypes) {
|
||||
if ((memoryTypes & SensorDirectChannel.TYPE_MEMORY_FILE) > 0) {
|
||||
mFlags |= FLAG_MEMORY_FILE_DIRECT_CHANNEL_SUPPORTED;
|
||||
} else {
|
||||
mFlags &= ~FLAG_MEMORY_FILE_DIRECT_CHANNEL_SUPPORTED;
|
||||
}
|
||||
if ((memoryTypes & ~SensorDirectChannel.TYPE_MEMORY_FILE) > 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Only TYPE_MEMORY_FILE direct channels can be supported for virtual "
|
||||
+ "sensors.");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@@ -92,7 +92,8 @@ public class SystemSensorManager extends SensorManager {
|
||||
private static native boolean nativeIsDataInjectionEnabled(long nativeInstance);
|
||||
|
||||
private static native int nativeCreateDirectChannel(
|
||||
long nativeInstance, long size, int channelType, int fd, HardwareBuffer buffer);
|
||||
long nativeInstance, int deviceId, long size, int channelType, int fd,
|
||||
HardwareBuffer buffer);
|
||||
private static native void nativeDestroyDirectChannel(
|
||||
long nativeInstance, int channelHandle);
|
||||
private static native int nativeConfigDirectChannel(
|
||||
@@ -695,6 +696,10 @@ public class SystemSensorManager extends SensorManager {
|
||||
/** @hide */
|
||||
protected SensorDirectChannel createDirectChannelImpl(
|
||||
MemoryFile memoryFile, HardwareBuffer hardwareBuffer) {
|
||||
int deviceId = mContext.getDeviceId();
|
||||
if (isDeviceSensorPolicyDefault(deviceId)) {
|
||||
deviceId = DEVICE_ID_DEFAULT;
|
||||
}
|
||||
int id;
|
||||
int type;
|
||||
long size;
|
||||
@@ -713,8 +718,8 @@ public class SystemSensorManager extends SensorManager {
|
||||
}
|
||||
|
||||
size = memoryFile.length();
|
||||
id = nativeCreateDirectChannel(
|
||||
mNativeInstance, size, SensorDirectChannel.TYPE_MEMORY_FILE, fd, null);
|
||||
id = nativeCreateDirectChannel(mNativeInstance, deviceId, size,
|
||||
SensorDirectChannel.TYPE_MEMORY_FILE, fd, null);
|
||||
if (id <= 0) {
|
||||
throw new UncheckedIOException(
|
||||
new IOException("create MemoryFile direct channel failed " + id));
|
||||
@@ -738,7 +743,7 @@ public class SystemSensorManager extends SensorManager {
|
||||
}
|
||||
size = hardwareBuffer.getWidth();
|
||||
id = nativeCreateDirectChannel(
|
||||
mNativeInstance, size, SensorDirectChannel.TYPE_HARDWARE_BUFFER,
|
||||
mNativeInstance, deviceId, size, SensorDirectChannel.TYPE_HARDWARE_BUFFER,
|
||||
-1, hardwareBuffer);
|
||||
if (id <= 0) {
|
||||
throw new UncheckedIOException(
|
||||
|
||||
@@ -266,7 +266,8 @@ static jboolean nativeIsDataInjectionEnabled(JNIEnv *_env, jclass _this, jlong s
|
||||
}
|
||||
|
||||
static jint nativeCreateDirectChannel(JNIEnv *_env, jclass _this, jlong sensorManager,
|
||||
jlong size, jint channelType, jint fd, jobject hardwareBufferObj) {
|
||||
jint deviceId, jlong size, jint channelType, jint fd,
|
||||
jobject hardwareBufferObj) {
|
||||
const native_handle_t *nativeHandle = nullptr;
|
||||
NATIVE_HANDLE_DECLARE_STORAGE(ashmemHandle, 1, 0);
|
||||
|
||||
@@ -287,7 +288,7 @@ static jint nativeCreateDirectChannel(JNIEnv *_env, jclass _this, jlong sensorMa
|
||||
}
|
||||
|
||||
SensorManager* mgr = reinterpret_cast<SensorManager*>(sensorManager);
|
||||
return mgr->createDirectChannel(size, channelType, nativeHandle);
|
||||
return mgr->createDirectChannel(deviceId, size, channelType, nativeHandle);
|
||||
}
|
||||
|
||||
static void nativeDestroyDirectChannel(JNIEnv *_env, jclass _this, jlong sensorManager,
|
||||
@@ -532,7 +533,7 @@ static const JNINativeMethod gSystemSensorManagerMethods[] = {
|
||||
|
||||
{"nativeIsDataInjectionEnabled", "(J)Z", (void *)nativeIsDataInjectionEnabled},
|
||||
|
||||
{"nativeCreateDirectChannel", "(JJIILandroid/hardware/HardwareBuffer;)I",
|
||||
{"nativeCreateDirectChannel", "(JIJIILandroid/hardware/HardwareBuffer;)I",
|
||||
(void *)nativeCreateDirectChannel},
|
||||
|
||||
{"nativeDestroyDirectChannel", "(JI)V", (void *)nativeDestroyDirectChannel},
|
||||
|
||||
@@ -17,9 +17,15 @@
|
||||
package android.companion.virtual.sensor;
|
||||
|
||||
import static android.hardware.Sensor.TYPE_ACCELEROMETER;
|
||||
import static android.hardware.SensorDirectChannel.RATE_STOP;
|
||||
import static android.hardware.SensorDirectChannel.RATE_VERY_FAST;
|
||||
import static android.hardware.SensorDirectChannel.TYPE_HARDWARE_BUFFER;
|
||||
import static android.hardware.SensorDirectChannel.TYPE_MEMORY_FILE;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.testng.Assert.assertThrows;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
|
||||
@@ -40,6 +46,8 @@ public class VirtualSensorConfigTest {
|
||||
final VirtualSensorConfig originalConfig =
|
||||
new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME)
|
||||
.setVendor(SENSOR_VENDOR)
|
||||
.setHighestDirectReportRateLevel(RATE_VERY_FAST)
|
||||
.setDirectChannelTypesSupported(TYPE_MEMORY_FILE)
|
||||
.build();
|
||||
final Parcel parcel = Parcel.obtain();
|
||||
originalConfig.writeToParcel(parcel, /* flags= */ 0);
|
||||
@@ -49,6 +57,39 @@ public class VirtualSensorConfigTest {
|
||||
assertThat(recreatedConfig.getType()).isEqualTo(originalConfig.getType());
|
||||
assertThat(recreatedConfig.getName()).isEqualTo(originalConfig.getName());
|
||||
assertThat(recreatedConfig.getVendor()).isEqualTo(originalConfig.getVendor());
|
||||
assertThat(recreatedConfig.getHighestDirectReportRateLevel()).isEqualTo(RATE_VERY_FAST);
|
||||
assertThat(recreatedConfig.getDirectChannelTypesSupported()).isEqualTo(TYPE_MEMORY_FILE);
|
||||
// From hardware/libhardware/include/hardware/sensors-base.h:
|
||||
// 0x400 is SENSOR_FLAG_DIRECT_CHANNEL_ASHMEM (i.e. TYPE_MEMORY_FILE)
|
||||
// 0x800 is SENSOR_FLAG_DIRECT_CHANNEL_GRALLOC (i.e. TYPE_HARDWARE_BUFFER)
|
||||
// 7 is SENSOR_FLAG_SHIFT_DIRECT_REPORT
|
||||
assertThat(recreatedConfig.getFlags()).isEqualTo(0x400 | RATE_VERY_FAST << 7);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hardwareBufferDirectChannelTypeSupported_throwsException() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME)
|
||||
.setDirectChannelTypesSupported(TYPE_HARDWARE_BUFFER | TYPE_MEMORY_FILE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directChannelTypeSupported_missingHighestReportRateLevel_throwsException() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME)
|
||||
.setDirectChannelTypesSupported(TYPE_MEMORY_FILE)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directChannelTypeSupported_missingDirectChannelTypeSupported_throwsException() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME)
|
||||
.setHighestDirectReportRateLevel(RATE_VERY_FAST)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -56,5 +97,8 @@ public class VirtualSensorConfigTest {
|
||||
final VirtualSensorConfig config =
|
||||
new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME).build();
|
||||
assertThat(config.getVendor()).isNull();
|
||||
assertThat(config.getHighestDirectReportRateLevel()).isEqualTo(RATE_STOP);
|
||||
assertThat(config.getDirectChannelTypesSupported()).isEqualTo(0);
|
||||
assertThat(config.getFlags()).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,11 @@ import android.companion.virtual.sensor.IVirtualSensorCallback;
|
||||
import android.companion.virtual.sensor.VirtualSensor;
|
||||
import android.companion.virtual.sensor.VirtualSensorConfig;
|
||||
import android.companion.virtual.sensor.VirtualSensorEvent;
|
||||
import android.hardware.SensorDirectChannel;
|
||||
import android.os.IBinder;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.RemoteException;
|
||||
import android.os.SharedMemory;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Slog;
|
||||
|
||||
@@ -36,6 +39,7 @@ import java.io.PrintWriter;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/** Controls virtual sensors, including their lifecycle and sensor event dispatch. */
|
||||
public class SensorController {
|
||||
@@ -47,6 +51,8 @@ public class SensorController {
|
||||
private static final int UNKNOWN_ERROR = (-2147483647 - 1); // INT32_MIN value
|
||||
private static final int BAD_VALUE = -22;
|
||||
|
||||
private static AtomicInteger sNextDirectChannelHandle = new AtomicInteger(1);
|
||||
|
||||
private final Object mLock;
|
||||
private final int mVirtualDeviceId;
|
||||
@GuardedBy("mLock")
|
||||
@@ -57,8 +63,6 @@ public class SensorController {
|
||||
private final SensorManagerInternal mSensorManagerInternal;
|
||||
private final VirtualDeviceManagerInternal mVdmInternal;
|
||||
|
||||
|
||||
|
||||
public SensorController(@NonNull Object lock, int virtualDeviceId,
|
||||
@Nullable IVirtualSensorCallback virtualSensorCallback) {
|
||||
mLock = lock;
|
||||
@@ -97,7 +101,7 @@ public class SensorController {
|
||||
throws SensorCreationException {
|
||||
final int handle = mSensorManagerInternal.createRuntimeSensor(mVirtualDeviceId,
|
||||
config.getType(), config.getName(),
|
||||
config.getVendor() == null ? "" : config.getVendor(),
|
||||
config.getVendor() == null ? "" : config.getVendor(), config.getFlags(),
|
||||
mRuntimeSensorCallback);
|
||||
if (handle <= 0) {
|
||||
throw new SensorCreationException("Received an invalid virtual sensor handle.");
|
||||
@@ -212,6 +216,66 @@ public class SensorController {
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onDirectChannelCreated(ParcelFileDescriptor fd) {
|
||||
if (mCallback == null) {
|
||||
Slog.e(TAG, "No sensor callback for virtual deviceId " + mVirtualDeviceId);
|
||||
return BAD_VALUE;
|
||||
} else if (fd == null) {
|
||||
Slog.e(TAG, "Received invalid ParcelFileDescriptor");
|
||||
return BAD_VALUE;
|
||||
}
|
||||
final int channelHandle = sNextDirectChannelHandle.getAndIncrement();
|
||||
SharedMemory sharedMemory = SharedMemory.fromFileDescriptor(fd);
|
||||
try {
|
||||
mCallback.onDirectChannelCreated(channelHandle, sharedMemory);
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "Failed to call sensor callback: " + e);
|
||||
return UNKNOWN_ERROR;
|
||||
}
|
||||
return channelHandle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDirectChannelDestroyed(int channelHandle) {
|
||||
if (mCallback == null) {
|
||||
Slog.e(TAG, "No sensor callback for virtual deviceId " + mVirtualDeviceId);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mCallback.onDirectChannelDestroyed(channelHandle);
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "Failed to call sensor callback: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onDirectChannelConfigured(int channelHandle, int sensorHandle,
|
||||
@SensorDirectChannel.RateLevel int rateLevel) {
|
||||
if (mCallback == null) {
|
||||
Slog.e(TAG, "No runtime sensor callback configured.");
|
||||
return BAD_VALUE;
|
||||
}
|
||||
VirtualSensor sensor = mVdmInternal.getVirtualSensor(mVirtualDeviceId, sensorHandle);
|
||||
if (sensor == null) {
|
||||
Slog.e(TAG, "No sensor found for deviceId=" + mVirtualDeviceId
|
||||
+ " and sensor handle=" + sensorHandle);
|
||||
return BAD_VALUE;
|
||||
}
|
||||
try {
|
||||
mCallback.onDirectChannelConfigured(channelHandle, sensor, rateLevel, sensorHandle);
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "Failed to call sensor callback: " + e);
|
||||
return UNKNOWN_ERROR;
|
||||
}
|
||||
if (rateLevel == SensorDirectChannel.RATE_STOP) {
|
||||
return OK;
|
||||
} else {
|
||||
// Use the sensor handle as a report token, i.e. a unique identifier of the sensor.
|
||||
return sensorHandle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package com.android.server.sensors;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.hardware.SensorDirectChannel;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@@ -58,7 +60,7 @@ public abstract class SensorManagerInternal {
|
||||
* @return The sensor handle.
|
||||
*/
|
||||
public abstract int createRuntimeSensor(int deviceId, int type, @NonNull String name,
|
||||
@NonNull String vendor, @NonNull RuntimeSensorCallback callback);
|
||||
@NonNull String vendor, int flags, @NonNull RuntimeSensorCallback callback);
|
||||
|
||||
/**
|
||||
* Unregisters the sensor with the given handle from the framework.
|
||||
@@ -98,9 +100,31 @@ public abstract class SensorManagerInternal {
|
||||
public interface RuntimeSensorCallback {
|
||||
/**
|
||||
* Invoked when the listeners of the runtime sensor have changed.
|
||||
* Returns an error code if the invocation was unsuccessful, zero otherwise.
|
||||
* Returns zero on success, negative error code otherwise.
|
||||
*/
|
||||
int onConfigurationChanged(int handle, boolean enabled, int samplingPeriodMicros,
|
||||
int batchReportLatencyMicros);
|
||||
|
||||
/**
|
||||
* Invoked when a direct sensor channel has been created.
|
||||
* Wraps the file descriptor in a {@link android.os.SharedMemory} object and passes it to
|
||||
* the client process.
|
||||
* Returns a positive identifier of the channel on success, negative error code otherwise.
|
||||
*/
|
||||
int onDirectChannelCreated(ParcelFileDescriptor fd);
|
||||
|
||||
/**
|
||||
* Invoked when a direct sensor channel has been destroyed.
|
||||
*/
|
||||
void onDirectChannelDestroyed(int channelHandle);
|
||||
|
||||
/**
|
||||
* Invoked when a direct sensor channel has been configured for a sensor.
|
||||
* If the invocation is unsuccessful, a negative error code is returned.
|
||||
* On success, the return value is zero if the rate level is {@code RATE_STOP}, and a
|
||||
* positive report token otherwise.
|
||||
*/
|
||||
int onDirectChannelConfigured(int channelHandle, int sensorHandle,
|
||||
@SensorDirectChannel.RateLevel int rateLevel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,8 @@ public class SensorService extends SystemService {
|
||||
private static native void unregisterProximityActiveListenerNative(long ptr);
|
||||
|
||||
private static native int registerRuntimeSensorNative(long ptr, int deviceId, int type,
|
||||
String name, String vendor, SensorManagerInternal.RuntimeSensorCallback callback);
|
||||
String name, String vendor, int flags,
|
||||
SensorManagerInternal.RuntimeSensorCallback 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);
|
||||
@@ -95,9 +96,9 @@ public class SensorService extends SystemService {
|
||||
class LocalService extends SensorManagerInternal {
|
||||
@Override
|
||||
public int createRuntimeSensor(int deviceId, int type, @NonNull String name,
|
||||
@NonNull String vendor, @NonNull RuntimeSensorCallback callback) {
|
||||
@NonNull String vendor, int flags, @NonNull RuntimeSensorCallback callback) {
|
||||
synchronized (mLock) {
|
||||
int handle = registerRuntimeSensorNative(mPtr, deviceId, type, name, vendor,
|
||||
int handle = registerRuntimeSensorNative(mPtr, deviceId, type, name, vendor, flags,
|
||||
callback);
|
||||
mRuntimeSensorHandles.add(handle);
|
||||
return handle;
|
||||
|
||||
@@ -17,10 +17,13 @@
|
||||
#define LOG_TAG "NativeSensorService"
|
||||
|
||||
#include <android-base/properties.h>
|
||||
#include <android_os_NativeHandle.h>
|
||||
#include <android_runtime/AndroidRuntime.h>
|
||||
#include <core_jni_helpers.h>
|
||||
#include <cutils/native_handle.h>
|
||||
#include <cutils/properties.h>
|
||||
#include <jni.h>
|
||||
#include <nativehelper/JNIPlatformHelp.h>
|
||||
#include <sensorservice/SensorService.h>
|
||||
#include <string.h>
|
||||
#include <utils/Log.h>
|
||||
@@ -28,6 +31,8 @@
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include "android_util_Binder.h"
|
||||
|
||||
#define PROXIMITY_ACTIVE_CLASS \
|
||||
"com/android/server/sensors/SensorManagerInternal$ProximityActiveListener"
|
||||
|
||||
@@ -38,7 +43,10 @@ namespace android {
|
||||
|
||||
static JavaVM* sJvm = nullptr;
|
||||
static jmethodID sMethodIdOnProximityActive;
|
||||
static jmethodID sMethodIdOnConfigurationChanged;
|
||||
static jmethodID sMethodIdRuntimeSensorOnConfigurationChanged;
|
||||
static jmethodID sMethodIdRuntimeSensorOnDirectChannelCreated;
|
||||
static jmethodID sMethodIdRuntimeSensorOnDirectChannelDestroyed;
|
||||
static jmethodID sMethodIdRuntimeSensorOnDirectChannelConfigured;
|
||||
|
||||
class NativeSensorService {
|
||||
public:
|
||||
@@ -47,7 +55,7 @@ public:
|
||||
void registerProximityActiveListener();
|
||||
void unregisterProximityActiveListener();
|
||||
jint registerRuntimeSensor(JNIEnv* env, jint deviceId, jint type, jstring name, jstring vendor,
|
||||
jobject callback);
|
||||
jint flags, jobject callback);
|
||||
void unregisterRuntimeSensor(jint handle);
|
||||
jboolean sendRuntimeSensorEvent(JNIEnv* env, jint handle, jint type, jlong timestamp,
|
||||
jfloatArray values);
|
||||
@@ -74,6 +82,9 @@ private:
|
||||
|
||||
status_t onConfigurationChanged(int32_t handle, bool enabled, int64_t samplingPeriodNs,
|
||||
int64_t batchReportLatencyNs) override;
|
||||
int onDirectChannelCreated(int fd) override;
|
||||
void onDirectChannelDestroyed(int channelHandle) override;
|
||||
int onDirectChannelConfigured(int channelHandle, int sensorHandle, int rateLevel) override;
|
||||
|
||||
private:
|
||||
jobject mCallback;
|
||||
@@ -108,7 +119,7 @@ void NativeSensorService::unregisterProximityActiveListener() {
|
||||
}
|
||||
|
||||
jint NativeSensorService::registerRuntimeSensor(JNIEnv* env, jint deviceId, jint type, jstring name,
|
||||
jstring vendor, jobject callback) {
|
||||
jstring vendor, jint flags, jobject callback) {
|
||||
if (mService == nullptr) {
|
||||
ALOGD("Dropping registerRuntimeSensor, sensor service not available.");
|
||||
return -1;
|
||||
@@ -119,6 +130,11 @@ jint NativeSensorService::registerRuntimeSensor(JNIEnv* env, jint deviceId, jint
|
||||
.vendor = env->GetStringUTFChars(vendor, 0),
|
||||
.version = sizeof(sensor_t),
|
||||
.type = type,
|
||||
#ifdef __LP64__
|
||||
.flags = static_cast<uint64_t>(flags),
|
||||
#else
|
||||
.flags = static_cast<uint32_t>(flags),
|
||||
#endif
|
||||
};
|
||||
|
||||
sp<RuntimeSensorCallbackDelegate> callbackDelegate(
|
||||
@@ -234,12 +250,39 @@ NativeSensorService::RuntimeSensorCallbackDelegate::~RuntimeSensorCallbackDelega
|
||||
status_t NativeSensorService::RuntimeSensorCallbackDelegate::onConfigurationChanged(
|
||||
int32_t handle, bool enabled, int64_t samplingPeriodNs, int64_t batchReportLatencyNs) {
|
||||
auto jniEnv = GetOrAttachJNIEnvironment(sJvm);
|
||||
return jniEnv->CallIntMethod(mCallback, sMethodIdOnConfigurationChanged,
|
||||
return jniEnv->CallIntMethod(mCallback, sMethodIdRuntimeSensorOnConfigurationChanged,
|
||||
static_cast<jint>(handle), static_cast<jboolean>(enabled),
|
||||
static_cast<jint>(ns2us(samplingPeriodNs)),
|
||||
static_cast<jint>(ns2us(batchReportLatencyNs)));
|
||||
}
|
||||
|
||||
int NativeSensorService::RuntimeSensorCallbackDelegate::onDirectChannelCreated(int fd) {
|
||||
if (fd <= 0) {
|
||||
return 0;
|
||||
}
|
||||
auto jniEnv = GetOrAttachJNIEnvironment(sJvm);
|
||||
jobject jfd = jniCreateFileDescriptor(jniEnv, fd);
|
||||
jobject parcelFileDescriptor = newParcelFileDescriptor(jniEnv, jfd);
|
||||
return jniEnv->CallIntMethod(mCallback, sMethodIdRuntimeSensorOnDirectChannelCreated,
|
||||
parcelFileDescriptor);
|
||||
}
|
||||
|
||||
void NativeSensorService::RuntimeSensorCallbackDelegate::onDirectChannelDestroyed(
|
||||
int channelHandle) {
|
||||
auto jniEnv = GetOrAttachJNIEnvironment(sJvm);
|
||||
return jniEnv->CallVoidMethod(mCallback, sMethodIdRuntimeSensorOnDirectChannelDestroyed,
|
||||
static_cast<jint>(channelHandle));
|
||||
}
|
||||
|
||||
int NativeSensorService::RuntimeSensorCallbackDelegate::onDirectChannelConfigured(int channelHandle,
|
||||
int sensorHandle,
|
||||
int rateLevel) {
|
||||
auto jniEnv = GetOrAttachJNIEnvironment(sJvm);
|
||||
return jniEnv->CallIntMethod(mCallback, sMethodIdRuntimeSensorOnDirectChannelConfigured,
|
||||
static_cast<jint>(channelHandle), static_cast<jint>(sensorHandle),
|
||||
static_cast<jint>(rateLevel));
|
||||
}
|
||||
|
||||
static jlong startSensorServiceNative(JNIEnv* env, jclass, jobject listener) {
|
||||
NativeSensorService* service = new NativeSensorService(env, listener);
|
||||
return reinterpret_cast<jlong>(service);
|
||||
@@ -256,9 +299,10 @@ static void unregisterProximityActiveListenerNative(JNIEnv* env, jclass, jlong p
|
||||
}
|
||||
|
||||
static jint registerRuntimeSensorNative(JNIEnv* env, jclass, jlong ptr, jint deviceId, jint type,
|
||||
jstring name, jstring vendor, jobject callback) {
|
||||
jstring name, jstring vendor, jint flags,
|
||||
jobject callback) {
|
||||
auto* service = reinterpret_cast<NativeSensorService*>(ptr);
|
||||
return service->registerRuntimeSensor(env, deviceId, type, name, vendor, callback);
|
||||
return service->registerRuntimeSensor(env, deviceId, type, name, vendor, flags, callback);
|
||||
}
|
||||
|
||||
static void unregisterRuntimeSensorNative(JNIEnv* env, jclass, jlong ptr, jint handle) {
|
||||
@@ -280,7 +324,7 @@ static const JNINativeMethod methods[] = {
|
||||
{"unregisterProximityActiveListenerNative", "(J)V",
|
||||
reinterpret_cast<void*>(unregisterProximityActiveListenerNative)},
|
||||
{"registerRuntimeSensorNative",
|
||||
"(JIILjava/lang/String;Ljava/lang/String;L" RUNTIME_SENSOR_CALLBACK_CLASS ";)I",
|
||||
"(JIILjava/lang/String;Ljava/lang/String;IL" RUNTIME_SENSOR_CALLBACK_CLASS ";)I",
|
||||
reinterpret_cast<void*>(registerRuntimeSensorNative)},
|
||||
{"unregisterRuntimeSensorNative", "(JI)V",
|
||||
reinterpret_cast<void*>(unregisterRuntimeSensorNative)},
|
||||
@@ -293,8 +337,17 @@ int register_android_server_sensor_SensorService(JavaVM* vm, JNIEnv* env) {
|
||||
jclass listenerClass = FindClassOrDie(env, PROXIMITY_ACTIVE_CLASS);
|
||||
sMethodIdOnProximityActive = GetMethodIDOrDie(env, listenerClass, "onProximityActive", "(Z)V");
|
||||
jclass runtimeSensorCallbackClass = FindClassOrDie(env, RUNTIME_SENSOR_CALLBACK_CLASS);
|
||||
sMethodIdOnConfigurationChanged =
|
||||
sMethodIdRuntimeSensorOnConfigurationChanged =
|
||||
GetMethodIDOrDie(env, runtimeSensorCallbackClass, "onConfigurationChanged", "(IZII)I");
|
||||
sMethodIdRuntimeSensorOnDirectChannelCreated =
|
||||
GetMethodIDOrDie(env, runtimeSensorCallbackClass, "onDirectChannelCreated",
|
||||
"(Landroid/os/ParcelFileDescriptor;)I");
|
||||
sMethodIdRuntimeSensorOnDirectChannelDestroyed =
|
||||
GetMethodIDOrDie(env, runtimeSensorCallbackClass, "onDirectChannelDestroyed", "(I)V");
|
||||
sMethodIdRuntimeSensorOnDirectChannelConfigured =
|
||||
GetMethodIDOrDie(env, runtimeSensorCallbackClass, "onDirectChannelConfigured",
|
||||
"(III)I");
|
||||
|
||||
return jniRegisterNativeMethods(env, "com/android/server/sensors/SensorService", methods,
|
||||
NELEM(methods));
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ public class SensorControllerTest {
|
||||
@Test
|
||||
public void createSensor_invalidHandle_throwsException() {
|
||||
doReturn(/* handle= */0).when(mSensorManagerInternalMock).createRuntimeSensor(
|
||||
anyInt(), anyInt(), anyString(), anyString(), any());
|
||||
anyInt(), anyInt(), anyString(), anyString(), anyInt(), any());
|
||||
|
||||
Throwable thrown = assertThrows(
|
||||
RuntimeException.class,
|
||||
@@ -138,7 +138,7 @@ public class SensorControllerTest {
|
||||
|
||||
private void doCreateSensorSuccessfully() {
|
||||
doReturn(SENSOR_HANDLE).when(mSensorManagerInternalMock).createRuntimeSensor(
|
||||
anyInt(), anyInt(), anyString(), anyString(), any());
|
||||
anyInt(), anyInt(), anyString(), anyString(), anyInt(), any());
|
||||
assertThat(mSensorController.createSensor(mSensorToken, mVirtualSensorConfig))
|
||||
.isEqualTo(SENSOR_HANDLE);
|
||||
}
|
||||
|
||||
@@ -500,7 +500,7 @@ public class VirtualDeviceManagerServiceTest {
|
||||
.build();
|
||||
|
||||
doReturn(SENSOR_HANDLE).when(mSensorManagerInternalMock).createRuntimeSensor(
|
||||
anyInt(), anyInt(), anyString(), anyString(), any());
|
||||
anyInt(), anyInt(), anyString(), anyString(), anyInt(), any());
|
||||
mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID_1, DEVICE_OWNER_UID_1, params);
|
||||
|
||||
VirtualSensor sensor = mLocalService.getVirtualSensor(VIRTUAL_DEVICE_ID_1, SENSOR_HANDLE);
|
||||
|
||||
Reference in New Issue
Block a user