diff options
8 files changed, 345 insertions, 4 deletions
diff --git a/core/java/android/os/CombinedVibrationEffect.aidl b/core/java/android/os/CombinedVibrationEffect.aidl new file mode 100644 index 000000000000..330733c2643f --- /dev/null +++ b/core/java/android/os/CombinedVibrationEffect.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 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.os; + +parcelable CombinedVibrationEffect; diff --git a/core/java/android/os/CombinedVibrationEffect.java b/core/java/android/os/CombinedVibrationEffect.java new file mode 100644 index 000000000000..77bfa577babd --- /dev/null +++ b/core/java/android/os/CombinedVibrationEffect.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2020 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.os; + +import android.annotation.NonNull; + +import java.util.Objects; + +/** + * A CombinedVibrationEffect describes a haptic effect to be performed by one or more {@link + * Vibrator Vibrators}. + * + * These effects may be any number of things, from single shot vibrations to complex waveforms. + * + * @hide + * @see VibrationEffect + */ +public abstract class CombinedVibrationEffect implements Parcelable { + private static final int PARCEL_TOKEN_MONO = 1; + + /** @hide to prevent subclassing from outside of the framework */ + public CombinedVibrationEffect() { + } + + /** + * Create a synced vibration effect. + * + * A synced vibration effect should be performed by multiple vibrators at the same time. + * + * @param effect The {@link VibrationEffect} to perform + * @return The desired combined effect. + */ + @NonNull + public static CombinedVibrationEffect createSynced(@NonNull VibrationEffect effect) { + CombinedVibrationEffect combined = new Mono(effect); + combined.validate(); + return combined; + } + + @Override + public int describeContents() { + return 0; + } + + /** @hide */ + public abstract void validate(); + + /** + * Represents a single {@link VibrationEffect} that should be executed in all vibrators in sync. + * + * @hide + */ + public static final class Mono extends CombinedVibrationEffect { + private final VibrationEffect mEffect; + + public Mono(Parcel in) { + mEffect = VibrationEffect.CREATOR.createFromParcel(in); + } + + public Mono(@NonNull VibrationEffect effect) { + mEffect = effect; + } + + public VibrationEffect getEffect() { + return mEffect; + } + + /** @hide */ + @Override + public void validate() { + mEffect.validate(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof CombinedVibrationEffect.Mono)) { + return false; + } + CombinedVibrationEffect.Mono other = (CombinedVibrationEffect.Mono) o; + return other.mEffect.equals(other.mEffect); + } + + @Override + public int hashCode() { + return Objects.hash(mEffect); + } + + @Override + public String toString() { + return "Mono{mEffect=" + mEffect + '}'; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(PARCEL_TOKEN_MONO); + mEffect.writeToParcel(out, flags); + } + + @NonNull + public static final Parcelable.Creator<Mono> CREATOR = + new Parcelable.Creator<Mono>() { + @Override + public Mono createFromParcel(@NonNull Parcel in) { + // Skip the type token + in.readInt(); + return new Mono(in); + } + + @Override + @NonNull + public Mono[] newArray(int size) { + return new Mono[size]; + } + }; + } + + @NonNull + public static final Parcelable.Creator<CombinedVibrationEffect> CREATOR = + new Parcelable.Creator<CombinedVibrationEffect>() { + @Override + public CombinedVibrationEffect createFromParcel(Parcel in) { + int token = in.readInt(); + if (token == PARCEL_TOKEN_MONO) { + return new CombinedVibrationEffect.Mono(in); + } else { + throw new IllegalStateException( + "Unexpected combined vibration event type token in parcel."); + } + } + + @Override + public CombinedVibrationEffect[] newArray(int size) { + return new CombinedVibrationEffect[size]; + } + }; +} diff --git a/core/java/android/os/IVibratorManagerService.aidl b/core/java/android/os/IVibratorManagerService.aidl index e821e3194d21..08d201977c49 100644 --- a/core/java/android/os/IVibratorManagerService.aidl +++ b/core/java/android/os/IVibratorManagerService.aidl @@ -16,9 +16,13 @@ package android.os; +import android.os.CombinedVibrationEffect; import android.os.VibrationAttributes; /** {@hide} */ interface IVibratorManagerService { int[] getVibratorIds(); + void vibrate(int uid, String opPkg, in CombinedVibrationEffect effect, + in VibrationAttributes attributes, String reason, IBinder token); + void cancelVibrate(IBinder token); } diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java index 487e46886914..21ad38b0e371 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -983,6 +983,8 @@ public abstract class VibrationEffect implements Parcelable { Composition.checkPrimitive(effect.id); Preconditions.checkArgumentInRange( effect.scale, 0.0f, 1.0f, "scale"); + Preconditions.checkArgumentNonNegative(effect.delay, + "Primitive delay must be zero or positive"); } } diff --git a/core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java b/core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java new file mode 100644 index 000000000000..faa67a8bbd62 --- /dev/null +++ b/core/tests/coretests/src/android/os/CombinedVibrationEffectTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 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.os; + +import static junit.framework.Assert.assertEquals; + +import static org.testng.Assert.assertThrows; + +import android.platform.test.annotations.Presubmit; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@Presubmit +@RunWith(JUnit4.class) +public class CombinedVibrationEffectTest { + + @Test + public void testValidateMono() { + CombinedVibrationEffect.createSynced(VibrationEffect.get(VibrationEffect.EFFECT_CLICK)); + + assertThrows(IllegalArgumentException.class, + () -> CombinedVibrationEffect.createSynced(new VibrationEffect.OneShot(-1, -1))); + } + + @Test + public void testSerializationMono() { + CombinedVibrationEffect original = CombinedVibrationEffect.createSynced( + VibrationEffect.get(VibrationEffect.EFFECT_CLICK)); + + Parcel parcel = Parcel.obtain(); + original.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + CombinedVibrationEffect restored = CombinedVibrationEffect.CREATOR.createFromParcel(parcel); + assertEquals(original, restored); + } +} diff --git a/core/tests/coretests/src/android/os/VibrationEffectTest.java b/core/tests/coretests/src/android/os/VibrationEffectTest.java index e6852929bbc2..c357414c8913 100644 --- a/core/tests/coretests/src/android/os/VibrationEffectTest.java +++ b/core/tests/coretests/src/android/os/VibrationEffectTest.java @@ -28,6 +28,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.testng.Assert.assertThrows; import android.content.ContentInterface; import android.content.ContentResolver; @@ -100,6 +101,73 @@ public class VibrationEffectTest { } @Test + public void testValidateOneShot() { + VibrationEffect.createOneShot(1, 255).validate(); + VibrationEffect.createOneShot(1, VibrationEffect.DEFAULT_AMPLITUDE).validate(); + + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.createOneShot(-1, 255).validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.createOneShot(0, 255).validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.createOneShot(1, -2).validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.createOneShot(1, 256).validate()); + } + + @Test + public void testValidatePrebaked() { + VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK).validate(); + VibrationEffect.createPredefined(VibrationEffect.RINGTONES[1]).validate(); + + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.createPredefined(-1).validate()); + } + + @Test + public void testValidateWaveform() { + VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -1).validate(); + VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, 0).validate(); + + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.createWaveform(new long[0], new int[0], -1).validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.createWaveform(TEST_TIMINGS, new int[0], -1).validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.createWaveform( + new long[]{0, 0, 0}, TEST_AMPLITUDES, -1).validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.createWaveform( + TEST_TIMINGS, new int[]{-1, -1, -2}, -1).validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.createWaveform( + TEST_TIMINGS, TEST_AMPLITUDES, TEST_TIMINGS.length).validate()); + } + + @Test + public void testValidateComposed() { + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10) + .compose() + .validate(); + + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.startComposition().addPrimitive(-1).compose().validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, -1, 10) + .compose() + .validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, -1) + .compose() + .validate()); + } + + @Test public void testScalePrebaked_ignoresScaleAndReturnsSameEffect() { VibrationEffect initial = VibrationEffect.get(VibrationEffect.RINGTONES[1]); assertSame(initial, initial.scale(0.5f)); diff --git a/services/core/java/com/android/server/VibratorManagerService.java b/services/core/java/com/android/server/VibratorManagerService.java index 2f35da79420e..356cd0cd937b 100644 --- a/services/core/java/com/android/server/VibratorManagerService.java +++ b/services/core/java/com/android/server/VibratorManagerService.java @@ -16,12 +16,15 @@ package com.android.server; +import android.annotation.Nullable; import android.content.Context; +import android.os.CombinedVibrationEffect; import android.os.IBinder; import android.os.IVibratorManagerService; import android.os.ResultReceiver; import android.os.ShellCallback; import android.os.ShellCommand; +import android.os.VibrationAttributes; import com.android.internal.annotations.VisibleForTesting; @@ -66,6 +69,17 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { return Arrays.copyOf(mVibratorIds, mVibratorIds.length); } + @Override // Binder call + public void vibrate(int uid, String opPkg, CombinedVibrationEffect effect, + @Nullable VibrationAttributes attrs, String reason, IBinder token) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override // Binder call + public void cancelVibrate(IBinder token) { + throw new UnsupportedOperationException("Not implemented"); + } + @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback cb, ResultReceiver resultReceiver) { diff --git a/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java index 8a22a2f5d92f..044bdbadb946 100644 --- a/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java @@ -16,10 +16,16 @@ package com.android.server; +import static com.android.server.testutils.TestUtils.assertExpectException; + import static org.junit.Assert.assertArrayEquals; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.os.CombinedVibrationEffect; +import android.os.Process; +import android.os.VibrationAttributes; +import android.os.VibrationEffect; import android.platform.test.annotations.Presubmit; import androidx.test.InstrumentationRegistry; @@ -40,9 +46,16 @@ import org.mockito.junit.MockitoRule; @Presubmit public class VibratorManagerServiceTest { - @Rule public MockitoRule rule = MockitoJUnit.rule(); + private static final int UID = Process.ROOT_UID; + private static final String PACKAGE_NAME = "package"; + private static final VibrationAttributes ALARM_ATTRS = + new VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_ALARM).build(); + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); - @Mock private VibratorManagerService.NativeWrapper mNativeWrapperMock; + @Mock + private VibratorManagerService.NativeWrapper mNativeWrapperMock; @Before public void setUp() throws Exception { @@ -72,7 +85,26 @@ public class VibratorManagerServiceTest { @Test public void getVibratorIds_withNonEmptyResultFromNative_returnsSameArray() { - when(mNativeWrapperMock.getVibratorIds()).thenReturn(new int[]{ 1, 2 }); - assertArrayEquals(new int[]{ 1, 2 }, createService().getVibratorIds()); + when(mNativeWrapperMock.getVibratorIds()).thenReturn(new int[]{1, 2}); + assertArrayEquals(new int[]{1, 2}, createService().getVibratorIds()); + } + + @Test + public void vibrate_isUnsupported() { + VibratorManagerService service = createService(); + CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced( + VibrationEffect.get(VibrationEffect.EFFECT_CLICK)); + assertExpectException(UnsupportedOperationException.class, + "Not implemented", + () -> service.vibrate(UID, PACKAGE_NAME, effect, ALARM_ATTRS, "reason", service)); + } + + @Test + public void cancelVibrate_isUnsupported() { + VibratorManagerService service = createService(); + CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced( + VibrationEffect.get(VibrationEffect.EFFECT_CLICK)); + assertExpectException(UnsupportedOperationException.class, + "Not implemented", () -> service.cancelVibrate(service)); } } |