summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Ahmad Khalil <khalilahmad@google.com> 2024-11-26 13:44:41 +0000
committer Ahmad Khalil <khalilahmad@google.com> 2024-11-29 22:23:31 +0000
commit7dba6f419b2b6f063e8a2616da62caad8fef84da (patch)
tree8c441950fa60de31139b58c6e60ae0c1fd03c402
parentf26b150c127d21081e422f6854079e6db0a78f0f (diff)
Add xml serialization of envelope vibration effects
Introduce new tags to the vibration.xsd scheme to support waveform and basic envelope effects. Bug: 347035918 Flag: android.os.vibrator.normalized_pwle_effects Test: atest android.os.vibrator.persistence Change-Id: I7a7a7a492df13844a91c6320dfbe2d62cc01109e
-rw-r--r--core/java/com/android/internal/vibrator/persistence/SerializedBasicEnvelopeEffect.java184
-rw-r--r--core/java/com/android/internal/vibrator/persistence/SerializedWaveformEnvelopeEffect.java182
-rw-r--r--core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java40
-rw-r--r--core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java57
-rw-r--r--core/java/com/android/internal/vibrator/persistence/XmlConstants.java8
-rw-r--r--core/java/com/android/internal/vibrator/persistence/XmlReader.java66
-rw-r--r--core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java492
-rw-r--r--core/xsd/vibrator/vibration/schema/current.txt38
-rw-r--r--core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd56
-rw-r--r--core/xsd/vibrator/vibration/vibration.xsd56
10 files changed, 1177 insertions, 2 deletions
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedBasicEnvelopeEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedBasicEnvelopeEffect.java
new file mode 100644
index 000000000000..a090c7abc2db
--- /dev/null
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedBasicEnvelopeEffect.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2024 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 com.android.internal.vibrator.persistence;
+
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_DURATION_MS;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_INITIAL_SHARPNESS;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_INTENSITY;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_SHARPNESS;
+import static com.android.internal.vibrator.persistence.XmlConstants.NAMESPACE;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_BASIC_ENVELOPE_EFFECT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_CONTROL_POINT;
+
+import android.annotation.NonNull;
+import android.os.VibrationEffect;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Serialized representation of a basic envelope effect created via
+ * {@link VibrationEffect.BasicEnvelopeBuilder}.
+ *
+ * @hide
+ */
+final class SerializedBasicEnvelopeEffect implements SerializedComposedEffect.SerializedSegment {
+ private final BasicControlPoint[] mControlPoints;
+ private final float mInitialSharpness;
+
+ SerializedBasicEnvelopeEffect(BasicControlPoint[] controlPoints, float initialSharpness) {
+ mControlPoints = controlPoints;
+ mInitialSharpness = initialSharpness;
+ }
+
+ @Override
+ public void write(@NonNull TypedXmlSerializer serializer) throws IOException {
+ serializer.startTag(NAMESPACE, TAG_BASIC_ENVELOPE_EFFECT);
+
+ if (!Float.isNaN(mInitialSharpness)) {
+ serializer.attributeFloat(NAMESPACE, ATTRIBUTE_INITIAL_SHARPNESS, mInitialSharpness);
+ }
+
+ for (BasicControlPoint point : mControlPoints) {
+ serializer.startTag(NAMESPACE, TAG_CONTROL_POINT);
+ serializer.attributeFloat(NAMESPACE, ATTRIBUTE_INTENSITY, point.mIntensity);
+ serializer.attributeFloat(NAMESPACE, ATTRIBUTE_SHARPNESS, point.mSharpness);
+ serializer.attributeLong(NAMESPACE, ATTRIBUTE_DURATION_MS, point.mDurationMs);
+ serializer.endTag(NAMESPACE, TAG_CONTROL_POINT);
+ }
+
+ serializer.endTag(NAMESPACE, TAG_BASIC_ENVELOPE_EFFECT);
+ }
+
+ @Override
+ public void deserializeIntoComposition(@NonNull VibrationEffect.Composition composition) {
+ VibrationEffect.BasicEnvelopeBuilder builder = new VibrationEffect.BasicEnvelopeBuilder();
+
+ if (!Float.isNaN(mInitialSharpness)) {
+ builder.setInitialSharpness(mInitialSharpness);
+ }
+
+ for (BasicControlPoint point : mControlPoints) {
+ builder.addControlPoint(point.mIntensity, point.mSharpness, point.mDurationMs);
+ }
+ composition.addEffect(builder.build());
+ }
+
+ @Override
+ public String toString() {
+ return "SerializedBasicEnvelopeEffect{"
+ + "initialSharpness=" + (Float.isNaN(mInitialSharpness) ? "" : mInitialSharpness)
+ + ", controlPoints=" + Arrays.toString(mControlPoints)
+ + '}';
+ }
+
+ static final class Builder {
+ private final List<BasicControlPoint> mControlPoints;
+ private float mInitialSharpness = Float.NaN;
+
+ Builder() {
+ mControlPoints = new ArrayList<>();
+ }
+
+ void setInitialSharpness(float sharpness) {
+ mInitialSharpness = sharpness;
+ }
+
+ void addControlPoint(float intensity, float sharpness, long durationMs) {
+ mControlPoints.add(new BasicControlPoint(intensity, sharpness, durationMs));
+ }
+
+ SerializedBasicEnvelopeEffect build() {
+ return new SerializedBasicEnvelopeEffect(
+ mControlPoints.toArray(new BasicControlPoint[0]), mInitialSharpness);
+ }
+ }
+
+ /** Parser implementation for {@link SerializedBasicEnvelopeEffect}. */
+ static final class Parser {
+
+ @NonNull
+ static SerializedBasicEnvelopeEffect parseNext(@NonNull TypedXmlPullParser parser,
+ @XmlConstants.Flags int flags) throws XmlParserException, IOException {
+ XmlValidator.checkStartTag(parser, TAG_BASIC_ENVELOPE_EFFECT);
+ XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_INITIAL_SHARPNESS);
+
+ Builder builder = new Builder();
+ builder.setInitialSharpness(
+ XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_INITIAL_SHARPNESS, 0f, 1f,
+ Float.NaN));
+
+ int outerDepth = parser.getDepth();
+
+ // Read all nested tags
+ while (XmlReader.readNextTagWithin(parser, outerDepth)) {
+ parseControlPoint(parser, builder);
+ // Consume tag
+ XmlReader.readEndTag(parser);
+ }
+
+ // Check schema assertions about <basic-envelope-effect>
+ XmlValidator.checkParserCondition(!builder.mControlPoints.isEmpty(),
+ "Expected tag %s to have at least one control point",
+ TAG_BASIC_ENVELOPE_EFFECT);
+ XmlValidator.checkParserCondition(builder.mControlPoints.getLast().mIntensity == 0,
+ "Basic envelope effects must end at a zero intensity control point");
+
+ return builder.build();
+ }
+
+ private static void parseControlPoint(TypedXmlPullParser parser, Builder builder)
+ throws XmlParserException {
+ XmlValidator.checkStartTag(parser, TAG_CONTROL_POINT);
+ XmlValidator.checkTagHasNoUnexpectedAttributes(
+ parser, ATTRIBUTE_DURATION_MS, ATTRIBUTE_INTENSITY,
+ ATTRIBUTE_SHARPNESS);
+ float intensity = XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_INTENSITY, 0,
+ 1);
+ float sharpness = XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_SHARPNESS, 0,
+ 1);
+ long durationMs = XmlReader.readAttributePositiveLong(parser, ATTRIBUTE_DURATION_MS);
+
+ builder.addControlPoint(intensity, sharpness, durationMs);
+ }
+ }
+
+ private static final class BasicControlPoint {
+ private final float mIntensity;
+ private final float mSharpness;
+ private final long mDurationMs;
+
+ BasicControlPoint(float intensity, float sharpness, long durationMs) {
+ mIntensity = intensity;
+ mSharpness = sharpness;
+ mDurationMs = durationMs;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.ROOT, "(%.2f, %.2f, %dms)", mIntensity, mSharpness,
+ mDurationMs);
+ }
+ }
+}
+
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedWaveformEnvelopeEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedWaveformEnvelopeEffect.java
new file mode 100644
index 000000000000..6a893430d7ad
--- /dev/null
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedWaveformEnvelopeEffect.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2024 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 com.android.internal.vibrator.persistence;
+
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_AMPLITUDE;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_DURATION_MS;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_FREQUENCY_HZ;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_INITIAL_FREQUENCY_HZ;
+import static com.android.internal.vibrator.persistence.XmlConstants.NAMESPACE;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_CONTROL_POINT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_ENVELOPE_EFFECT;
+
+import android.annotation.NonNull;
+import android.os.VibrationEffect;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Serialized representation of a waveform envelope effect created via
+ * {@link VibrationEffect.WaveformEnvelopeBuilder}.
+ *
+ * @hide
+ */
+final class SerializedWaveformEnvelopeEffect implements SerializedComposedEffect.SerializedSegment {
+
+ private final WaveformControlPoint[] mControlPoints;
+ private final float mInitialFrequency;
+
+ SerializedWaveformEnvelopeEffect(WaveformControlPoint[] controlPoints, float initialFrequency) {
+ mControlPoints = controlPoints;
+ mInitialFrequency = initialFrequency;
+ }
+
+ @Override
+ public void write(@NonNull TypedXmlSerializer serializer) throws IOException {
+ serializer.startTag(NAMESPACE, TAG_WAVEFORM_ENVELOPE_EFFECT);
+
+ if (!Float.isNaN(mInitialFrequency)) {
+ serializer.attributeFloat(NAMESPACE, ATTRIBUTE_INITIAL_FREQUENCY_HZ, mInitialFrequency);
+ }
+
+ for (WaveformControlPoint point : mControlPoints) {
+ serializer.startTag(NAMESPACE, TAG_CONTROL_POINT);
+ serializer.attributeFloat(NAMESPACE, ATTRIBUTE_AMPLITUDE, point.mAmplitude);
+ serializer.attributeFloat(NAMESPACE, ATTRIBUTE_FREQUENCY_HZ, point.mFrequency);
+ serializer.attributeLong(NAMESPACE, ATTRIBUTE_DURATION_MS, point.mDurationMs);
+ serializer.endTag(NAMESPACE, TAG_CONTROL_POINT);
+ }
+
+ serializer.endTag(NAMESPACE, TAG_WAVEFORM_ENVELOPE_EFFECT);
+ }
+
+ @Override
+ public void deserializeIntoComposition(@NonNull VibrationEffect.Composition composition) {
+ VibrationEffect.WaveformEnvelopeBuilder builder =
+ new VibrationEffect.WaveformEnvelopeBuilder();
+
+ if (!Float.isNaN(mInitialFrequency)) {
+ builder.setInitialFrequencyHz(mInitialFrequency);
+ }
+
+ for (WaveformControlPoint point : mControlPoints) {
+ builder.addControlPoint(point.mAmplitude, point.mFrequency, point.mDurationMs);
+ }
+ composition.addEffect(builder.build());
+ }
+
+ @Override
+ public String toString() {
+ return "SerializedWaveformEnvelopeEffect{"
+ + "InitialFrequency=" + (Float.isNaN(mInitialFrequency) ? "" : mInitialFrequency)
+ + ", controlPoints=" + Arrays.toString(mControlPoints)
+ + '}';
+ }
+
+ static final class Builder {
+ private final List<WaveformControlPoint> mControlPoints;
+ private float mInitialFrequencyHz = Float.NaN;
+
+ Builder() {
+ mControlPoints = new ArrayList<>();
+ }
+
+ void setInitialFrequencyHz(float frequencyHz) {
+ mInitialFrequencyHz = frequencyHz;
+ }
+
+ void addControlPoint(float amplitude, float frequencyHz, long durationMs) {
+ mControlPoints.add(new WaveformControlPoint(amplitude, frequencyHz, durationMs));
+ }
+
+ SerializedWaveformEnvelopeEffect build() {
+ return new SerializedWaveformEnvelopeEffect(
+ mControlPoints.toArray(new WaveformControlPoint[0]), mInitialFrequencyHz);
+ }
+ }
+
+ /** Parser implementation for {@link SerializedWaveformEnvelopeEffect}. */
+ static final class Parser {
+
+ @NonNull
+ static SerializedWaveformEnvelopeEffect parseNext(@NonNull TypedXmlPullParser parser,
+ @XmlConstants.Flags int flags) throws XmlParserException, IOException {
+ XmlValidator.checkStartTag(parser, TAG_WAVEFORM_ENVELOPE_EFFECT);
+ XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_INITIAL_FREQUENCY_HZ);
+
+ Builder builder = new Builder();
+ builder.setInitialFrequencyHz(
+ XmlReader.readAttributePositiveFloat(parser, ATTRIBUTE_INITIAL_FREQUENCY_HZ,
+ Float.NaN));
+
+ int outerDepth = parser.getDepth();
+
+ while (XmlReader.readNextTagWithin(parser, outerDepth)) {
+ parseControlPoint(parser, builder);
+ // Consume tag
+ XmlReader.readEndTag(parser);
+ }
+
+ // Check schema assertions about <waveform-envelope-effect>
+ XmlValidator.checkParserCondition(!builder.mControlPoints.isEmpty(),
+ "Expected tag %s to have at least one control point",
+ TAG_WAVEFORM_ENVELOPE_EFFECT);
+
+ return builder.build();
+ }
+
+ private static void parseControlPoint(TypedXmlPullParser parser, Builder builder)
+ throws XmlParserException {
+ XmlValidator.checkStartTag(parser, TAG_CONTROL_POINT);
+ XmlValidator.checkTagHasNoUnexpectedAttributes(
+ parser, ATTRIBUTE_DURATION_MS, ATTRIBUTE_AMPLITUDE,
+ ATTRIBUTE_FREQUENCY_HZ);
+ float amplitude = XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_AMPLITUDE, 0,
+ 1);
+ float frequencyHz = XmlReader.readAttributePositiveFloat(parser,
+ ATTRIBUTE_FREQUENCY_HZ);
+ long durationMs = XmlReader.readAttributePositiveLong(parser, ATTRIBUTE_DURATION_MS);
+
+ builder.addControlPoint(amplitude, frequencyHz, durationMs);
+ }
+ }
+
+ private static final class WaveformControlPoint {
+ private final float mAmplitude;
+ private final float mFrequency;
+ private final long mDurationMs;
+
+ WaveformControlPoint(float amplitude, float frequency, long durationMs) {
+ mAmplitude = amplitude;
+ mFrequency = frequency;
+ mDurationMs = durationMs;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.ROOT, "(%.2f, %.2f, %dms)", mAmplitude, mFrequency,
+ mDurationMs);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
index a9fbcafa128d..314bfe40ee0b 100644
--- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
+++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
@@ -16,11 +16,13 @@
package com.android.internal.vibrator.persistence;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_BASIC_ENVELOPE_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PREDEFINED_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PRIMITIVE_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VENDOR_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VIBRATION_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_EFFECT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_ENVELOPE_EFFECT;
import android.annotation.NonNull;
import android.os.VibrationEffect;
@@ -92,6 +94,32 @@ import java.util.List;
* }
* </pre>
*
+ * * Waveform Envelope effects
+ *
+ * <pre>
+ * {@code
+ * <vibration-effect>
+ * <waveform-envelope-effect initialFrequencyHz="20.0">
+ * <control-point amplitude="0.2" frequencyHz="80.0" durationMs="50" />
+ * <control-point amplitude="0.5" frequencyHz="150.0" durationMs="50" />
+ * </envelope-effect>
+ * </vibration-effect>
+ * }
+ * </pre>
+ *
+ * * Basic Envelope effects
+ *
+ * <pre>
+ * {@code
+ * <vibration-effect>
+ * <basic-envelope-effect initialSharpness="0.3">
+ * <control-point intensity="0.2" sharpness="0.5" durationMs="50" />
+ * <control-point intensity="0.0" sharpness="1.0" durationMs="50" />
+ * </envelope-effect>
+ * </vibration-effect>
+ * }
+ * </pre>
+ *
* @hide
*/
public class VibrationEffectXmlParser {
@@ -151,6 +179,18 @@ public class VibrationEffectXmlParser {
serializedVibration = new SerializedComposedEffect(
SerializedAmplitudeStepWaveform.Parser.parseNext(parser));
break;
+ case TAG_WAVEFORM_ENVELOPE_EFFECT:
+ if (Flags.normalizedPwleEffects()) {
+ serializedVibration = new SerializedComposedEffect(
+ SerializedWaveformEnvelopeEffect.Parser.parseNext(parser, flags));
+ break;
+ } // else fall through
+ case TAG_BASIC_ENVELOPE_EFFECT:
+ if (Flags.normalizedPwleEffects()) {
+ serializedVibration = new SerializedComposedEffect(
+ SerializedBasicEnvelopeEffect.Parser.parseNext(parser, flags));
+ break;
+ } // else fall through
default:
throw new XmlParserException("Unexpected tag " + parser.getName()
+ " in vibration tag " + vibrationTagName);
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
index cb834a5eac7e..ebe34344c6f5 100644
--- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
+++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
@@ -19,9 +19,11 @@ package com.android.internal.vibrator.persistence;
import android.annotation.NonNull;
import android.os.PersistableBundle;
import android.os.VibrationEffect;
+import android.os.vibrator.BasicPwleSegment;
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
@@ -45,6 +47,8 @@ import java.util.List;
* <li>A composition created exclusively via
* {@link VibrationEffect.Composition#addPrimitive(int, float, int)}
* <li>{@link VibrationEffect#createVendorEffect(PersistableBundle)}
+ * <li>{@link VibrationEffect.WaveformEnvelopeBuilder}
+ * <li>{@link VibrationEffect.BasicEnvelopeBuilder}
* </ul>
*
* @hide
@@ -77,6 +81,12 @@ public final class VibrationEffectXmlSerializer {
if (firstSegment instanceof PrimitiveSegment) {
return serializePrimitiveEffect(composed);
}
+ if (Flags.normalizedPwleEffects() && firstSegment instanceof PwleSegment) {
+ return serializeWaveformEnvelopeEffect(composed);
+ }
+ if (Flags.normalizedPwleEffects() && firstSegment instanceof BasicPwleSegment) {
+ return serializeBasicEnvelopeEffect(composed);
+ }
return serializeWaveformEffect(composed);
}
@@ -110,6 +120,53 @@ public final class VibrationEffectXmlSerializer {
return new SerializedComposedEffect(primitives);
}
+ private static SerializedComposedEffect serializeWaveformEnvelopeEffect(
+ VibrationEffect.Composed effect) throws XmlSerializerException {
+ SerializedWaveformEnvelopeEffect.Builder builder =
+ new SerializedWaveformEnvelopeEffect.Builder();
+ List<VibrationEffectSegment> segments = effect.getSegments();
+ XmlValidator.checkSerializerCondition(effect.getRepeatIndex() == -1,
+ "Unsupported repeating waveform envelope effect %s", effect);
+ for (int i = 0; i < segments.size(); i++) {
+ XmlValidator.checkSerializerCondition(segments.get(i) instanceof PwleSegment,
+ "Unsupported segment for waveform envelope effect %s", segments.get(i));
+ PwleSegment segment = (PwleSegment) segments.get(i);
+
+ if (i == 0 && segment.getStartFrequencyHz() != segment.getEndFrequencyHz()) {
+ // Initial frequency explicitly defined.
+ builder.setInitialFrequencyHz(segment.getStartFrequencyHz());
+ }
+
+ builder.addControlPoint(segment.getEndAmplitude(), segment.getEndFrequencyHz(),
+ segment.getDuration());
+ }
+
+ return new SerializedComposedEffect(builder.build());
+ }
+
+ private static SerializedComposedEffect serializeBasicEnvelopeEffect(
+ VibrationEffect.Composed effect) throws XmlSerializerException {
+ SerializedBasicEnvelopeEffect.Builder builder = new SerializedBasicEnvelopeEffect.Builder();
+ List<VibrationEffectSegment> segments = effect.getSegments();
+ XmlValidator.checkSerializerCondition(effect.getRepeatIndex() == -1,
+ "Unsupported repeating basic envelope effect %s", effect);
+ for (int i = 0; i < segments.size(); i++) {
+ XmlValidator.checkSerializerCondition(segments.get(i) instanceof BasicPwleSegment,
+ "Unsupported segment for basic envelope effect %s", segments.get(i));
+ BasicPwleSegment segment = (BasicPwleSegment) segments.get(i);
+
+ if (i == 0 && segment.getStartSharpness() != segment.getEndSharpness()) {
+ // Initial sharpness explicitly defined.
+ builder.setInitialSharpness(segment.getStartSharpness());
+ }
+
+ builder.addControlPoint(segment.getEndIntensity(), segment.getEndSharpness(),
+ segment.getDuration());
+ }
+
+ return new SerializedComposedEffect(builder.build());
+ }
+
private static SerializedComposedEffect serializeWaveformEffect(
VibrationEffect.Composed effect) throws XmlSerializerException {
SerializedAmplitudeStepWaveform.Builder serializedWaveformBuilder =
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
index 4122215a2b04..df262cfecd5a 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
@@ -42,14 +42,22 @@ public final class XmlConstants {
public static final String TAG_PREDEFINED_EFFECT = "predefined-effect";
public static final String TAG_PRIMITIVE_EFFECT = "primitive-effect";
public static final String TAG_VENDOR_EFFECT = "vendor-effect";
+ public static final String TAG_WAVEFORM_ENVELOPE_EFFECT = "waveform-envelope-effect";
+ public static final String TAG_BASIC_ENVELOPE_EFFECT = "basic-envelope-effect";
public static final String TAG_WAVEFORM_EFFECT = "waveform-effect";
public static final String TAG_WAVEFORM_ENTRY = "waveform-entry";
public static final String TAG_REPEATING = "repeating";
+ public static final String TAG_CONTROL_POINT = "control-point";
public static final String ATTRIBUTE_NAME = "name";
public static final String ATTRIBUTE_FALLBACK = "fallback";
public static final String ATTRIBUTE_DURATION_MS = "durationMs";
public static final String ATTRIBUTE_AMPLITUDE = "amplitude";
+ public static final String ATTRIBUTE_FREQUENCY_HZ = "frequencyHz";
+ public static final String ATTRIBUTE_INITIAL_FREQUENCY_HZ = "initialFrequencyHz";
+ public static final String ATTRIBUTE_INTENSITY = "intensity";
+ public static final String ATTRIBUTE_SHARPNESS = "sharpness";
+ public static final String ATTRIBUTE_INITIAL_SHARPNESS = "initialSharpness";
public static final String ATTRIBUTE_SCALE = "scale";
public static final String ATTRIBUTE_DELAY_MS = "delayMs";
public static final String ATTRIBUTE_DELAY_TYPE = "delayType";
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlReader.java b/core/java/com/android/internal/vibrator/persistence/XmlReader.java
index 0ac6fefc8cb2..1c4a783f0fe4 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlReader.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlReader.java
@@ -221,12 +221,63 @@ public final class XmlReader {
if (parser.getAttributeIndex(NAMESPACE, attrName) < 0) {
return defaultValue;
}
+
+ return readAttributeFloatInRange(parser, attrName, lowerInclusive, upperInclusive);
+ }
+
+ /**
+ * Read attribute from current tag as a float within given inclusive range.
+ */
+ public static float readAttributeFloatInRange(
+ TypedXmlPullParser parser, String attrName, float lowerInclusive,
+ float upperInclusive) throws XmlParserException {
String tagName = parser.getName();
float value = readAttributeFloat(parser, attrName);
XmlValidator.checkParserCondition(value >= lowerInclusive && value <= upperInclusive,
- "Unexpected %s = %f in tag %s, expected %s in [%f, %f]",
- attrName, value, tagName, attrName, lowerInclusive, upperInclusive);
+ "Unexpected %s = %f in tag %s, expected %s in [%f, %f]", attrName, value, tagName,
+ attrName, lowerInclusive, upperInclusive);
+ return value;
+ }
+
+ /**
+ * Read attribute from current tag as a positive float, returning default value if attribute
+ * is missing.
+ */
+ public static float readAttributePositiveFloat(TypedXmlPullParser parser, String attrName,
+ float defaultValue) throws XmlParserException {
+ if (parser.getAttributeIndex(NAMESPACE, attrName) < 0) {
+ return defaultValue;
+ }
+
+ return readAttributePositiveFloat(parser, attrName);
+ }
+
+ /**
+ * Read attribute from current tag as a positive float.
+ */
+ public static float readAttributePositiveFloat(TypedXmlPullParser parser, String attrName)
+ throws XmlParserException {
+ String tagName = parser.getName();
+ float value = readAttributeFloat(parser, attrName);
+
+ XmlValidator.checkParserCondition(value > 0,
+ "Unexpected %s = %d in tag %s, expected %s > 0", attrName, value, tagName,
+ attrName);
+ return value;
+ }
+
+ /**
+ * Read attribute from current tag as a positive long.
+ */
+ public static long readAttributePositiveLong(TypedXmlPullParser parser, String attrName)
+ throws XmlParserException {
+ String tagName = parser.getName();
+ long value = readAttributeLong(parser, attrName);
+
+ XmlValidator.checkParserCondition(value > 0,
+ "Unexpected %s = %d in tag %s, expected %s > 0", attrName, value, tagName,
+ attrName);
return value;
}
@@ -251,4 +302,15 @@ public final class XmlReader {
throw XmlParserException.createFromPullParserException(tagName, attrName, rawValue, e);
}
}
+
+ private static long readAttributeLong(TypedXmlPullParser parser, String attrName)
+ throws XmlParserException {
+ String tagName = parser.getName();
+ try {
+ return parser.getAttributeLong(NAMESPACE, attrName);
+ } catch (XmlPullParserException e) {
+ String rawValue = parser.getAttributeValue(NAMESPACE, attrName);
+ throw XmlParserException.createFromPullParserException(tagName, attrName, rawValue, e);
+ }
+ }
}
diff --git a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
index 40710577b154..5f25e9315831 100644
--- a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
@@ -439,6 +439,498 @@ public class VibrationEffectXmlSerializationTest {
}
@Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testWaveformEnvelopeEffect_allSucceed() throws Exception {
+ VibrationEffect effect = new VibrationEffect.WaveformEnvelopeBuilder()
+ .addControlPoint(0.2f, 80f, 10)
+ .addControlPoint(0.5f, 150f, 10)
+ .build();
+
+ String xml = """
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+ <control-point amplitude="0.5" frequencyHz="150.0" durationMs="10" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, xml);
+ assertPublicApisRoundTrip(effect);
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, xml);
+ assertHiddenApisRoundTrip(effect);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testWaveformEnvelopeEffectWithInitialFrequency_allSucceed() throws Exception {
+ VibrationEffect effect = new VibrationEffect.WaveformEnvelopeBuilder()
+ .setInitialFrequencyHz(20)
+ .addControlPoint(0.2f, 80f, 10)
+ .addControlPoint(0.5f, 150f, 10)
+ .build();
+
+ String xml = """
+ <vibration-effect>
+ <waveform-envelope-effect initialFrequencyHz="20.0">
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+ <control-point amplitude="0.5" frequencyHz="150.0" durationMs="10" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, xml);
+ assertPublicApisRoundTrip(effect);
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, xml);
+ assertHiddenApisRoundTrip(effect);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testWaveformEnvelopeEffect_badXml_throwsException() throws IOException {
+ // Incomplete XML
+ assertParseElementFails("""
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+ </vibration-effect>
+ """);
+ assertParseElementFails("""
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10">
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """);
+ assertParseElementFails("""
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+ </waveform-envelope-effect>
+ """);
+
+ // Bad vibration XML
+ assertParseElementFails("""
+ <vibration-effect>
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """);
+
+ // "waveform-envelope-effect" tag with invalid attributes
+ assertParseElementFails("""
+ <vibration-effect>
+ <waveform-envelope-effect init_freq="20.0">
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testWaveformEnvelopeEffect_noControlPoints_allFail() throws IOException {
+ String xml = "<vibration-effect><waveform-envelope-effect/></vibration-effect>";
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = "<vibration-effect><waveform-envelope-effect> \n "
+ + "</waveform-envelope-effect></vibration-effect>";
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = "<vibration-effect><waveform-envelope-effect>invalid</waveform-envelope-effect"
+ + "></vibration-effect>";
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect initialFrequencyHz="20.0" />
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect initialFrequencyHz="20.0"> \n </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect initialFrequencyHz="20.0">
+ invalid
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect initialFrequencyHz="20.0">
+ <control-point />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testWaveformEnvelopeEffect_badControlPointData_allFail() throws IOException {
+ String xml = """
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="-1" frequencyHz="80.0" durationMs="100" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.2" frequencyHz="0" durationMs="100" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect initialFrequencyHz="0">
+ <control-point amplitude="0.2" frequencyHz="30" durationMs="100" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="0" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.2" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testWaveformEnvelopeEffect_featureFlagDisabled_allFail() throws Exception {
+ VibrationEffect effect = new VibrationEffect.WaveformEnvelopeBuilder()
+ .setInitialFrequencyHz(20)
+ .addControlPoint(0.2f, 80f, 10)
+ .addControlPoint(0.5f, 150f, 10)
+ .build();
+
+ String xml = """
+ <vibration-effect>
+ <waveform-envelope-effect initialFrequencyHz="20.0">
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+ <control-point amplitude="0.5" frequencyHz="150.0" durationMs="10" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserFails(xml);
+ assertPublicApisSerializerFails(effect);
+ assertHiddenApisParserFails(xml);
+ assertHiddenApisSerializerFails(effect);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicEnvelopeEffect_allSucceed() throws Exception {
+ VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
+ .addControlPoint(0.2f, 0.5f, 10)
+ .addControlPoint(0.0f, 1f, 10)
+ .build();
+
+ String xml = """
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="0.2" sharpness="0.5" durationMs="10" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, xml);
+ assertPublicApisRoundTrip(effect);
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, xml);
+ assertHiddenApisRoundTrip(effect);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicEnvelopeEffectWithInitialSharpness_allSucceed() throws Exception {
+ VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
+ .setInitialSharpness(0.3f)
+ .addControlPoint(0.2f, 0.5f, 10)
+ .addControlPoint(0.0f, 1f, 10)
+ .build();
+
+ String xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="0.3">
+ <control-point intensity="0.2" sharpness="0.5" durationMs="10" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, xml);
+ assertPublicApisRoundTrip(effect);
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, xml);
+ assertHiddenApisRoundTrip(effect);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicEnvelopeEffect_badXml_throwsException() throws IOException {
+ // Incomplete XML
+ assertParseElementFails("""
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="0.2" sharpness="0.8" durationMs="10" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </vibration-effect>
+ """);
+ assertParseElementFails("""
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="0.2" sharpness="0.8" durationMs="10">
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """);
+ assertParseElementFails("""
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="0.2" sharpness="0.8" durationMs="10" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ """);
+
+ // Bad vibration XML
+ assertParseElementFails("""
+ <vibration-effect>
+ <control-point intensity="0.2" sharpness="0.8" durationMs="10" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """);
+
+ // "basic-envelope-effect" tag with invalid attributes
+ assertParseElementFails("""
+ <vibration-effect>
+ <basic-envelope-effect init_sharp="20.0">
+ <control-point intensity="0.2" sharpness="0.8" durationMs="10" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicEnvelopeEffect_noControlPoints_allFail() throws IOException {
+ String xml = "<vibration-effect><basic-envelope-effect/></vibration-effect>";
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = "<vibration-effect><basic-envelope-effect> \n "
+ + "</basic-envelope-effect></vibration-effect>";
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = "<vibration-effect><basic-envelope-effect>invalid</basic-envelope-effect"
+ + "></vibration-effect>";
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="0.2" />
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="0.2"> \n </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="0.2">
+ invalid
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="0.2">
+ <control-point />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicEnvelopeEffect_badControlPointData_allFail() throws IOException {
+ String xml = """
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="-1" sharpness="0.8" durationMs="100" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="0.2" sharpness="-1" durationMs="100" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="-1.0">
+ <control-point intensity="0.2" sharpness="0.8" durationMs="0" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="2.0">
+ <control-point intensity="0.2" sharpness="0.8" durationMs="0" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="0.2" sharpness="0.8" durationMs="10" />
+ <control-point intensity="0.5" sharpness="0.8" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="0.2" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicEnvelopeEffect_featureFlagDisabled_allFail() throws Exception {
+ VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
+ .setInitialSharpness(0.3f)
+ .addControlPoint(0.2f, 0.5f, 10)
+ .addControlPoint(0.0f, 1f, 10)
+ .build();
+
+ String xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="0.3">
+ <control-point intensity="0.2" sharpness="0.5" durationMs="10" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserFails(xml);
+ assertPublicApisSerializerFails(effect);
+
+ assertHiddenApisParserFails(xml);
+ assertHiddenApisSerializerFails(effect);
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
public void testVendorEffect_allSucceed() throws Exception {
PersistableBundle vendorData = new PersistableBundle();
diff --git a/core/xsd/vibrator/vibration/schema/current.txt b/core/xsd/vibrator/vibration/schema/current.txt
index b4148d657b0d..29f8d199c1d1 100644
--- a/core/xsd/vibrator/vibration/schema/current.txt
+++ b/core/xsd/vibrator/vibration/schema/current.txt
@@ -1,6 +1,23 @@
// Signature format: 2.0
package com.android.internal.vibrator.persistence {
+ public class BasicControlPoint {
+ ctor public BasicControlPoint();
+ method public long getDurationMs();
+ method public float getIntensity();
+ method public float getSharpness();
+ method public void setDurationMs(long);
+ method public void setIntensity(float);
+ method public void setSharpness(float);
+ }
+
+ public class BasicEnvelopeEffect {
+ ctor public BasicEnvelopeEffect();
+ method public java.util.List<com.android.internal.vibrator.persistence.BasicControlPoint> getControlPoint();
+ method public float getInitialSharpness();
+ method public void setInitialSharpness(float);
+ }
+
public class PredefinedEffect {
ctor public PredefinedEffect();
method public com.android.internal.vibrator.persistence.PredefinedEffectName getName();
@@ -47,14 +64,18 @@ package com.android.internal.vibrator.persistence {
public class VibrationEffect {
ctor public VibrationEffect();
+ method public com.android.internal.vibrator.persistence.BasicEnvelopeEffect getBasicEnvelopeEffect_optional();
method public com.android.internal.vibrator.persistence.PredefinedEffect getPredefinedEffect_optional();
method public com.android.internal.vibrator.persistence.PrimitiveEffect getPrimitiveEffect_optional();
method public byte[] getVendorEffect_optional();
method public com.android.internal.vibrator.persistence.WaveformEffect getWaveformEffect_optional();
+ method public com.android.internal.vibrator.persistence.WaveformEnvelopeEffect getWaveformEnvelopeEffect_optional();
+ method public void setBasicEnvelopeEffect_optional(com.android.internal.vibrator.persistence.BasicEnvelopeEffect);
method public void setPredefinedEffect_optional(com.android.internal.vibrator.persistence.PredefinedEffect);
method public void setPrimitiveEffect_optional(com.android.internal.vibrator.persistence.PrimitiveEffect);
method public void setVendorEffect_optional(byte[]);
method public void setWaveformEffect_optional(com.android.internal.vibrator.persistence.WaveformEffect);
+ method public void setWaveformEnvelopeEffect_optional(com.android.internal.vibrator.persistence.WaveformEnvelopeEffect);
}
public class VibrationSelect {
@@ -67,6 +88,16 @@ package com.android.internal.vibrator.persistence {
enum_constant public static final com.android.internal.vibrator.persistence.WaveformAmplitudeDefault _default;
}
+ public class WaveformControlPoint {
+ ctor public WaveformControlPoint();
+ method public float getAmplitude();
+ method public long getDurationMs();
+ method public float getFrequencyHz();
+ method public void setAmplitude(float);
+ method public void setDurationMs(long);
+ method public void setFrequencyHz(float);
+ }
+
public class WaveformEffect {
ctor public WaveformEffect();
method public com.android.internal.vibrator.persistence.WaveformEffect.Repeating getRepeating();
@@ -87,6 +118,13 @@ package com.android.internal.vibrator.persistence {
method public void setDurationMs(java.math.BigInteger);
}
+ public class WaveformEnvelopeEffect {
+ ctor public WaveformEnvelopeEffect();
+ method public java.util.List<com.android.internal.vibrator.persistence.WaveformControlPoint> getControlPoint();
+ method public float getInitialFrequencyHz();
+ method public void setInitialFrequencyHz(float);
+ }
+
public class XmlParser {
ctor public XmlParser();
method public static String readText(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
diff --git a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
index 910a9b700b5c..b4df2d187702 100644
--- a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
+++ b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
@@ -54,6 +54,12 @@
<xs:element name="primitive-effect" type="PrimitiveEffect"/>
</xs:sequence>
+ <!-- Waveform envelope effect -->
+ <xs:element name="waveform-envelope-effect" type="WaveformEnvelopeEffect"/>
+
+ <!-- Basic envelope effect -->
+ <xs:element name="basic-envelope-effect" type="BasicEnvelopeEffect"/>
+
</xs:choice>
</xs:complexType>
@@ -180,4 +186,54 @@
</xs:restriction>
</xs:simpleType>
+ <!-- Definition of a waveform envelope effect -->
+ <xs:complexType name="WaveformEnvelopeEffect">
+ <xs:sequence>
+ <xs:element name="control-point" maxOccurs="unbounded" minOccurs="1"
+ type="WaveformControlPoint" />
+ </xs:sequence>
+ <xs:attribute name="initialFrequencyHz" type="ControlPointFrequency" />
+ </xs:complexType>
+
+ <!-- Definition of a basic envelope effect -->
+ <xs:complexType name="BasicEnvelopeEffect">
+ <xs:sequence>
+ <xs:element name="control-point" maxOccurs="unbounded" minOccurs="1"
+ type="BasicControlPoint" />
+ </xs:sequence>
+ <xs:attribute name="initialSharpness" type="NormalizedControlPointUnit" />
+ </xs:complexType>
+
+ <xs:complexType name="WaveformControlPoint">
+ <xs:attribute name="amplitude" type="NormalizedControlPointUnit" use="required"/>
+ <xs:attribute name="frequencyHz" type="ControlPointFrequency" use="required"/>
+ <xs:attribute name="durationMs" type="PositiveLong" use="required"/>
+ </xs:complexType>
+
+ <xs:complexType name="BasicControlPoint">
+ <xs:attribute name="intensity" type="NormalizedControlPointUnit" use="required"/>
+ <xs:attribute name="sharpness" type="NormalizedControlPointUnit" use="required"/>
+ <xs:attribute name="durationMs" type="PositiveLong" use="required"/>
+ </xs:complexType>
+
+ <xs:simpleType name="ControlPointFrequency">
+ <xs:restriction base="xs:float">
+ <xs:minExclusive value="0"/>
+ </xs:restriction>
+ </xs:simpleType>
+
+ <xs:simpleType name="PositiveLong">
+ <xs:restriction base="xs:long">
+ <xs:minExclusive value="0"/>
+ </xs:restriction>
+ </xs:simpleType>
+
+ <!-- Normalized control point unit float in [0,1] -->
+ <xs:simpleType name="NormalizedControlPointUnit">
+ <xs:restriction base="xs:float">
+ <xs:minInclusive value="0"/>
+ <xs:maxInclusive value="1"/>
+ </xs:restriction>
+ </xs:simpleType>
+
</xs:schema>
diff --git a/core/xsd/vibrator/vibration/vibration.xsd b/core/xsd/vibrator/vibration/vibration.xsd
index 3c8e01605659..fba966faa9c9 100644
--- a/core/xsd/vibrator/vibration/vibration.xsd
+++ b/core/xsd/vibrator/vibration/vibration.xsd
@@ -52,6 +52,12 @@
<xs:element name="primitive-effect" type="PrimitiveEffect"/>
</xs:sequence>
+ <!-- Waveform envelope effect -->
+ <xs:element name="waveform-envelope-effect" type="WaveformEnvelopeEffect"/>
+
+ <!-- Basic envelope effect -->
+ <xs:element name="basic-envelope-effect" type="BasicEnvelopeEffect"/>
+
</xs:choice>
</xs:complexType>
@@ -157,4 +163,54 @@
</xs:restriction>
</xs:simpleType>
+ <!-- Definition of a waveform envelope effect -->
+ <xs:complexType name="WaveformEnvelopeEffect">
+ <xs:sequence>
+ <xs:element name="control-point" maxOccurs="unbounded" minOccurs="1"
+ type="WaveformControlPoint" />
+ </xs:sequence>
+ <xs:attribute name="initialFrequencyHz" type="ControlPointFrequency" />
+ </xs:complexType>
+
+ <!-- Definition of a basic envelope effect -->
+ <xs:complexType name="BasicEnvelopeEffect">
+ <xs:sequence>
+ <xs:element name="control-point" maxOccurs="unbounded" minOccurs="1"
+ type="BasicControlPoint" />
+ </xs:sequence>
+ <xs:attribute name="initialSharpness" type="NormalizedControlPointUnit" />
+ </xs:complexType>
+
+ <xs:complexType name="WaveformControlPoint">
+ <xs:attribute name="amplitude" type="NormalizedControlPointUnit" use="required"/>
+ <xs:attribute name="frequencyHz" type="ControlPointFrequency" use="required"/>
+ <xs:attribute name="durationMs" type="PositiveLong" use="required"/>
+ </xs:complexType>
+
+ <xs:complexType name="BasicControlPoint">
+ <xs:attribute name="intensity" type="NormalizedControlPointUnit" use="required"/>
+ <xs:attribute name="sharpness" type="NormalizedControlPointUnit" use="required"/>
+ <xs:attribute name="durationMs" type="PositiveLong" use="required"/>
+ </xs:complexType>
+
+ <xs:simpleType name="ControlPointFrequency">
+ <xs:restriction base="xs:float">
+ <xs:minExclusive value="0"/>
+ </xs:restriction>
+ </xs:simpleType>
+
+ <xs:simpleType name="PositiveLong">
+ <xs:restriction base="xs:long">
+ <xs:minExclusive value="0"/>
+ </xs:restriction>
+ </xs:simpleType>
+
+ <!-- Normalized control point unit float in [0,1] -->
+ <xs:simpleType name="NormalizedControlPointUnit">
+ <xs:restriction base="xs:float">
+ <xs:minInclusive value="0"/>
+ <xs:maxInclusive value="1"/>
+ </xs:restriction>
+ </xs:simpleType>
+
</xs:schema>