From bf7a301ab05c8b3fcc0c7f6e217609af9b87fe1b Mon Sep 17 00:00:00 2001 From: Lais Andrade Date: Fri, 17 Apr 2020 12:29:39 +0000 Subject: [PATCH] Apply intensity settings to composed vibration effects Use the existing maxAmplitude and gamma adjustment values to apply the relative scaling of default vibration intensity settings and user settings on top of the PrimitiveEffect#scale value. As a consequence, the relative scaled amplitude of a OneShot or Waveform vibration effect should match the scaled PrimitiveEffect#scale value. Fix: 154089649 Test: atest FrameworksCoreTests:VibrationEffectTest Change-Id: I3ca0042a42675289a2ed8110c9dc3798055ebf4a --- core/java/android/os/VibrationEffect.java | 28 +++++ .../src/android/os/VibrationEffectTest.java | 102 ++++++++++++++++-- .../com/android/server/VibratorService.java | 3 + 3 files changed, 127 insertions(+), 6 deletions(-) diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java index ca861577ab37f..1fef071563103 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -897,6 +897,34 @@ public abstract class VibrationEffect implements Parcelable { return -1; } + /** + * Scale all primitives of this effect. + * + * @param gamma the gamma adjustment to apply + * @param maxAmplitude the new maximum amplitude of the effect, must be between 0 and + * MAX_AMPLITUDE + * @throws IllegalArgumentException if maxAmplitude less than 0 or more than MAX_AMPLITUDE + * + * @return A {@link Composed} effect with same but scaled primitives. + */ + public Composed scale(float gamma, int maxAmplitude) { + if (maxAmplitude > MAX_AMPLITUDE || maxAmplitude < 0) { + throw new IllegalArgumentException( + "Amplitude is negative or greater than MAX_AMPLITUDE"); + } + if (gamma == 1.0f && maxAmplitude == MAX_AMPLITUDE) { + // Just return a copy of the original if there's no scaling to be done. + return new Composed(mPrimitiveEffects); + } + List scaledPrimitives = new ArrayList<>(); + for (Composition.PrimitiveEffect primitive : mPrimitiveEffects) { + float adjustedScale = MathUtils.pow(primitive.scale, gamma); + float newScale = adjustedScale * maxAmplitude / (float) MAX_AMPLITUDE; + scaledPrimitives.add(new Composition.PrimitiveEffect( + primitive.id, newScale, primitive.delay)); + } + return new Composed(scaledPrimitives); + } /** * @hide diff --git a/core/tests/coretests/src/android/os/VibrationEffectTest.java b/core/tests/coretests/src/android/os/VibrationEffectTest.java index ea778fd6710a0..a354f1d8109f1 100644 --- a/core/tests/coretests/src/android/os/VibrationEffectTest.java +++ b/core/tests/coretests/src/android/os/VibrationEffectTest.java @@ -22,9 +22,12 @@ import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.content.ContentInterface; +import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.net.Uri; @@ -37,6 +40,8 @@ import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class VibrationEffectTest { + private static final float SCALE_TOLERANCE = 1e-2f; + private static final String RINGTONE_URI_1 = "content://test/system/ringtone_1"; private static final String RINGTONE_URI_2 = "content://test/system/ringtone_2"; private static final String RINGTONE_URI_3 = "content://test/system/ringtone_3"; @@ -54,6 +59,12 @@ public class VibrationEffectTest { VibrationEffect.createOneShot(TEST_TIMING, VibrationEffect.DEFAULT_AMPLITUDE); private static final VibrationEffect TEST_WAVEFORM = VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -1); + private static final VibrationEffect TEST_COMPOSED = + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0, 100) + .compose(); @Test public void getRingtones_noPrebakedRingtones() { @@ -123,8 +134,14 @@ public class VibrationEffectTest { @Test public void testScaleWaveform() { - VibrationEffect.Waveform scaled = - ((VibrationEffect.Waveform) TEST_WAVEFORM).scale(1.1f, 200); + VibrationEffect.Waveform initial = (VibrationEffect.Waveform) TEST_WAVEFORM; + + VibrationEffect.Waveform copied = initial.scale(1f, 255); + assertEquals(255, copied.getAmplitudes()[0]); + assertEquals(0, copied.getAmplitudes()[1]); + assertEquals(-1, copied.getAmplitudes()[2]); + + VibrationEffect.Waveform scaled = initial.scale(1.1f, 200); assertEquals(200, scaled.getAmplitudes()[0]); assertEquals(0, scaled.getAmplitudes()[1]); } @@ -156,6 +173,66 @@ public class VibrationEffectTest { } } + @Test + public void testScaleComposed() { + VibrationEffect.Composed initial = (VibrationEffect.Composed) TEST_COMPOSED; + + VibrationEffect.Composed copied = initial.scale(1, 255); + assertEquals(1f, copied.getPrimitiveEffects().get(0).scale); + assertEquals(0.5f, copied.getPrimitiveEffects().get(1).scale); + assertEquals(0f, copied.getPrimitiveEffects().get(2).scale); + + VibrationEffect.Composed halved = initial.scale(1, 128); + assertEquals(0.5f, halved.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE); + assertEquals(0.25f, halved.getPrimitiveEffects().get(1).scale, SCALE_TOLERANCE); + assertEquals(0f, halved.getPrimitiveEffects().get(2).scale); + + VibrationEffect.Composed scaledUp = initial.scale(0.5f, 255); + assertEquals(1f, scaledUp.getPrimitiveEffects().get(0).scale); // does not scale up from 1 + assertTrue(0.5f < scaledUp.getPrimitiveEffects().get(1).scale); + assertEquals(0f, scaledUp.getPrimitiveEffects().get(2).scale); + + VibrationEffect.Composed restored = scaledUp.scale(2, 255); + assertEquals(1f, restored.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE); + assertEquals(0.5f, restored.getPrimitiveEffects().get(1).scale, SCALE_TOLERANCE); + assertEquals(0f, restored.getPrimitiveEffects().get(2).scale); + + VibrationEffect.Composed scaledDown = initial.scale(2, 255); + assertEquals(1f, scaledDown.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE); + assertTrue(0.5f > scaledDown.getPrimitiveEffects().get(1).scale); + assertEquals(0f, scaledDown.getPrimitiveEffects().get(2).scale, SCALE_TOLERANCE); + + VibrationEffect.Composed changeMax = initial.scale(1f, 51); + assertEquals(0.2f, changeMax.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE); + assertEquals(0.1f, changeMax.getPrimitiveEffects().get(1).scale, SCALE_TOLERANCE); + assertEquals(0f, changeMax.getPrimitiveEffects().get(2).scale); + } + + @Test + public void testScaleComposedFailsWhenMaxAmplitudeAboveThreshold() { + try { + ((VibrationEffect.Composed) TEST_COMPOSED).scale(1.1f, 1000); + fail("Max amplitude above threshold, should throw IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testScaleAppliesSameAdjustmentsOnAllEffects() { + VibrationEffect.OneShot oneShot = new VibrationEffect.OneShot(TEST_TIMING, TEST_AMPLITUDE); + VibrationEffect.Waveform waveform = new VibrationEffect.Waveform( + new long[] { TEST_TIMING }, new int[]{ TEST_AMPLITUDE }, -1); + VibrationEffect.Composed composed = + (VibrationEffect.Composed) VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, TEST_AMPLITUDE / 255f) + .compose(); + + assertEquals(oneShot.scale(2f, 128).getAmplitude(), + waveform.scale(2f, 128).getAmplitudes()[0]); + assertEquals(oneShot.scale(2f, 128).getAmplitude() / 255f, // convert amplitude to scale + composed.scale(2f, 128).getPrimitiveEffects().get(0).scale, + SCALE_TOLERANCE); + } private Resources mockRingtoneResources() { return mockRingtoneResources(new String[] { @@ -172,9 +249,22 @@ public class VibrationEffectTest { return mockResources; } - private Context mockContext(Resources r) { - Context ctx = mock(Context.class); - when(ctx.getResources()).thenReturn(r); - return ctx; + private Context mockContext(Resources resources) { + Context context = mock(Context.class); + ContentInterface contentInterface = mock(ContentInterface.class); + ContentResolver contentResolver = ContentResolver.wrap(contentInterface); + + try { + // ContentResolver#uncanonicalize is final, so we need to mock the ContentInterface it + // delegates the call to for the tests that require matching with the mocked URIs. + when(contentInterface.uncanonicalize(any())).then( + invocation -> invocation.getArgument(0)); + when(context.getContentResolver()).thenReturn(contentResolver); + when(context.getResources()).thenReturn(resources); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + + return context; } } diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java index ac4a42ca7024e..e066d99147ba2 100644 --- a/services/core/java/com/android/server/VibratorService.java +++ b/services/core/java/com/android/server/VibratorService.java @@ -1034,6 +1034,9 @@ public class VibratorService extends IVibratorService.Stub VibrationEffect.Waveform waveform = (VibrationEffect.Waveform) vib.effect; waveform = waveform.resolve(mDefaultVibrationAmplitude); scaledEffect = waveform.scale(scale.gamma, scale.maxAmplitude); + } else if (vib.effect instanceof VibrationEffect.Composed) { + VibrationEffect.Composed composed = (VibrationEffect.Composed) vib.effect; + scaledEffect = composed.scale(scale.gamma, scale.maxAmplitude); } else { Slog.w(TAG, "Unable to apply intensity scaling, unknown VibrationEffect type"); }