From 68a199f60dd4a79fe7041a9538ea5268d754dd5e Mon Sep 17 00:00:00 2001 From: Lais Andrade Date: Mon, 22 Mar 2021 16:13:54 +0000 Subject: [PATCH] Implement PWLE support Implement composePwle method and related frequency control getter introduced to the IVibrator interface. Bug: 167947076 Test: VibratorControllerTest and VibraionThreadTest Change-Id: If15f47fae33bce0bcb916e84af0c6712068f3a00 --- core/java/android/os/VibratorInfo.java | 64 +++++++++- .../src/android/os/VibratorInfoTest.java | 21 +++- .../DeviceVibrationEffectAdapter.java | 102 ++++++++++++---- .../server/vibrator/VibrationThread.java | 12 +- .../server/vibrator/VibratorController.java | 37 ++++-- .../vibrator/VibratorManagerService.java | 57 ++++++--- ...oid_server_vibrator_VibratorController.cpp | 115 ++++++++++++++++-- .../DeviceVibrationEffectAdapterTest.java | 64 +++++++++- .../FakeVibratorControllerProvider.java | 63 ++++++++-- .../server/vibrator/VibrationThreadTest.java | 48 ++++++++ .../vibrator/VibratorControllerTest.java | 16 ++- 11 files changed, 510 insertions(+), 89 deletions(-) diff --git a/core/java/android/os/VibratorInfo.java b/core/java/android/os/VibratorInfo.java index 64e51e71962a3..671daa0f0f819 100644 --- a/core/java/android/os/VibratorInfo.java +++ b/core/java/android/os/VibratorInfo.java @@ -19,6 +19,7 @@ package android.os; import android.annotation.FloatRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.hardware.vibrator.Braking; import android.hardware.vibrator.IVibrator; import android.util.Log; import android.util.MathUtils; @@ -45,6 +46,8 @@ public final class VibratorInfo implements Parcelable { @Nullable private final SparseBooleanArray mSupportedEffects; @Nullable + private final SparseBooleanArray mSupportedBraking; + @Nullable private final SparseBooleanArray mSupportedPrimitives; private final float mQFactor; private final FrequencyMapping mFrequencyMapping; @@ -53,17 +56,19 @@ public final class VibratorInfo implements Parcelable { mId = in.readInt(); mCapabilities = in.readLong(); mSupportedEffects = in.readSparseBooleanArray(); + mSupportedBraking = in.readSparseBooleanArray(); mSupportedPrimitives = in.readSparseBooleanArray(); mQFactor = in.readFloat(); mFrequencyMapping = in.readParcelable(VibratorInfo.class.getClassLoader()); } /** @hide */ - public VibratorInfo(int id, long capabilities, int[] supportedEffects, + public VibratorInfo(int id, long capabilities, int[] supportedEffects, int[] supportedBraking, int[] supportedPrimitives, float qFactor, @NonNull FrequencyMapping frequencyMapping) { mId = id; mCapabilities = capabilities; mSupportedEffects = toSparseBooleanArray(supportedEffects); + mSupportedBraking = toSparseBooleanArray(supportedBraking); mSupportedPrimitives = toSparseBooleanArray(supportedPrimitives); mQFactor = qFactor; mFrequencyMapping = frequencyMapping; @@ -74,6 +79,7 @@ public final class VibratorInfo implements Parcelable { dest.writeInt(mId); dest.writeLong(mCapabilities); dest.writeSparseBooleanArray(mSupportedEffects); + dest.writeSparseBooleanArray(mSupportedBraking); dest.writeSparseBooleanArray(mSupportedPrimitives); dest.writeFloat(mQFactor); dest.writeParcelable(mFrequencyMapping, flags); @@ -95,6 +101,7 @@ public final class VibratorInfo implements Parcelable { VibratorInfo that = (VibratorInfo) o; return mId == that.mId && mCapabilities == that.mCapabilities && Objects.equals(mSupportedEffects, that.mSupportedEffects) + && Objects.equals(mSupportedBraking, that.mSupportedBraking) && Objects.equals(mSupportedPrimitives, that.mSupportedPrimitives) && Objects.equals(mQFactor, that.mQFactor) && Objects.equals(mFrequencyMapping, that.mFrequencyMapping); @@ -102,8 +109,8 @@ public final class VibratorInfo implements Parcelable { @Override public int hashCode() { - return Objects.hash(mId, mCapabilities, mSupportedEffects, mSupportedPrimitives, - mQFactor, mFrequencyMapping); + return Objects.hash(mId, mCapabilities, mSupportedEffects, mSupportedBraking, + mSupportedPrimitives, mQFactor, mFrequencyMapping); } @Override @@ -113,6 +120,7 @@ public final class VibratorInfo implements Parcelable { + ", mCapabilities=" + Arrays.toString(getCapabilitiesNames()) + ", mCapabilities flags=" + Long.toBinaryString(mCapabilities) + ", mSupportedEffects=" + Arrays.toString(getSupportedEffectsNames()) + + ", mSupportedBraking=" + Arrays.toString(getSupportedBrakingNames()) + ", mSupportedPrimitives=" + Arrays.toString(getSupportedPrimitivesNames()) + ", mQFactor=" + mQFactor + ", mFrequencyMapping=" + mFrequencyMapping @@ -133,6 +141,23 @@ public final class VibratorInfo implements Parcelable { return hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL); } + /** + * Returns a default value to be applied to composed PWLE effects for braking. + * + * @return a supported braking value, one of android.hardware.vibrator.Braking.* + */ + public int getDefaultBraking() { + if (mSupportedBraking != null) { + int size = mSupportedBraking.size(); + for (int i = 0; i < size; i++) { + if (mSupportedBraking.keyAt(i) != Braking.NONE) { + return mSupportedBraking.keyAt(i); + } + } + } + return Braking.NONE; + } + /** * Query whether the vibrator supports the given effect. * @@ -147,7 +172,7 @@ public final class VibratorInfo implements Parcelable { if (mSupportedEffects == null) { return Vibrator.VIBRATION_EFFECT_SUPPORT_UNKNOWN; } - return mSupportedEffects.get(effectId, false) ? Vibrator.VIBRATION_EFFECT_SUPPORT_YES + return mSupportedEffects.get(effectId) ? Vibrator.VIBRATION_EFFECT_SUPPORT_YES : Vibrator.VIBRATION_EFFECT_SUPPORT_NO; } @@ -160,7 +185,7 @@ public final class VibratorInfo implements Parcelable { public boolean isPrimitiveSupported( @VibrationEffect.Composition.PrimitiveType int primitiveId) { return hasCapability(IVibrator.CAP_COMPOSE_EFFECTS) && mSupportedPrimitives != null - && mSupportedPrimitives.get(primitiveId, false); + && mSupportedPrimitives.get(primitiveId); } /** @@ -251,12 +276,18 @@ public final class VibratorInfo implements Parcelable { if (hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) { names.add("COMPOSE_EFFECTS"); } + if (hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS)) { + names.add("COMPOSE_PWLE_EFFECTS"); + } if (hasCapability(IVibrator.CAP_ALWAYS_ON_CONTROL)) { names.add("ALWAYS_ON_CONTROL"); } if (hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL)) { names.add("AMPLITUDE_CONTROL"); } + if (hasCapability(IVibrator.CAP_FREQUENCY_CONTROL)) { + names.add("FREQUENCY_CONTROL"); + } if (hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) { names.add("EXTERNAL_CONTROL"); } @@ -277,6 +308,26 @@ public final class VibratorInfo implements Parcelable { return names; } + private String[] getSupportedBrakingNames() { + if (mSupportedBraking == null) { + return new String[0]; + } + String[] names = new String[mSupportedBraking.size()]; + for (int i = 0; i < mSupportedBraking.size(); i++) { + switch (mSupportedBraking.keyAt(i)) { + case Braking.NONE: + names[i] = "NONE"; + break; + case Braking.CLAB: + names[i] = "CLAB"; + break; + default: + names[i] = Integer.toString(mSupportedBraking.keyAt(i)); + } + } + return names; + } + private String[] getSupportedPrimitivesNames() { if (mSupportedPrimitives == null) { return new String[0]; @@ -478,7 +529,8 @@ public final class VibratorInfo implements Parcelable { @Override public String toString() { return "FrequencyMapping{" - + "mMinFrequency=" + mMinFrequencyHz + + "mRelativeFrequencyRange=" + mRelativeFrequencyRange + + ", mMinFrequency=" + mMinFrequencyHz + ", mResonantFrequency=" + mResonantFrequencyHz + ", mMaxFrequency=" + (mMinFrequencyHz + mFrequencyResolutionHz * (mMaxAmplitudes.length - 1)) diff --git a/core/tests/coretests/src/android/os/VibratorInfoTest.java b/core/tests/coretests/src/android/os/VibratorInfoTest.java index 40fc00a971222..2521f7551a3c6 100644 --- a/core/tests/coretests/src/android/os/VibratorInfoTest.java +++ b/core/tests/coretests/src/android/os/VibratorInfoTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; +import android.hardware.vibrator.Braking; import android.hardware.vibrator.IVibrator; import android.platform.test.annotations.Presubmit; import android.util.Range; @@ -97,6 +98,16 @@ public class VibratorInfoTest { assertFalse(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK)); } + @Test + public void testGetDefaultBraking_returnsFirstSupportedBraking() { + assertEquals(Braking.NONE, new InfoBuilder().build().getDefaultBraking()); + assertEquals(Braking.CLAB, + new InfoBuilder() + .setSupportedBraking(Braking.NONE, Braking.CLAB) + .build() + .getDefaultBraking()); + } + @Test public void testGetFrequencyRange_invalidFrequencyMappingReturnsEmptyRange() { // Invalid, contains NaN values or empty array. @@ -318,6 +329,7 @@ public class VibratorInfoTest { private int mId = 0; private int mCapabilities = 0; private int[] mSupportedEffects = null; + private int[] mSupportedBraking = null; private int[] mSupportedPrimitives = null; private float mQFactor = Float.NaN; private VibratorInfo.FrequencyMapping mFrequencyMapping = EMPTY_FREQUENCY_MAPPING; @@ -337,6 +349,11 @@ public class VibratorInfoTest { return this; } + public InfoBuilder setSupportedBraking(int... supportedBraking) { + mSupportedBraking = supportedBraking; + return this; + } + public InfoBuilder setSupportedPrimitives(int... supportedPrimitives) { mSupportedPrimitives = supportedPrimitives; return this; @@ -353,8 +370,8 @@ public class VibratorInfoTest { } public VibratorInfo build() { - return new VibratorInfo(mId, mCapabilities, mSupportedEffects, mSupportedPrimitives, - mQFactor, mFrequencyMapping); + return new VibratorInfo(mId, mCapabilities, mSupportedEffects, mSupportedBraking, + mSupportedPrimitives, mQFactor, mFrequencyMapping); } } } diff --git a/services/core/java/com/android/server/vibrator/DeviceVibrationEffectAdapter.java b/services/core/java/com/android/server/vibrator/DeviceVibrationEffectAdapter.java index 953837a566982..7f2b07b6f367b 100644 --- a/services/core/java/com/android/server/vibrator/DeviceVibrationEffectAdapter.java +++ b/services/core/java/com/android/server/vibrator/DeviceVibrationEffectAdapter.java @@ -16,6 +16,7 @@ package com.android.server.vibrator; +import android.hardware.vibrator.IVibrator; import android.os.VibrationEffect; import android.os.VibratorInfo; import android.os.vibrator.RampSegment; @@ -30,25 +31,31 @@ import java.util.List; /** Adapts a {@link VibrationEffect} to a specific device, taking into account its capabilities. */ final class DeviceVibrationEffectAdapter implements VibrationEffectModifier { - /** - * Adapts a sequence of {@link VibrationEffectSegment} to device's absolute frequency values - * and respective supported amplitudes. - * - *

This adapter preserves the segment count. - */ - interface AmplitudeFrequencyAdapter { - List apply(List segments, - VibratorInfo info); + /** Adapts a sequence of {@link VibrationEffectSegment} to device's capabilities. */ + interface SegmentsAdapter { + + /** + * Modifies the given segments list by adding/removing segments to it based on the + * device capabilities specified by given {@link VibratorInfo}. + * + * @param segments List of {@link VibrationEffectSegment} to be adapter to the device. + * @param repeatIndex Repeat index on the current segment list. + * @param info The device vibrator info that the segments must be adapted to. + * @return The new repeat index on the modifies list. + */ + int apply(List segments, int repeatIndex, VibratorInfo info); } - private final AmplitudeFrequencyAdapter mAmplitudeFrequencyAdapter; + private final SegmentsAdapter mAmplitudeFrequencyAdapter; + private final SegmentsAdapter mStepToRampAdapter; DeviceVibrationEffectAdapter() { this(new ClippingAmplitudeFrequencyAdapter()); } - DeviceVibrationEffectAdapter(AmplitudeFrequencyAdapter amplitudeFrequencyAdapter) { + DeviceVibrationEffectAdapter(SegmentsAdapter amplitudeFrequencyAdapter) { mAmplitudeFrequencyAdapter = amplitudeFrequencyAdapter; + mStepToRampAdapter = new StepToRampAdapter(); } @Override @@ -58,14 +65,62 @@ final class DeviceVibrationEffectAdapter implements VibrationEffectModifier mappedSegments = mAmplitudeFrequencyAdapter.apply( - composed.getSegments(), info); + List newSegments = new ArrayList<>(composed.getSegments()); + int newRepeatIndex = composed.getRepeatIndex(); - // TODO(b/167947076): add ramp to step adapter once PWLE capability is introduced + // Maps steps that should be handled by PWLE to ramps. + // This should be done before frequency is converted from relative to absolute values. + newRepeatIndex = mStepToRampAdapter.apply(newSegments, newRepeatIndex, info); + + // Adapt amplitude and frequency values to device supported ones, converting frequency + // to absolute values in Hertz. + newRepeatIndex = mAmplitudeFrequencyAdapter.apply(newSegments, newRepeatIndex, info); + + // TODO(b/167947076): add ramp to step adapter // TODO(b/167947076): add filter that removes unsupported primitives // TODO(b/167947076): add filter that replaces unsupported prebaked with fallback - return new VibrationEffect.Composed(mappedSegments, composed.getRepeatIndex()); + return new VibrationEffect.Composed(newSegments, newRepeatIndex); + } + + /** + * Adapter that converts step segments that should be handled as PWLEs to ramp segments. + * + *

This leves the list unchanged if the device do not have compose PWLE capability. + */ + private static final class StepToRampAdapter implements SegmentsAdapter { + @Override + public int apply(List segments, int repeatIndex, + VibratorInfo info) { + if (!info.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS)) { + // The vibrator do not have PWLE capability, so keep the segments unchanged. + return repeatIndex; + } + int segmentCount = segments.size(); + // Convert steps that require frequency control to ramps. + for (int i = 0; i < segmentCount; i++) { + VibrationEffectSegment segment = segments.get(i); + if ((segment instanceof StepSegment) + && ((StepSegment) segment).getFrequency() != 0) { + segments.set(i, apply((StepSegment) segment)); + } + } + // Convert steps that are next to ramps to also become ramps, so they can be composed + // together in the same PWLE waveform. + for (int i = 1; i < segmentCount; i++) { + if (segments.get(i) instanceof RampSegment) { + for (int j = i - 1; j >= 0 && (segments.get(j) instanceof StepSegment); j--) { + segments.set(j, apply((StepSegment) segments.get(j))); + } + } + } + return repeatIndex; + } + + private RampSegment apply(StepSegment segment) { + return new RampSegment(segment.getAmplitude(), segment.getAmplitude(), + segment.getFrequency(), segment.getFrequency(), (int) segment.getDuration()); + } } /** @@ -74,25 +129,24 @@ final class DeviceVibrationEffectAdapter implements VibrationEffectModifierDevices with no frequency control will collapse all frequencies to zero and leave * amplitudes unchanged. + * + *

The frequency value returned in segments will be absolute, conveted with + * {@link VibratorInfo#getAbsoluteFrequency(float)}. */ - private static final class ClippingAmplitudeFrequencyAdapter - implements AmplitudeFrequencyAdapter { + private static final class ClippingAmplitudeFrequencyAdapter implements SegmentsAdapter { @Override - public List apply(List segments, + public int apply(List segments, int repeatIndex, VibratorInfo info) { - List result = new ArrayList<>(); int segmentCount = segments.size(); for (int i = 0; i < segmentCount; i++) { VibrationEffectSegment segment = segments.get(i); if (segment instanceof StepSegment) { - result.add(apply((StepSegment) segment, info)); + segments.set(i, apply((StepSegment) segment, info)); } else if (segment instanceof RampSegment) { - result.add(apply((RampSegment) segment, info)); - } else { - result.add(segment); + segments.set(i, apply((RampSegment) segment, info)); } } - return result; + return repeatIndex; } private StepSegment apply(StepSegment segment, VibratorInfo info) { diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java index dccf8c7858cc9..df85e262bd5dd 100644 --- a/services/core/java/com/android/server/vibrator/VibrationThread.java +++ b/services/core/java/com/android/server/vibrator/VibrationThread.java @@ -280,7 +280,6 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { vibratorOffTimeout); } if (segment instanceof RampSegment) { - // TODO(b/167947076): check capabilities to play steps with PWLE once APIs introduced return new ComposePwleStep(latestStartTime, controller, effect, segmentIndex, vibratorOffTimeout); } @@ -828,14 +827,14 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { } if (DEBUG) { - Slog.d(TAG, "Compose " + primitives.size() + " primitives on vibrator " + Slog.d(TAG, "Compose " + primitives + " primitives on vibrator " + controller.getVibratorInfo().getId()); } mVibratorOnResult = controller.on( primitives.toArray(new PrimitiveSegment[primitives.size()]), mVibration.id); - return nextSteps(/* segmntsPlayed= */ primitives.size()); + return nextSteps(/* segmentsPlayed= */ primitives.size()); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); } @@ -865,11 +864,6 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { VibrationEffectSegment segment = effect.getSegments().get(i); if (segment instanceof RampSegment) { pwles.add((RampSegment) segment); - } else if (segment instanceof StepSegment) { - StepSegment stepSegment = (StepSegment) segment; - pwles.add(new RampSegment(stepSegment.getAmplitude(), - stepSegment.getAmplitude(), stepSegment.getFrequency(), - stepSegment.getFrequency(), (int) stepSegment.getDuration())); } else { break; } @@ -882,7 +876,7 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { } if (DEBUG) { - Slog.d(TAG, "Compose " + pwles.size() + " PWLEs on vibrator " + Slog.d(TAG, "Compose " + pwles + " PWLEs on vibrator " + controller.getVibratorInfo().getId()); } mVibratorOnResult = controller.on(pwles.toArray(new RampSegment[pwles.size()]), diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java index 7cb3140ecb1a3..a09bfc57ba233 100644 --- a/services/core/java/com/android/server/vibrator/VibratorController.java +++ b/services/core/java/com/android/server/vibrator/VibratorController.java @@ -66,7 +66,8 @@ final class VibratorController { NativeWrapper nativeWrapper) { mNativeWrapper = nativeWrapper; mNativeWrapper.init(vibratorId, listener); - mVibratorInfo = mNativeWrapper.getInfo(); + // TODO(b/167947076): load suggested range from config + mVibratorInfo = mNativeWrapper.getInfo(/* suggestedFrequencyRange= */ 100); Preconditions.checkNotNull(mVibratorInfo, "Failed to retrieve data for vibrator %d", vibratorId); } @@ -251,8 +252,17 @@ final class VibratorController { * @return The duration of the effect playing, or 0 if unsupported. */ public long on(RampSegment[] primitives, long vibrationId) { - // TODO(b/167947076): forward to the HAL once APIs are introduced - return 0; + if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS)) { + return 0; + } + synchronized (mLock) { + int braking = mVibratorInfo.getDefaultBraking(); + long duration = mNativeWrapper.composePwle(primitives, braking, vibrationId); + if (duration > 0) { + notifyVibratorOnLocked(); + } + return duration; + } } /** Turns off the vibrator.This will affect the state of {@link #isVibrating()}. */ @@ -337,13 +347,21 @@ final class VibratorController { private static native void setAmplitude(long nativePtr, float amplitude); private static native long performEffect(long nativePtr, long effect, long strength, long vibrationId); + private static native long performComposedEffect(long nativePtr, PrimitiveSegment[] effect, long vibrationId); + + private static native long performPwleEffect(long nativePtr, RampSegment[] effect, + int braking, long vibrationId); + private static native void setExternalControl(long nativePtr, boolean enabled); + private static native void alwaysOnEnable(long nativePtr, long id, long effect, long strength); + private static native void alwaysOnDisable(long nativePtr, long id); - private static native VibratorInfo getInfo(long nativePtr); + + private static native VibratorInfo getInfo(long nativePtr, float suggestedFrequencyRange); private long mNativePtr = 0; @@ -385,11 +403,16 @@ final class VibratorController { return performEffect(mNativePtr, effect, strength, vibrationId); } - /** Turns vibrator on to perform one of the supported composed effects. */ + /** Turns vibrator on to perform effect composed of give primitives effect. */ public long compose(PrimitiveSegment[] primitives, long vibrationId) { return performComposedEffect(mNativePtr, primitives, vibrationId); } + /** Turns vibrator on to perform PWLE effect composed of given primitives. */ + public long composePwle(RampSegment[] primitives, int braking, long vibrationId) { + return performPwleEffect(mNativePtr, primitives, braking, vibrationId); + } + /** Enabled the device vibrator to be controlled by another service. */ public void setExternalControl(boolean enabled) { setExternalControl(mNativePtr, enabled); @@ -406,8 +429,8 @@ final class VibratorController { } /** Return device vibrator metadata. */ - public VibratorInfo getInfo() { - return getInfo(mNativePtr); + public VibratorInfo getInfo(float suggestedFrequencyRange) { + return getInfo(mNativePtr, suggestedFrequencyRange); } } } diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index f28a559478829..b1d6050f82c2d 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -1540,6 +1540,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private void addWaveformToComposition(VibrationEffect.Composition composition) { boolean hasAmplitudes = false; + boolean hasFrequencies = false; + boolean isContinuous = false; int repeat = -1; int delay = 0; @@ -1552,35 +1554,49 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { repeat = Integer.parseInt(getNextArgRequired()); } else if ("-w".equals(nextOption)) { delay = Integer.parseInt(getNextArgRequired()); + } else if ("-f".equals(nextOption)) { + hasFrequencies = true; + } else if ("-c".equals(nextOption)) { + isContinuous = true; } } - List durations = new ArrayList<>(); - List amplitudes = new ArrayList<>(); - VibrationEffect waveform; + List durations = new ArrayList<>(); + List amplitudes = new ArrayList<>(); + List frequencies = new ArrayList<>(); + float nextAmplitude = 0; String nextArg; while ((nextArg = peekNextArg()) != null) { try { - durations.add(Long.parseLong(nextArg)); + durations.add(Integer.parseInt(nextArg)); getNextArgRequired(); // consume the duration } catch (NumberFormatException e) { // nextArg is not a duration, finish reading. break; } if (hasAmplitudes) { - amplitudes.add(Integer.parseInt(getNextArgRequired())); + amplitudes.add( + Float.parseFloat(getNextArgRequired()) / VibrationEffect.MAX_AMPLITUDE); + } else { + amplitudes.add(nextAmplitude); + nextAmplitude = 1 - nextAmplitude; + } + if (hasFrequencies) { + frequencies.add(Float.parseFloat(getNextArgRequired())); + } else { + frequencies.add(0f); } } - long[] durationArray = durations.stream().mapToLong(Long::longValue).toArray(); - if (hasAmplitudes) { - int[] amplitudeArray = amplitudes.stream().mapToInt(Integer::intValue).toArray(); - waveform = VibrationEffect.createWaveform(durationArray, amplitudeArray, repeat); - } else { - waveform = VibrationEffect.createWaveform(durationArray, repeat); + VibrationEffect.WaveformBuilder waveform = VibrationEffect.startWaveform(); + for (int i = 0; i < durations.size(); i++) { + if (isContinuous) { + waveform.addRamp(amplitudes.get(i), frequencies.get(i), durations.get(i)); + } else { + waveform.addStep(amplitudes.get(i), frequencies.get(i), durations.get(i)); + } } - - composition.addEffect(waveform, delay); + composition.addEffect(waveform.build(repeat), delay); } private void addPrebakedToComposition(VibrationEffect.Composition composition) { @@ -1659,7 +1675,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { pw.println(" wait time in milliseconds."); pw.println(" If -a is provided, the command accepts a second argument for "); pw.println(" amplitude, in a scale of 1-255."); - pw.println(" waveform [-w delay] [-r index] [-a] ( [])..."); + pw.print(" waveform [-w delay] [-r index] [-a] [-f] [-c] "); + pw.println("( [] [])..."); pw.println(" Vibrates for durations and amplitudes in list; ignored when "); pw.println(" device is on DND (Do Not Disturb) mode; touch feedback strength "); pw.println(" user setting will be used to scale amplitude."); @@ -1667,9 +1684,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { pw.println(" wait time in milliseconds."); pw.println(" If -r is provided, the waveform loops back to the specified"); pw.println(" index (e.g. 0 loops from the beginning)"); - pw.println(" If -a is provided, the command accepts duration-amplitude pairs;"); - pw.println(" otherwise, it accepts durations only and alternates off/on"); - pw.println(" Duration is in milliseconds; amplitude is a scale of 1-255."); + pw.println(" If -a is provided, the command expects amplitude to follow each"); + pw.println(" duration; otherwise, it accepts durations only and alternates"); + pw.println(" off/on"); + pw.println(" If -f is provided, the command expects frequency to follow each"); + pw.println(" amplitude or duration; otherwise, it uses resonant frequency"); + pw.println(" If -c is provided, the waveform is continuous and will ramp"); + pw.println(" between values; otherwise each entry is a fixed step."); + pw.println(" Duration is in milliseconds; amplitude is a scale of 1-255;"); + pw.println(" frequency is a relative value around resonant frequency 0;"); pw.println(" prebaked [-w delay] [-b] "); pw.println(" Vibrates with prebaked effect; ignored when device is on DND "); pw.println(" (Do Not Disturb) mode; touch feedback strength user setting "); diff --git a/services/core/jni/com_android_server_vibrator_VibratorController.cpp b/services/core/jni/com_android_server_vibrator_VibratorController.cpp index 11fd8ffb8e9b6..cf64a6847b463 100644 --- a/services/core/jni/com_android_server_vibrator_VibratorController.cpp +++ b/services/core/jni/com_android_server_vibrator_VibratorController.cpp @@ -48,6 +48,13 @@ static struct { jfieldID scale; jfieldID delay; } sPrimitiveClassInfo; +static struct { + jfieldID startAmplitude; + jfieldID endAmplitude; + jfieldID startFrequency; + jfieldID endFrequency; + jfieldID duration; +} sRampClassInfo; static_assert(static_cast(V1_0::EffectStrength::LIGHT) == static_cast(aidl::EffectStrength::LIGHT)); @@ -127,6 +134,37 @@ private: const jobject mCallbackListener; }; +static aidl::BrakingPwle brakingPwle(aidl::Braking braking, int32_t duration) { + aidl::BrakingPwle pwle; + pwle.braking = braking; + pwle.duration = duration; + return pwle; +} + +static aidl::ActivePwle activePwleFromJavaPrimitive(JNIEnv* env, jobject ramp) { + aidl::ActivePwle pwle; + pwle.startAmplitude = + static_cast(env->GetFloatField(ramp, sRampClassInfo.startAmplitude)); + pwle.endAmplitude = static_cast(env->GetFloatField(ramp, sRampClassInfo.endAmplitude)); + pwle.startFrequency = + static_cast(env->GetFloatField(ramp, sRampClassInfo.startFrequency)); + pwle.endFrequency = static_cast(env->GetFloatField(ramp, sRampClassInfo.endFrequency)); + pwle.duration = static_cast(env->GetIntField(ramp, sRampClassInfo.duration)); + return pwle; +} + +/* Return true if braking is not NONE and the active PWLE starts and ends with zero amplitude. */ +static bool shouldBeReplacedWithBraking(aidl::ActivePwle activePwle, aidl::Braking braking) { + return (braking != aidl::Braking::NONE) && (activePwle.startAmplitude == 0) && + (activePwle.endAmplitude == 0); +} + +/* Return true if braking is not NONE and the active PWLE only ends with zero amplitude. */ +static bool shouldAddLastBraking(aidl::ActivePwle lastActivePwle, aidl::Braking braking) { + return (braking != aidl::Braking::NONE) && (lastActivePwle.startAmplitude > 0) && + (lastActivePwle.endAmplitude == 0); +} + static aidl::CompositeEffect effectFromJavaPrimitive(JNIEnv* env, jobject primitive) { aidl::CompositeEffect effect; effect.primitive = static_cast( @@ -254,6 +292,40 @@ static jlong vibratorPerformComposedEffect(JNIEnv* env, jclass /* clazz */, jlon return result.isOk() ? result.value().count() : (result.isUnsupported() ? 0 : -1); } +static jlong vibratorPerformPwleEffect(JNIEnv* env, jclass /* clazz */, jlong ptr, + jobjectArray waveform, jint brakingId, jlong vibrationId) { + VibratorControllerWrapper* wrapper = reinterpret_cast(ptr); + if (wrapper == nullptr) { + ALOGE("vibratorPerformPwleEffect failed because native wrapper was not initialized"); + return -1; + } + aidl::Braking braking = static_cast(brakingId); + size_t size = env->GetArrayLength(waveform); + std::vector primitives; + std::chrono::milliseconds totalDuration(0); + for (size_t i = 0; i < size; i++) { + jobject element = env->GetObjectArrayElement(waveform, i); + aidl::ActivePwle activePwle = activePwleFromJavaPrimitive(env, element); + if ((i > 0) && shouldBeReplacedWithBraking(activePwle, braking)) { + primitives.push_back(brakingPwle(braking, activePwle.duration)); + } else { + primitives.push_back(activePwle); + } + totalDuration += std::chrono::milliseconds(activePwle.duration); + + if ((i == (size - 1)) && shouldAddLastBraking(activePwle, braking)) { + primitives.push_back(brakingPwle(braking, 0 /* duration */)); + } + } + + auto callback = wrapper->createCallback(vibrationId); + auto performPwleEffectFn = [&](std::shared_ptr hal) { + return hal->performPwleEffect(primitives, callback); + }; + auto result = wrapper->halCall(performPwleEffectFn, "performPwleEffect"); + return result.isOk() ? totalDuration.count() : (result.isUnsupported() ? 0 : -1); +} + static void vibratorAlwaysOnEnable(JNIEnv* env, jclass /* clazz */, jlong ptr, jlong id, jlong effect, jlong strength) { VibratorControllerWrapper* wrapper = reinterpret_cast(ptr); @@ -280,7 +352,8 @@ static void vibratorAlwaysOnDisable(JNIEnv* env, jclass /* clazz */, jlong ptr, wrapper->halCall(alwaysOnDisableFn, "alwaysOnDisable"); } -static jobject vibratorGetInfo(JNIEnv* env, jclass /* clazz */, jlong ptr) { +static jobject vibratorGetInfo(JNIEnv* env, jclass /* clazz */, jlong ptr, + jfloat suggestedSafeRange) { VibratorControllerWrapper* wrapper = reinterpret_cast(ptr); if (wrapper == nullptr) { ALOGE("vibratorGetInfo failed because native wrapper was not initialized"); @@ -290,10 +363,14 @@ static jobject vibratorGetInfo(JNIEnv* env, jclass /* clazz */, jlong ptr) { jlong capabilities = static_cast(info.capabilities.valueOr(vibrator::Capabilities::NONE)); + jfloat minFrequency = static_cast(info.minFrequency.valueOr(NAN)); jfloat resonantFrequency = static_cast(info.resonantFrequency.valueOr(NAN)); + jfloat frequencyResolution = static_cast(info.frequencyResolution.valueOr(NAN)); jfloat qFactor = static_cast(info.qFactor.valueOr(NAN)); jintArray supportedEffects = nullptr; + jintArray supportedBraking = nullptr; jintArray supportedPrimitives = nullptr; + jfloatArray maxAmplitudes = nullptr; if (info.supportedEffects.isOk()) { std::vector effects = info.supportedEffects.value(); @@ -301,21 +378,32 @@ static jobject vibratorGetInfo(JNIEnv* env, jclass /* clazz */, jlong ptr) { env->SetIntArrayRegion(supportedEffects, 0, effects.size(), reinterpret_cast(effects.data())); } + if (info.supportedBraking.isOk()) { + std::vector braking = info.supportedBraking.value(); + supportedBraking = env->NewIntArray(braking.size()); + env->SetIntArrayRegion(supportedBraking, 0, braking.size(), + reinterpret_cast(braking.data())); + } if (info.supportedPrimitives.isOk()) { std::vector primitives = info.supportedPrimitives.value(); supportedPrimitives = env->NewIntArray(primitives.size()); env->SetIntArrayRegion(supportedPrimitives, 0, primitives.size(), reinterpret_cast(primitives.data())); } + if (info.maxAmplitudes.isOk()) { + std::vector amplitudes = info.maxAmplitudes.value(); + maxAmplitudes = env->NewFloatArray(amplitudes.size()); + env->SetFloatArrayRegion(maxAmplitudes, 0, amplitudes.size(), + reinterpret_cast(amplitudes.data())); + } - jobject frequencyMapping = - env->NewObject(sFrequencyMappingClass, sFrequencyMappingCtor, NAN /* minFrequencyHz*/, - resonantFrequency, NAN /* frequencyResolutionHz*/, - NAN /* suggestedSafeRangeHz */, nullptr /* maxAmplitudes */); + jobject frequencyMapping = env->NewObject(sFrequencyMappingClass, sFrequencyMappingCtor, + minFrequency, resonantFrequency, frequencyResolution, + suggestedSafeRange, maxAmplitudes); return env->NewObject(sVibratorInfoClass, sVibratorInfoCtor, wrapper->getVibratorId(), - capabilities, supportedEffects, supportedPrimitives, qFactor, - frequencyMapping); + capabilities, supportedEffects, supportedBraking, supportedPrimitives, + qFactor, frequencyMapping); } static const JNINativeMethod method_table[] = { @@ -330,10 +418,12 @@ static const JNINativeMethod method_table[] = { {"performEffect", "(JJJJ)J", (void*)vibratorPerformEffect}, {"performComposedEffect", "(J[Landroid/os/vibrator/PrimitiveSegment;J)J", (void*)vibratorPerformComposedEffect}, + {"performPwleEffect", "(J[Landroid/os/vibrator/RampSegment;IJ)J", + (void*)vibratorPerformPwleEffect}, {"setExternalControl", "(JZ)V", (void*)vibratorSetExternalControl}, {"alwaysOnEnable", "(JJJJ)V", (void*)vibratorAlwaysOnEnable}, {"alwaysOnDisable", "(JJ)V", (void*)vibratorAlwaysOnDisable}, - {"getInfo", "(J)Landroid/os/VibratorInfo;", (void*)vibratorGetInfo}, + {"getInfo", "(JF)Landroid/os/VibratorInfo;", (void*)vibratorGetInfo}, }; int register_android_server_vibrator_VibratorController(JavaVM* jvm, JNIEnv* env) { @@ -348,6 +438,13 @@ int register_android_server_vibrator_VibratorController(JavaVM* jvm, JNIEnv* env sPrimitiveClassInfo.scale = GetFieldIDOrDie(env, primitiveClass, "mScale", "F"); sPrimitiveClassInfo.delay = GetFieldIDOrDie(env, primitiveClass, "mDelay", "I"); + jclass rampClass = FindClassOrDie(env, "android/os/vibrator/RampSegment"); + sRampClassInfo.startAmplitude = GetFieldIDOrDie(env, rampClass, "mStartAmplitude", "F"); + sRampClassInfo.endAmplitude = GetFieldIDOrDie(env, rampClass, "mEndAmplitude", "F"); + sRampClassInfo.startFrequency = GetFieldIDOrDie(env, rampClass, "mStartFrequency", "F"); + sRampClassInfo.endFrequency = GetFieldIDOrDie(env, rampClass, "mEndFrequency", "F"); + sRampClassInfo.duration = GetFieldIDOrDie(env, rampClass, "mDuration", "I"); + jclass frequencyMappingClass = FindClassOrDie(env, "android/os/VibratorInfo$FrequencyMapping"); sFrequencyMappingClass = (jclass)env->NewGlobalRef(frequencyMappingClass); sFrequencyMappingCtor = GetMethodIDOrDie(env, sFrequencyMappingClass, "", "(FFFF[F)V"); @@ -355,7 +452,7 @@ int register_android_server_vibrator_VibratorController(JavaVM* jvm, JNIEnv* env jclass vibratorInfoClass = FindClassOrDie(env, "android/os/VibratorInfo"); sVibratorInfoClass = (jclass)env->NewGlobalRef(vibratorInfoClass); sVibratorInfoCtor = GetMethodIDOrDie(env, sVibratorInfoClass, "", - "(IJ[I[IFLandroid/os/VibratorInfo$FrequencyMapping;)V"); + "(IJ[I[I[IFLandroid/os/VibratorInfo$FrequencyMapping;)V"); return jniRegisterNativeMethods(env, "com/android/server/vibrator/VibratorController$NativeWrapper", diff --git a/services/tests/servicestests/src/com/android/server/vibrator/DeviceVibrationEffectAdapterTest.java b/services/tests/servicestests/src/com/android/server/vibrator/DeviceVibrationEffectAdapterTest.java index dcff4791a907a..c29593f4bd9af 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/DeviceVibrationEffectAdapterTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/DeviceVibrationEffectAdapterTest.java @@ -18,6 +18,7 @@ package com.android.server.vibrator; import static org.junit.Assert.assertEquals; +import android.hardware.vibrator.IVibrator; import android.os.VibrationEffect; import android.os.VibratorInfo; import android.os.vibrator.PrebakedSegment; @@ -30,6 +31,7 @@ import org.junit.Before; import org.junit.Test; import java.util.Arrays; +import java.util.stream.IntStream; /** * Tests for {@link DeviceVibrationEffectAdapter}. @@ -74,6 +76,60 @@ public class DeviceVibrationEffectAdapterTest { assertEquals(effect, mAdapter.apply(effect, createVibratorInfo(TEST_FREQUENCY_MAPPING))); } + @Test + public void testStepAndRampSegments_withPwleCapability_convertsStepsToRamps() { + VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList( + new StepSegment(/* amplitude= */ 0, /* frequency= */ 1, /* duration= */ 10), + new StepSegment(/* amplitude= */ 0.5f, /* frequency= */ 0, /* duration= */ 100), + new RampSegment(/* startAmplitude= */ 1, /* endAmplitude= */ 1, + /* startFrequency= */ -4, /* endFrequency= */ 2, /* duration= */ 50), + new RampSegment(/* startAmplitude= */ 0.8f, /* endAmplitude= */ 0.2f, + /* startFrequency= */ 10, /* endFrequency= */ -5, /* duration= */ 20)), + /* repeatIndex= */ 2); + + VibrationEffect.Composed expected = new VibrationEffect.Composed(Arrays.asList( + new RampSegment(/* startAmplitude= */ 0, /* endAmplitude*/ 0, + /* startFrequency= */ 175, /* endFrequency= */ 175, /* duration= */ 10), + new RampSegment(/* startAmplitude= */ 0.5f, /* endAmplitude= */ 0.5f, + /* startFrequency= */ 150, /* endFrequency= */ 150, /* duration= */ 100), + new RampSegment(/* startAmplitude= */ 0.1f, /* endAmplitude= */ 0.8f, + /* startFrequency= */ 50, /* endFrequency= */ 200, /* duration= */ 50), + new RampSegment(/* startAmplitude= */ 0.8f, /* endAmplitude= */ 0.1f, + /* startFrequency= */ 200, /* endFrequency= */ 50, /* duration= */ 20)), + /* repeatIndex= */ 2); + + VibratorInfo info = createVibratorInfo(TEST_FREQUENCY_MAPPING, + IVibrator.CAP_COMPOSE_PWLE_EFFECTS); + assertEquals(expected, mAdapter.apply(effect, info)); + } + + @Test + public void testStepAndRampSegments_withPwleCapabilityAndNoFrequency_keepsOriginalSteps() { + VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList( + new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, /* duration= */ 10), + new StepSegment(/* amplitude= */ 0.5f, /* frequency= */ 0, /* duration= */ 100), + new PrimitiveSegment(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 10), + new RampSegment(/* startAmplitude= */ 1, /* endAmplitude= */ 1, + /* startFrequency= */ -4, /* endFrequency= */ 2, /* duration= */ 50), + new RampSegment(/* startAmplitude= */ 0.8f, /* endAmplitude= */ 0.2f, + /* startFrequency= */ 10, /* endFrequency= */ -5, /* duration= */ 20)), + /* repeatIndex= */ 2); + + VibrationEffect.Composed expected = new VibrationEffect.Composed(Arrays.asList( + new StepSegment(/* amplitude= */ 0, /* frequency= */ 150, /* duration= */ 10), + new StepSegment(/* amplitude= */ 0.5f, /* frequency= */ 150, /* duration= */ 100), + new PrimitiveSegment(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 10), + new RampSegment(/* startAmplitude= */ 0.1f, /* endAmplitude= */ 0.8f, + /* startFrequency= */ 50, /* endFrequency= */ 200, /* duration= */ 50), + new RampSegment(/* startAmplitude= */ 0.8f, /* endAmplitude= */ 0.1f, + /* startFrequency= */ 200, /* endFrequency= */ 50, /* duration= */ 20)), + /* repeatIndex= */ 2); + + VibratorInfo info = createVibratorInfo(TEST_FREQUENCY_MAPPING, + IVibrator.CAP_COMPOSE_PWLE_EFFECTS); + assertEquals(expected, mAdapter.apply(effect, info)); + } + @Test public void testStepAndRampSegments_emptyMapping_returnsSameAmplitudesAndFrequencyZero() { VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList( @@ -123,8 +179,10 @@ public class DeviceVibrationEffectAdapterTest { assertEquals(expected, mAdapter.apply(effect, createVibratorInfo(TEST_FREQUENCY_MAPPING))); } - private static VibratorInfo createVibratorInfo(VibratorInfo.FrequencyMapping frequencyMapping) { - return new VibratorInfo(/* id= */ 0, /* capabilities= */ 0, null, null, - /* qFactor= */ Float.NaN, frequencyMapping); + private static VibratorInfo createVibratorInfo(VibratorInfo.FrequencyMapping frequencyMapping, + int... capabilities) { + int cap = IntStream.of(capabilities).reduce((a, b) -> a | b).orElse(0); + return new VibratorInfo(/* id= */ 0, cap, null, null, null, /* qFactor= */ Float.NaN, + frequencyMapping); } } diff --git a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java index aefd2175d00e3..1d715c830fcf8 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java @@ -23,6 +23,7 @@ import android.os.VibrationEffect; import android.os.VibratorInfo; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; +import android.os.vibrator.RampSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationEffectSegment; @@ -43,6 +44,7 @@ final class FakeVibratorControllerProvider { private final Map mEnabledAlwaysOnEffects = new HashMap<>(); private final List mEffectSegments = new ArrayList<>(); + private final List mBraking = new ArrayList<>(); private final List mAmplitudes = new ArrayList<>(); private final Handler mHandler; private final FakeNativeWrapper mNativeWrapper; @@ -52,9 +54,13 @@ final class FakeVibratorControllerProvider { private int mCapabilities; private int[] mSupportedEffects; + private int[] mSupportedBraking; private int[] mSupportedPrimitives; - private float mResonantFrequency; - private float mQFactor; + private float mMinFrequency = Float.NaN; + private float mResonantFrequency = Float.NaN; + private float mFrequencyResolution = Float.NaN; + private float mQFactor = Float.NaN; + private float[] mMaxAmplitudes; private final class FakeNativeWrapper extends VibratorController.NativeWrapper { public int vibratorId; @@ -116,6 +122,19 @@ final class FakeVibratorControllerProvider { return duration; } + @Override + public long composePwle(RampSegment[] primitives, int braking, long vibrationId) { + long duration = 0; + for (RampSegment primitive : primitives) { + duration += primitive.getDuration(); + mEffectSegments.add(primitive); + } + mBraking.add(braking); + applyLatency(); + scheduleListener(duration, vibrationId); + return duration; + } + @Override public void setExternalControl(boolean enabled) { } @@ -132,10 +151,11 @@ final class FakeVibratorControllerProvider { } @Override - public VibratorInfo getInfo() { + public VibratorInfo getInfo(float suggestedFrequencyRange) { VibratorInfo.FrequencyMapping frequencyMapping = new VibratorInfo.FrequencyMapping( - Float.NaN, mResonantFrequency, Float.NaN, Float.NaN, null); - return new VibratorInfo(vibratorId, mCapabilities, mSupportedEffects, + mMinFrequency, mResonantFrequency, mFrequencyResolution, + suggestedFrequencyRange, mMaxAmplitudes); + return new VibratorInfo(vibratorId, mCapabilities, mSupportedEffects, mSupportedBraking, mSupportedPrimitives, mQFactor, frequencyMapping); } @@ -198,6 +218,15 @@ final class FakeVibratorControllerProvider { mSupportedEffects = effects; } + /** Set the effects supported by the fake vibrator hardware. */ + public void setSupportedBraking(int... braking) { + if (braking != null) { + braking = Arrays.copyOf(braking, braking.length); + Arrays.sort(braking); + } + mSupportedBraking = braking; + } + /** Set the primitives supported by the fake vibrator hardware. */ public void setSupportedPrimitives(int... primitives) { if (primitives != null) { @@ -208,8 +237,18 @@ final class FakeVibratorControllerProvider { } /** Set the resonant frequency of the fake vibrator hardware. */ - public void setResonantFrequency(float resonantFrequency) { - mResonantFrequency = resonantFrequency; + public void setResonantFrequency(float frequencyHz) { + mResonantFrequency = frequencyHz; + } + + /** Set the minimum frequency of the fake vibrator hardware. */ + public void setMinFrequency(float frequencyHz) { + mMinFrequency = frequencyHz; + } + + /** Set the frequency resolution of the fake vibrator hardware. */ + public void setFrequencyResolution(float frequencyHz) { + mFrequencyResolution = frequencyHz; } /** Set the Q factor of the fake vibrator hardware. */ @@ -217,6 +256,11 @@ final class FakeVibratorControllerProvider { mQFactor = qFactor; } + /** Set the max amplitude supported for each frequency f the fake vibrator hardware. */ + public void setMaxAmplitudes(float... maxAmplitudes) { + mMaxAmplitudes = maxAmplitudes; + } + /** * Return the amplitudes set by this controller, including zeroes for each time the vibrator was * turned off. @@ -225,6 +269,11 @@ final class FakeVibratorControllerProvider { return new ArrayList<>(mAmplitudes); } + /** Return the braking values passed to the compose PWLE method. */ + public List getBraking() { + return mBraking; + } + /** Return list of {@link VibrationEffectSegment} played by this controller, in order. */ public List getEffectSegments() { return new ArrayList<>(mEffectSegments); diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java index 00ac55d7ad236..c439b9c56e74e 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java @@ -31,6 +31,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.hardware.vibrator.Braking; import android.hardware.vibrator.IVibrator; import android.hardware.vibrator.IVibratorManager; import android.os.CombinedVibrationEffect; @@ -43,6 +44,7 @@ import android.os.VibrationEffect; import android.os.test.TestLooper; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; +import android.os.vibrator.RampSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationEffectSegment; import android.platform.test.annotations.LargeTest; @@ -418,6 +420,42 @@ public class VibrationThreadTest { assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()); } + @Test + public void vibrate_singleVibratorPwle_runsComposePwle() throws Exception { + mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS); + mVibratorProviders.get(VIBRATOR_ID).setSupportedBraking(Braking.CLAB); + mVibratorProviders.get(VIBRATOR_ID).setMinFrequency(100); + mVibratorProviders.get(VIBRATOR_ID).setResonantFrequency(150); + mVibratorProviders.get(VIBRATOR_ID).setFrequencyResolution(50); + mVibratorProviders.get(VIBRATOR_ID).setMaxAmplitudes( + 0.5f /* 100Hz*/, 1 /* 150Hz */, 0.6f /* 200Hz */); + + long vibrationId = 1; + VibrationEffect effect = VibrationEffect.startWaveform() + .addStep(1, 10) + .addRamp(0, 20) + .addStep(0.8f, 1, 30) + .addRamp(0.6f, -1, 40) + .build(); + VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); + waitForCompletion(thread); + + verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(100L)); + verify(mIBatteryStatsMock).noteVibratorOff(eq(UID)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); + verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED)); + assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating()); + assertEquals(Arrays.asList( + expectedRamp(/* amplitude= */ 1, /* frequency= */ 150, /* duration= */ 10), + expectedRamp(/* StartAmplitude= */ 1, /* endAmplitude= */ 0, + /* startFrequency= */ 150, /* endFrequency= */ 150, /* duration= */ 20), + expectedRamp(/* amplitude= */ 0.6f, /* frequency= */ 200, /* duration= */ 30), + expectedRamp(/* StartAmplitude= */ 0.6f, /* endAmplitude= */ 0.5f, + /* startFrequency= */ 200, /* endFrequency= */ 100, /* duration= */ 40)), + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments()); + assertEquals(Arrays.asList(Braking.CLAB), mVibratorProviders.get(VIBRATOR_ID).getBraking()); + } + @Test public void vibrate_singleVibratorCancelled_vibratorStopped() throws Exception { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); @@ -969,6 +1007,16 @@ public class VibrationThreadTest { return new PrimitiveSegment(primitiveId, scale, delay); } + private VibrationEffectSegment expectedRamp(float amplitude, float frequency, int duration) { + return expectedRamp(amplitude, amplitude, frequency, frequency, duration); + } + + private VibrationEffectSegment expectedRamp(float startAmplitude, float endAmplitude, + float startFrequency, float endFrequency, int duration) { + return new RampSegment(startAmplitude, endAmplitude, startFrequency, endFrequency, + duration); + } + private List expectedAmplitudes(int... amplitudes) { return Arrays.stream(amplitudes) .mapToObj(amplitude -> amplitude / 255f) diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java index dd5da5a6f41ab..2e9aad11f16bc 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.notNull; @@ -34,6 +35,7 @@ import static org.mockito.Mockito.when; import android.content.ContentResolver; import android.content.ContextWrapper; +import android.hardware.vibrator.Braking; import android.hardware.vibrator.IVibrator; import android.os.IBinder; import android.os.IVibratorStateListener; @@ -226,15 +228,19 @@ public class VibratorControllerTest { } @Test - public void on_withComposedPwle_ignoresEffect() { + public void on_withComposedPwle_performsEffect() { + mockVibratorCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS); + when(mNativeWrapperMock.composePwle(any(), anyInt(), anyLong())).thenReturn(15L); VibratorController controller = createController(); RampSegment[] primitives = new RampSegment[]{ new RampSegment(/* startAmplitude= */ 0, /* endAmplitude= */ 1, /* startFrequency= */ -1, /* endFrequency= */ 1, /* duration= */ 10) }; - assertEquals(0L, controller.on(primitives, 12)); - assertFalse(controller.isVibrating()); + assertEquals(15L, controller.on(primitives, 12)); + assertTrue(controller.isVibrating()); + + verify(mNativeWrapperMock).composePwle(eq(primitives), eq(Braking.NONE), eq(12L)); } @Test @@ -291,8 +297,8 @@ public class VibratorControllerTest { private void mockVibratorCapabilities(int capabilities) { VibratorInfo.FrequencyMapping frequencyMapping = new VibratorInfo.FrequencyMapping( Float.NaN, Float.NaN, Float.NaN, Float.NaN, null); - when(mNativeWrapperMock.getInfo()).thenReturn( - new VibratorInfo(VIBRATOR_ID, capabilities, null, null, Float.NaN, + when(mNativeWrapperMock.getInfo(/* suggestedFrequencyRange= */ 100)).thenReturn( + new VibratorInfo(VIBRATOR_ID, capabilities, null, null, null, Float.NaN, frequencyMapping)); }