summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Ahmad Khalil <khalilahmad@google.com> 2024-12-08 21:16:32 +0000
committer Ahmad Khalil <khalilahmad@google.com> 2024-12-13 17:09:58 +0000
commit77a809f1d7b59749fad219ffb46ed1f63d6c8ca5 (patch)
treeb347331cf50e18f501e6a0fdc45ba61e15981d8a
parentfb3baa77b66a8a1a4cd4e92d6336c7f0ad0bce59 (diff)
Add xml serialization for repeating effects
Introduce new tags to the vibration.xsd scheme to support repeating effects which could include a preamble. Bug: 347035918 Flag: android.os.vibrator.normalized_pwle_effects Test: atest android.os.vibrator.persistence Change-Id: I2e14fec3d0310f1eec9683bd9d887facae82143b
-rw-r--r--core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java11
-rw-r--r--core/java/com/android/internal/vibrator/persistence/LegacyVibrationEffectXmlSerializer.java (renamed from core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java)2
-rw-r--r--core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java43
-rw-r--r--core/java/com/android/internal/vibrator/persistence/SerializedRepeatingEffect.java215
-rw-r--r--core/java/com/android/internal/vibrator/persistence/SerializedWaveformEffectEntries.java121
-rw-r--r--core/java/com/android/internal/vibrator/persistence/VibrationEffectSerializer.java336
-rw-r--r--core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java27
-rw-r--r--core/java/com/android/internal/vibrator/persistence/XmlConstants.java2
-rw-r--r--core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java539
-rw-r--r--core/xsd/vibrator/vibration/schema/current.txt24
-rw-r--r--core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd24
-rw-r--r--core/xsd/vibrator/vibration/vibration.xsd25
12 files changed, 1345 insertions, 24 deletions
diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
index a95ce7914d8b..c7778dee5ebe 100644
--- a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
+++ b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
@@ -22,7 +22,8 @@ import android.annotation.TestApi;
import android.os.VibrationEffect;
import android.util.Xml;
-import com.android.internal.vibrator.persistence.VibrationEffectXmlSerializer;
+import com.android.internal.vibrator.persistence.LegacyVibrationEffectXmlSerializer;
+import com.android.internal.vibrator.persistence.VibrationEffectSerializer;
import com.android.internal.vibrator.persistence.XmlConstants;
import com.android.internal.vibrator.persistence.XmlSerializedVibration;
import com.android.internal.vibrator.persistence.XmlSerializerException;
@@ -123,7 +124,13 @@ public final class VibrationXmlSerializer {
}
try {
- serializedVibration = VibrationEffectXmlSerializer.serialize(effect, serializerFlags);
+ if (android.os.vibrator.Flags.normalizedPwleEffects()) {
+ serializedVibration = VibrationEffectSerializer.serialize(effect,
+ serializerFlags);
+ } else {
+ serializedVibration = LegacyVibrationEffectXmlSerializer.serialize(effect,
+ serializerFlags);
+ }
XmlValidator.checkSerializedVibration(serializedVibration, effect);
} catch (XmlSerializerException e) {
// Serialization failed or created incomplete representation, fail before writing.
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java b/core/java/com/android/internal/vibrator/persistence/LegacyVibrationEffectXmlSerializer.java
index ebe34344c6f5..be30750ff281 100644
--- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
+++ b/core/java/com/android/internal/vibrator/persistence/LegacyVibrationEffectXmlSerializer.java
@@ -53,7 +53,7 @@ import java.util.List;
*
* @hide
*/
-public final class VibrationEffectXmlSerializer {
+public final class LegacyVibrationEffectXmlSerializer {
/**
* Creates a serialized representation of the input {@code vibration}.
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java b/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java
index cd7dcfdac906..efc7e354995d 100644
--- a/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java
@@ -35,6 +35,7 @@ import com.android.modules.utils.TypedXmlSerializer;
import java.io.IOException;
import java.util.Arrays;
+import java.util.function.BiConsumer;
/**
* Serialized representation of a waveform effect created via
@@ -144,7 +145,7 @@ final class SerializedAmplitudeStepWaveform implements SerializedSegment {
// Read all nested tag that is not a repeating tag as a waveform entry.
while (XmlReader.readNextTagWithin(parser, outerDepth)
&& !TAG_REPEATING.equals(parser.getName())) {
- parseWaveformEntry(parser, waveformBuilder);
+ parseWaveformEntry(parser, waveformBuilder::addDurationAndAmplitude);
}
// If found a repeating tag, read its content.
@@ -162,6 +163,25 @@ final class SerializedAmplitudeStepWaveform implements SerializedSegment {
return waveformBuilder.build();
}
+ static void parseWaveformEntry(TypedXmlPullParser parser,
+ BiConsumer<Integer, Integer> builder) throws XmlParserException, IOException {
+ XmlValidator.checkStartTag(parser, TAG_WAVEFORM_ENTRY);
+ XmlValidator.checkTagHasNoUnexpectedAttributes(
+ parser, ATTRIBUTE_DURATION_MS, ATTRIBUTE_AMPLITUDE);
+
+ String rawAmplitude = parser.getAttributeValue(NAMESPACE, ATTRIBUTE_AMPLITUDE);
+ int amplitude = VALUE_AMPLITUDE_DEFAULT.equals(rawAmplitude)
+ ? VibrationEffect.DEFAULT_AMPLITUDE
+ : XmlReader.readAttributeIntInRange(
+ parser, ATTRIBUTE_AMPLITUDE, 0, VibrationEffect.MAX_AMPLITUDE);
+ int durationMs = XmlReader.readAttributeIntNonNegative(parser, ATTRIBUTE_DURATION_MS);
+
+ builder.accept(durationMs, amplitude);
+
+ // Consume tag
+ XmlReader.readEndTag(parser);
+ }
+
private static void parseRepeating(TypedXmlPullParser parser, Builder waveformBuilder)
throws XmlParserException, IOException {
XmlValidator.checkStartTag(parser, TAG_REPEATING);
@@ -172,7 +192,7 @@ final class SerializedAmplitudeStepWaveform implements SerializedSegment {
boolean hasEntry = false;
int outerDepth = parser.getDepth();
while (XmlReader.readNextTagWithin(parser, outerDepth)) {
- parseWaveformEntry(parser, waveformBuilder);
+ parseWaveformEntry(parser, waveformBuilder::addDurationAndAmplitude);
hasEntry = true;
}
@@ -182,24 +202,5 @@ final class SerializedAmplitudeStepWaveform implements SerializedSegment {
// Consume tag
XmlReader.readEndTag(parser, TAG_REPEATING, outerDepth);
}
-
- private static void parseWaveformEntry(TypedXmlPullParser parser, Builder waveformBuilder)
- throws XmlParserException, IOException {
- XmlValidator.checkStartTag(parser, TAG_WAVEFORM_ENTRY);
- XmlValidator.checkTagHasNoUnexpectedAttributes(
- parser, ATTRIBUTE_DURATION_MS, ATTRIBUTE_AMPLITUDE);
-
- String rawAmplitude = parser.getAttributeValue(NAMESPACE, ATTRIBUTE_AMPLITUDE);
- int amplitude = VALUE_AMPLITUDE_DEFAULT.equals(rawAmplitude)
- ? VibrationEffect.DEFAULT_AMPLITUDE
- : XmlReader.readAttributeIntInRange(
- parser, ATTRIBUTE_AMPLITUDE, 0, VibrationEffect.MAX_AMPLITUDE);
- int durationMs = XmlReader.readAttributeIntNonNegative(parser, ATTRIBUTE_DURATION_MS);
-
- waveformBuilder.addDurationAndAmplitude(durationMs, amplitude);
-
- // Consume tag
- XmlReader.readEndTag(parser);
- }
}
}
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedRepeatingEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedRepeatingEffect.java
new file mode 100644
index 000000000000..12acc7247b86
--- /dev/null
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedRepeatingEffect.java
@@ -0,0 +1,215 @@
+/*
+ * 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.NAMESPACE;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_BASIC_ENVELOPE_EFFECT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PREAMBLE;
+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_REPEATING;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_REPEATING_EFFECT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_ENTRY;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_ENVELOPE_EFFECT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+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.List;
+
+/**
+ * Serialized representation of a repeating effect created via
+ * {@link VibrationEffect#createRepeatingEffect}.
+ *
+ * @hide
+ */
+public class SerializedRepeatingEffect implements SerializedComposedEffect.SerializedSegment {
+
+ @Nullable
+ private final SerializedComposedEffect mSerializedPreamble;
+ @NonNull
+ private final SerializedComposedEffect mSerializedRepeating;
+
+ SerializedRepeatingEffect(@Nullable SerializedComposedEffect serializedPreamble,
+ @NonNull SerializedComposedEffect serializedRepeating) {
+ mSerializedPreamble = serializedPreamble;
+ mSerializedRepeating = serializedRepeating;
+ }
+
+ @Override
+ public void write(@NonNull TypedXmlSerializer serializer) throws IOException {
+ serializer.startTag(NAMESPACE, TAG_REPEATING_EFFECT);
+
+ if (mSerializedPreamble != null) {
+ serializer.startTag(NAMESPACE, TAG_PREAMBLE);
+ mSerializedPreamble.writeContent(serializer);
+ serializer.endTag(NAMESPACE, TAG_PREAMBLE);
+ }
+
+ serializer.startTag(NAMESPACE, TAG_REPEATING);
+ mSerializedRepeating.writeContent(serializer);
+ serializer.endTag(NAMESPACE, TAG_REPEATING);
+
+ serializer.endTag(NAMESPACE, TAG_REPEATING_EFFECT);
+ }
+
+ @Override
+ public void deserializeIntoComposition(@NonNull VibrationEffect.Composition composition) {
+ if (mSerializedPreamble != null) {
+ composition.addEffect(
+ VibrationEffect.createRepeatingEffect(mSerializedPreamble.deserialize(),
+ mSerializedRepeating.deserialize()));
+ return;
+ }
+
+ composition.addEffect(
+ VibrationEffect.createRepeatingEffect(mSerializedRepeating.deserialize()));
+ }
+
+ @Override
+ public String toString() {
+ return "SerializedRepeatingEffect{"
+ + "preamble=" + mSerializedPreamble
+ + ", repeating=" + mSerializedRepeating
+ + '}';
+ }
+
+ static final class Builder {
+ private SerializedComposedEffect mPreamble;
+ private SerializedComposedEffect mRepeating;
+
+ void setPreamble(SerializedComposedEffect effect) {
+ mPreamble = effect;
+ }
+
+ void setRepeating(SerializedComposedEffect effect) {
+ mRepeating = effect;
+ }
+
+ boolean hasRepeatingSegment() {
+ return mRepeating != null;
+ }
+
+ SerializedRepeatingEffect build() {
+ return new SerializedRepeatingEffect(mPreamble, mRepeating);
+ }
+ }
+
+ /** Parser implementation for {@link SerializedRepeatingEffect}. */
+ static final class Parser {
+
+ @NonNull
+ static SerializedRepeatingEffect parseNext(@NonNull TypedXmlPullParser parser,
+ @XmlConstants.Flags int flags) throws XmlParserException, IOException {
+ XmlValidator.checkStartTag(parser, TAG_REPEATING_EFFECT);
+ XmlValidator.checkTagHasNoUnexpectedAttributes(parser);
+
+ Builder builder = new Builder();
+ int outerDepth = parser.getDepth();
+
+ boolean hasNestedTag = XmlReader.readNextTagWithin(parser, outerDepth);
+ if (hasNestedTag && TAG_PREAMBLE.equals(parser.getName())) {
+ builder.setPreamble(parseEffect(parser, TAG_PREAMBLE, flags));
+ hasNestedTag = XmlReader.readNextTagWithin(parser, outerDepth);
+ }
+
+ XmlValidator.checkParserCondition(hasNestedTag,
+ "Missing %s tag in %s", TAG_REPEATING, TAG_REPEATING_EFFECT);
+ builder.setRepeating(parseEffect(parser, TAG_REPEATING, flags));
+
+ XmlValidator.checkParserCondition(builder.hasRepeatingSegment(),
+ "Unexpected %s tag with no repeating segment", TAG_REPEATING_EFFECT);
+
+ // Consume tag
+ XmlReader.readEndTag(parser, TAG_REPEATING_EFFECT, outerDepth);
+
+ return builder.build();
+ }
+
+ private static SerializedComposedEffect parseEffect(TypedXmlPullParser parser,
+ String tagName, int flags) throws XmlParserException, IOException {
+ XmlValidator.checkStartTag(parser, tagName);
+ XmlValidator.checkTagHasNoUnexpectedAttributes(parser);
+ int vibrationTagDepth = parser.getDepth();
+ XmlValidator.checkParserCondition(
+ XmlReader.readNextTagWithin(parser, vibrationTagDepth),
+ "Unsupported empty %s tag", tagName);
+
+ SerializedComposedEffect effect;
+ switch (parser.getName()) {
+ case TAG_PREDEFINED_EFFECT:
+ effect = new SerializedComposedEffect(
+ SerializedPredefinedEffect.Parser.parseNext(parser, flags));
+ break;
+ case TAG_PRIMITIVE_EFFECT:
+ effect = parsePrimitiveEffects(parser, vibrationTagDepth);
+ break;
+ case TAG_WAVEFORM_ENTRY:
+ effect = parseWaveformEntries(parser, vibrationTagDepth);
+ break;
+ case TAG_WAVEFORM_ENVELOPE_EFFECT:
+ effect = new SerializedComposedEffect(
+ SerializedWaveformEnvelopeEffect.Parser.parseNext(parser, flags));
+ break;
+ case TAG_BASIC_ENVELOPE_EFFECT:
+ effect = new SerializedComposedEffect(
+ SerializedBasicEnvelopeEffect.Parser.parseNext(parser, flags));
+ break;
+ default:
+ throw new XmlParserException("Unexpected tag " + parser.getName()
+ + " in vibration tag " + tagName);
+ }
+
+ // Consume tag
+ XmlReader.readEndTag(parser, tagName, vibrationTagDepth);
+
+ return effect;
+ }
+
+ private static SerializedComposedEffect parsePrimitiveEffects(TypedXmlPullParser parser,
+ int vibrationTagDepth)
+ throws IOException, XmlParserException {
+ List<SerializedComposedEffect.SerializedSegment> primitives = new ArrayList<>();
+ do { // First primitive tag already open
+ primitives.add(SerializedCompositionPrimitive.Parser.parseNext(parser));
+ } while (XmlReader.readNextTagWithin(parser, vibrationTagDepth));
+ return new SerializedComposedEffect(primitives.toArray(
+ new SerializedComposedEffect.SerializedSegment[
+ primitives.size()]));
+ }
+
+ private static SerializedComposedEffect parseWaveformEntries(TypedXmlPullParser parser,
+ int vibrationTagDepth)
+ throws IOException, XmlParserException {
+ SerializedWaveformEffectEntries.Builder waveformBuilder =
+ new SerializedWaveformEffectEntries.Builder();
+ do { // First waveform-entry tag already open
+ SerializedWaveformEffectEntries
+ .Parser.parseWaveformEntry(parser, waveformBuilder);
+ } while (XmlReader.readNextTagWithin(parser, vibrationTagDepth));
+ XmlValidator.checkParserCondition(waveformBuilder.hasNonZeroDuration(),
+ "Unexpected %s tag with total duration zero", TAG_WAVEFORM_ENTRY);
+ return new SerializedComposedEffect(waveformBuilder.build());
+ }
+ }
+}
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedWaveformEffectEntries.java b/core/java/com/android/internal/vibrator/persistence/SerializedWaveformEffectEntries.java
new file mode 100644
index 000000000000..8849e75e7891
--- /dev/null
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedWaveformEffectEntries.java
@@ -0,0 +1,121 @@
+/*
+ * 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.NAMESPACE;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_ENTRY;
+import static com.android.internal.vibrator.persistence.XmlConstants.VALUE_AMPLITUDE_DEFAULT;
+
+import android.annotation.NonNull;
+import android.os.VibrationEffect;
+import android.util.IntArray;
+import android.util.LongArray;
+
+import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Serialized representation of a list of waveform entries created via
+ * {@link VibrationEffect#createWaveform(long[], int[], int)}.
+ *
+ * @hide
+ */
+final class SerializedWaveformEffectEntries implements SerializedSegment {
+
+ @NonNull
+ private final long[] mTimings;
+ @NonNull
+ private final int[] mAmplitudes;
+
+ private SerializedWaveformEffectEntries(@NonNull long[] timings,
+ @NonNull int[] amplitudes) {
+ mTimings = timings;
+ mAmplitudes = amplitudes;
+ }
+
+ @Override
+ public void deserializeIntoComposition(@NonNull VibrationEffect.Composition composition) {
+ composition.addEffect(VibrationEffect.createWaveform(mTimings, mAmplitudes, -1));
+ }
+
+ @Override
+ public void write(@NonNull TypedXmlSerializer serializer) throws IOException {
+ for (int i = 0; i < mTimings.length; i++) {
+ serializer.startTag(NAMESPACE, TAG_WAVEFORM_ENTRY);
+
+ if (mAmplitudes[i] == VibrationEffect.DEFAULT_AMPLITUDE) {
+ serializer.attribute(NAMESPACE, ATTRIBUTE_AMPLITUDE, VALUE_AMPLITUDE_DEFAULT);
+ } else {
+ serializer.attributeInt(NAMESPACE, ATTRIBUTE_AMPLITUDE, mAmplitudes[i]);
+ }
+
+ serializer.attributeLong(NAMESPACE, ATTRIBUTE_DURATION_MS, mTimings[i]);
+ serializer.endTag(NAMESPACE, TAG_WAVEFORM_ENTRY);
+ }
+
+ }
+
+ @Override
+ public String toString() {
+ return "SerializedWaveformEffectEntries{"
+ + "timings=" + Arrays.toString(mTimings)
+ + ", amplitudes=" + Arrays.toString(mAmplitudes)
+ + '}';
+ }
+
+ /** Builder for {@link SerializedWaveformEffectEntries}. */
+ static final class Builder {
+ private final LongArray mTimings = new LongArray();
+ private final IntArray mAmplitudes = new IntArray();
+
+ void addDurationAndAmplitude(long durationMs, int amplitude) {
+ mTimings.add(durationMs);
+ mAmplitudes.add(amplitude);
+ }
+
+ boolean hasNonZeroDuration() {
+ for (int i = 0; i < mTimings.size(); i++) {
+ if (mTimings.get(i) > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ SerializedWaveformEffectEntries build() {
+ return new SerializedWaveformEffectEntries(
+ mTimings.toArray(), mAmplitudes.toArray());
+ }
+ }
+
+ /** Parser implementation for the {@link XmlConstants#TAG_WAVEFORM_ENTRY}. */
+ static final class Parser {
+
+ /** Parses a single {@link XmlConstants#TAG_WAVEFORM_ENTRY} into the builder. */
+ public static void parseWaveformEntry(TypedXmlPullParser parser, Builder waveformBuilder)
+ throws XmlParserException, IOException {
+ SerializedAmplitudeStepWaveform.Parser.parseWaveformEntry(parser,
+ waveformBuilder::addDurationAndAmplitude);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectSerializer.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectSerializer.java
new file mode 100644
index 000000000000..df483ecdf881
--- /dev/null
+++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectSerializer.java
@@ -0,0 +1,336 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+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;
+
+import java.util.List;
+import java.util.function.BiConsumer;
+
+/**
+ * Serializer implementation for {@link VibrationEffect}.
+ *
+ * <p>This serializer does not support effects created with {@link VibrationEffect.WaveformBuilder}
+ * nor {@link VibrationEffect.Composition#addEffect(VibrationEffect)}. It only supports vibration
+ * effects defined as:
+ *
+ * <ul>
+ * <li>{@link VibrationEffect#createPredefined(int)}
+ * <li>{@link VibrationEffect#createWaveform(long[], int[], int)}
+ * <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>
+ *
+ * <p>This serializer also supports repeating effects. For repeating waveform effects, it attempts
+ * to serialize the effect as a single unit. If this fails, it falls back to serializing it as a
+ * sequence of individual waveform entries.
+ *
+ * @hide
+ */
+public class VibrationEffectSerializer {
+ private static final String TAG = "VibrationEffectSerializer";
+
+ /**
+ * Creates a serialized representation of the input {@code vibration}.
+ */
+ @NonNull
+ public static XmlSerializedVibration<? extends VibrationEffect> serialize(
+ @NonNull VibrationEffect vibration, @XmlConstants.Flags int flags)
+ throws XmlSerializerException {
+
+ if (Flags.vendorVibrationEffects()
+ && (vibration instanceof VibrationEffect.VendorEffect vendorEffect)) {
+ return serializeVendorEffect(vendorEffect);
+ }
+
+ XmlValidator.checkSerializerCondition(vibration instanceof VibrationEffect.Composed,
+ "Unsupported VibrationEffect type %s", vibration);
+
+ VibrationEffect.Composed composed = (VibrationEffect.Composed) vibration;
+ XmlValidator.checkSerializerCondition(!composed.getSegments().isEmpty(),
+ "Unsupported empty VibrationEffect %s", vibration);
+
+ List<VibrationEffectSegment> segments = composed.getSegments();
+ int repeatIndex = composed.getRepeatIndex();
+
+ SerializedComposedEffect serializedEffect;
+ if (repeatIndex >= 0) {
+ serializedEffect = trySerializeRepeatingAmplitudeWaveformEffect(segments, repeatIndex);
+ if (serializedEffect == null) {
+ serializedEffect = serializeRepeatingEffect(segments, repeatIndex, flags);
+ }
+ } else {
+ serializedEffect = serializeNonRepeatingEffect(segments, flags);
+ }
+
+ return serializedEffect;
+ }
+
+ private static SerializedComposedEffect serializeRepeatingEffect(
+ List<VibrationEffectSegment> segments, int repeatIndex, @XmlConstants.Flags int flags)
+ throws XmlSerializerException {
+
+ SerializedRepeatingEffect.Builder builder = new SerializedRepeatingEffect.Builder();
+ if (repeatIndex > 0) {
+ List<VibrationEffectSegment> preambleSegments = segments.subList(0, repeatIndex);
+ builder.setPreamble(serializeEffectEntries(preambleSegments, flags));
+
+ // Update segments to match the repeating block only, after preamble was consumed.
+ segments = segments.subList(repeatIndex, segments.size());
+ }
+
+ builder.setRepeating(serializeEffectEntries(segments, flags));
+
+ return new SerializedComposedEffect(builder.build());
+ }
+
+ @NonNull
+ private static SerializedComposedEffect serializeNonRepeatingEffect(
+ List<VibrationEffectSegment> segments, @XmlConstants.Flags int flags)
+ throws XmlSerializerException {
+ SerializedComposedEffect effect = trySerializeNonWaveformEffect(segments, flags);
+ if (effect == null) {
+ effect = serializeWaveformEffect(segments);
+ }
+
+ return effect;
+ }
+
+ @NonNull
+ private static SerializedComposedEffect serializeEffectEntries(
+ List<VibrationEffectSegment> segments, @XmlConstants.Flags int flags)
+ throws XmlSerializerException {
+ SerializedComposedEffect effect = trySerializeNonWaveformEffect(segments, flags);
+ if (effect == null) {
+ effect = serializeWaveformEffectEntries(segments);
+ }
+
+ return effect;
+ }
+
+ @Nullable
+ private static SerializedComposedEffect trySerializeNonWaveformEffect(
+ List<VibrationEffectSegment> segments, int flags) throws XmlSerializerException {
+ VibrationEffectSegment firstSegment = segments.getFirst();
+
+ if (firstSegment instanceof PrebakedSegment) {
+ return serializePredefinedEffect(segments, flags);
+ }
+ if (firstSegment instanceof PrimitiveSegment) {
+ return serializePrimitiveEffect(segments);
+ }
+ if (firstSegment instanceof PwleSegment) {
+ return serializeWaveformEnvelopeEffect(segments);
+ }
+ if (firstSegment instanceof BasicPwleSegment) {
+ return serializeBasicEnvelopeEffect(segments);
+ }
+
+ return null;
+ }
+
+ private static SerializedComposedEffect serializePredefinedEffect(
+ List<VibrationEffectSegment> segments, @XmlConstants.Flags int flags)
+ throws XmlSerializerException {
+ XmlValidator.checkSerializerCondition(segments.size() == 1,
+ "Unsupported multiple segments in predefined effect: %s", segments);
+ return new SerializedComposedEffect(serializePrebakedSegment(segments.getFirst(), flags));
+ }
+
+ private static SerializedVendorEffect serializeVendorEffect(
+ VibrationEffect.VendorEffect effect) {
+ return new SerializedVendorEffect(effect.getVendorData());
+ }
+
+ private static SerializedComposedEffect serializePrimitiveEffect(
+ List<VibrationEffectSegment> segments) throws XmlSerializerException {
+ SerializedComposedEffect.SerializedSegment[] primitives =
+ new SerializedComposedEffect.SerializedSegment[segments.size()];
+ for (int i = 0; i < segments.size(); i++) {
+ primitives[i] = serializePrimitiveSegment(segments.get(i));
+ }
+
+ return new SerializedComposedEffect(primitives);
+ }
+
+ private static SerializedComposedEffect serializeWaveformEnvelopeEffect(
+ List<VibrationEffectSegment> segments) throws XmlSerializerException {
+ SerializedWaveformEnvelopeEffect.Builder builder =
+ new SerializedWaveformEnvelopeEffect.Builder();
+ 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(
+ List<VibrationEffectSegment> segments) throws XmlSerializerException {
+ SerializedBasicEnvelopeEffect.Builder builder = new SerializedBasicEnvelopeEffect.Builder();
+ 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 trySerializeRepeatingAmplitudeWaveformEffect(
+ List<VibrationEffectSegment> segments, int repeatingIndex) {
+ SerializedAmplitudeStepWaveform.Builder builder =
+ new SerializedAmplitudeStepWaveform.Builder();
+
+ for (int i = 0; i < segments.size(); i++) {
+ if (repeatingIndex == i) {
+ builder.setRepeatIndexToCurrentEntry();
+ }
+ try {
+ serializeStepSegment(segments.get(i), builder::addDurationAndAmplitude);
+ } catch (XmlSerializerException e) {
+ return null;
+ }
+ }
+
+ return new SerializedComposedEffect(builder.build());
+ }
+
+ private static SerializedComposedEffect serializeWaveformEffect(
+ List<VibrationEffectSegment> segments) throws XmlSerializerException {
+ SerializedAmplitudeStepWaveform.Builder builder =
+ new SerializedAmplitudeStepWaveform.Builder();
+ for (int i = 0; i < segments.size(); i++) {
+ serializeStepSegment(segments.get(i), builder::addDurationAndAmplitude);
+ }
+
+ return new SerializedComposedEffect(builder.build());
+ }
+
+ private static SerializedComposedEffect serializeWaveformEffectEntries(
+ List<VibrationEffectSegment> segments) throws XmlSerializerException {
+ SerializedWaveformEffectEntries.Builder builder =
+ new SerializedWaveformEffectEntries.Builder();
+ for (int i = 0; i < segments.size(); i++) {
+ serializeStepSegment(segments.get(i), builder::addDurationAndAmplitude);
+ }
+
+ return new SerializedComposedEffect(builder.build());
+ }
+
+ private static void serializeStepSegment(VibrationEffectSegment segment,
+ BiConsumer<Long, Integer> builder) throws XmlSerializerException {
+ XmlValidator.checkSerializerCondition(segment instanceof StepSegment,
+ "Unsupported segment for waveform effect %s", segment);
+
+ XmlValidator.checkSerializerCondition(
+ Float.compare(((StepSegment) segment).getFrequencyHz(), 0) == 0,
+ "Unsupported segment with non-default frequency %f",
+ ((StepSegment) segment).getFrequencyHz());
+
+ builder.accept(segment.getDuration(),
+ toAmplitudeInt(((StepSegment) segment).getAmplitude()));
+ }
+
+ private static SerializedPredefinedEffect serializePrebakedSegment(
+ VibrationEffectSegment segment, @XmlConstants.Flags int flags)
+ throws XmlSerializerException {
+ XmlValidator.checkSerializerCondition(segment instanceof PrebakedSegment,
+ "Unsupported segment for predefined effect %s", segment);
+
+ PrebakedSegment prebaked = (PrebakedSegment) segment;
+ XmlConstants.PredefinedEffectName effectName = XmlConstants.PredefinedEffectName.findById(
+ prebaked.getEffectId(), flags);
+
+ XmlValidator.checkSerializerCondition(effectName != null,
+ "Unsupported predefined effect id %s", prebaked.getEffectId());
+
+ if ((flags & XmlConstants.FLAG_ALLOW_HIDDEN_APIS) == 0) {
+ // Only allow effects with default fallback flag if using the public APIs schema.
+ XmlValidator.checkSerializerCondition(
+ prebaked.shouldFallback() == PrebakedSegment.DEFAULT_SHOULD_FALLBACK,
+ "Unsupported predefined effect with should fallback %s",
+ prebaked.shouldFallback());
+ }
+
+ return new SerializedPredefinedEffect(effectName, prebaked.shouldFallback());
+ }
+
+ private static SerializedCompositionPrimitive serializePrimitiveSegment(
+ VibrationEffectSegment segment) throws XmlSerializerException {
+ XmlValidator.checkSerializerCondition(segment instanceof PrimitiveSegment,
+ "Unsupported segment for primitive composition %s", segment);
+
+ PrimitiveSegment primitive = (PrimitiveSegment) segment;
+ XmlConstants.PrimitiveEffectName primitiveName =
+ XmlConstants.PrimitiveEffectName.findById(primitive.getPrimitiveId());
+
+ XmlValidator.checkSerializerCondition(primitiveName != null,
+ "Unsupported primitive effect id %s", primitive.getPrimitiveId());
+
+ XmlConstants.PrimitiveDelayType delayType = null;
+
+ if (Flags.primitiveCompositionAbsoluteDelay()) {
+ delayType = XmlConstants.PrimitiveDelayType.findByType(primitive.getDelayType());
+ XmlValidator.checkSerializerCondition(delayType != null,
+ "Unsupported primitive delay type %s", primitive.getDelayType());
+ } else {
+ XmlValidator.checkSerializerCondition(
+ primitive.getDelayType() == PrimitiveSegment.DEFAULT_DELAY_TYPE,
+ "Unsupported primitive delay type %s", primitive.getDelayType());
+ }
+
+ return new SerializedCompositionPrimitive(
+ primitiveName, primitive.getScale(), primitive.getDelay(), delayType);
+ }
+
+ private static int toAmplitudeInt(float amplitude) {
+ return Float.compare(amplitude, VibrationEffect.DEFAULT_AMPLITUDE) == 0
+ ? VibrationEffect.DEFAULT_AMPLITUDE
+ : Math.round(amplitude * VibrationEffect.MAX_AMPLITUDE);
+ }
+}
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
index 314bfe40ee0b..efd75fc17cb7 100644
--- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
+++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
@@ -19,6 +19,7 @@ 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_REPEATING_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;
@@ -120,6 +121,26 @@ import java.util.List;
* }
* </pre>
*
+ * * Repeating effects
+ *
+ * <pre>
+ * {@code
+ * <vibration-effect>
+ * <repeating-effect>
+ * <preamble>
+ * <primitive-effect name="click" />
+ * </preamble>
+ * <repeating>
+ * <basic-envelope-effect>
+ * <control-point intensity="0.3" sharpness="0.4" durationMs="25" />
+ * <control-point intensity="0.0" sharpness="0.5" durationMs="30" />
+ * </basic-envelope-effect>
+ * </repeating>
+ * </repeating-effect>
+ * </vibration-effect>
+ * }
+ * </pre>
+ *
* @hide
*/
public class VibrationEffectXmlParser {
@@ -191,6 +212,12 @@ public class VibrationEffectXmlParser {
SerializedBasicEnvelopeEffect.Parser.parseNext(parser, flags));
break;
} // else fall through
+ case TAG_REPEATING_EFFECT:
+ if (Flags.normalizedPwleEffects()) {
+ serializedVibration = new SerializedComposedEffect(
+ SerializedRepeatingEffect.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/XmlConstants.java b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
index df262cfecd5a..cc5c7cfb4683 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
@@ -45,8 +45,10 @@ public final class XmlConstants {
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_REPEATING_EFFECT = "repeating-effect";
public static final String TAG_WAVEFORM_ENTRY = "waveform-entry";
public static final String TAG_REPEATING = "repeating";
+ public static final String TAG_PREAMBLE = "preamble";
public static final String TAG_CONTROL_POINT = "control-point";
public static final String ATTRIBUTE_NAME = "name";
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 5f25e9315831..c05888560f10 100644
--- a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
@@ -931,6 +931,545 @@ public class VibrationEffectXmlSerializationTest {
}
@Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testRepeating_withWaveformEnvelopeEffect_allSucceed() throws Exception {
+ VibrationEffect preamble = new VibrationEffect.WaveformEnvelopeBuilder()
+ .addControlPoint(0.1f, 50f, 10)
+ .addControlPoint(0.2f, 60f, 20)
+ .build();
+ VibrationEffect repeating = new VibrationEffect.WaveformEnvelopeBuilder()
+ .setInitialFrequencyHz(70f)
+ .addControlPoint(0.3f, 80f, 25)
+ .addControlPoint(0.4f, 90f, 30)
+ .build();
+ VibrationEffect effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+
+ String xml = """
+ <vibration-effect>
+ <repeating-effect>
+ <preamble>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.1" frequencyHz="50.0" durationMs="10"/>
+ <control-point amplitude="0.2" frequencyHz="60.0" durationMs="20"/>
+ </waveform-envelope-effect>
+ </preamble>
+ <repeating>
+ <waveform-envelope-effect initialFrequencyHz="70.0">
+ <control-point amplitude="0.3" frequencyHz="80.0" durationMs="25"/>
+ <control-point amplitude="0.4" frequencyHz="90.0" durationMs="30"/>
+ </waveform-envelope-effect>
+ </repeating>
+ </repeating-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, "0.1", "0.2", "0.3", "0.4", "50.0", "60.0",
+ "70.0", "80.0", "90.0", "10", "20", "25", "30");
+ assertPublicApisRoundTrip(effect);
+
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, "0.1", "0.2", "0.3", "0.4", "50.0", "60.0",
+ "70.0", "80.0", "90.0", "10", "20", "25", "30");
+ assertHiddenApisRoundTrip(effect);
+
+ effect = VibrationEffect.createRepeatingEffect(repeating);
+
+ xml = """
+ <vibration-effect>
+ <repeating-effect>
+ <repeating>
+ <waveform-envelope-effect initialFrequencyHz="70.0">
+ <control-point amplitude="0.3" frequencyHz="80.0" durationMs="25"/>
+ <control-point amplitude="0.4" frequencyHz="90.0" durationMs="30"/>
+ </waveform-envelope-effect>
+ </repeating>
+ </repeating-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, "0.3", "0.4", "70.0", "80.0", "90.0", "25",
+ "30");
+ assertPublicApisRoundTrip(effect);
+
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, "0.3", "0.4", "70.0", "80.0", "90.0", "25",
+ "30");
+ assertHiddenApisRoundTrip(effect);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testRepeating_withBasicEnvelopeEffect_allSucceed() throws Exception {
+ VibrationEffect preamble = new VibrationEffect.BasicEnvelopeBuilder()
+ .addControlPoint(0.1f, 0.1f, 10)
+ .addControlPoint(0.2f, 0.2f, 20)
+ .addControlPoint(0.0f, 0.2f, 20)
+ .build();
+ VibrationEffect repeating = new VibrationEffect.BasicEnvelopeBuilder()
+ .setInitialSharpness(0.3f)
+ .addControlPoint(0.3f, 0.4f, 25)
+ .addControlPoint(0.4f, 0.6f, 30)
+ .addControlPoint(0.0f, 0.7f, 35)
+ .build();
+ VibrationEffect effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+
+ String xml = """
+ <vibration-effect>
+ <repeating-effect>
+ <preamble>
+ <basic-envelope-effect>
+ <control-point intensity="0.1" sharpness="0.1" durationMs="10" />
+ <control-point intensity="0.2" sharpness="0.2" durationMs="20" />
+ <control-point intensity="0.0" sharpness="0.2" durationMs="20" />
+ </basic-envelope-effect>
+ </preamble>
+ <repeating>
+ <basic-envelope-effect initialSharpness="0.3">
+ <control-point intensity="0.3" sharpness="0.4" durationMs="25" />
+ <control-point intensity="0.4" sharpness="0.6" durationMs="30" />
+ <control-point intensity="0.0" sharpness="0.7" durationMs="35" />
+ </basic-envelope-effect>
+ </repeating>
+ </repeating-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, "0.0", "0.1", "0.2", "0.3", "0.4", "0.1", "0.2",
+ "0.3", "0.4", "0.6", "0.7", "10", "20", "25", "30", "35");
+ assertPublicApisRoundTrip(effect);
+
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, "0.0", "0.1", "0.2", "0.3", "0.4", "0.1", "0.2",
+ "0.3", "0.4", "0.6", "0.7", "10", "20", "25", "30", "35");
+ assertHiddenApisRoundTrip(effect);
+
+ effect = VibrationEffect.createRepeatingEffect(repeating);
+
+ xml = """
+ <vibration-effect>
+ <repeating-effect>
+ <repeating>
+ <basic-envelope-effect initialSharpness="0.3">
+ <control-point intensity="0.3" sharpness="0.4" durationMs="25" />
+ <control-point intensity="0.4" sharpness="0.6" durationMs="30" />
+ <control-point intensity="0.0" sharpness="0.7" durationMs="35" />
+ </basic-envelope-effect>
+ </repeating>
+ </repeating-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, "0.3", "0.4", "0.0", "0.4", "0.6", "0.7", "25",
+ "30", "35");
+ assertPublicApisRoundTrip(effect);
+
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, "0.3", "0.4", "0.0", "0.4", "0.6", "0.7", "25",
+ "30", "35");
+ assertHiddenApisRoundTrip(effect);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testRepeating_withPredefinedEffects_allSucceed() throws Exception {
+ for (Map.Entry<String, Integer> entry : createPublicPredefinedEffectsMap().entrySet()) {
+ VibrationEffect preamble = VibrationEffect.get(entry.getValue());
+ VibrationEffect repeating = VibrationEffect.get(entry.getValue());
+ VibrationEffect effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+ String xml = String.format("""
+ <vibration-effect>
+ <repeating-effect>
+ <preamble>
+ <predefined-effect name="%s"/>
+ </preamble>
+ <repeating>
+ <predefined-effect name="%s"/>
+ </repeating>
+ </repeating-effect>
+ </vibration-effect>
+ """,
+ entry.getKey(), entry.getKey());
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, entry.getKey());
+ assertPublicApisRoundTrip(effect);
+
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, entry.getKey());
+ assertHiddenApisRoundTrip(effect);
+
+ effect = VibrationEffect.createRepeatingEffect(repeating);
+ xml = String.format("""
+ <vibration-effect>
+ <repeating-effect>
+ <repeating>
+ <predefined-effect name="%s"/>
+ </repeating>
+ </repeating-effect>
+ </vibration-effect>
+ """,
+ entry.getKey());
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, entry.getKey());
+ assertPublicApisRoundTrip(effect);
+
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, entry.getKey());
+ assertHiddenApisRoundTrip(effect);
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testRepeating_withWaveformEntry_allSucceed() throws Exception {
+ VibrationEffect preamble = VibrationEffect.createWaveform(new long[]{123, 456, 789, 0},
+ new int[]{254, 1, 255, 0}, /* repeat= */ -1);
+ VibrationEffect repeating = VibrationEffect.createWaveform(new long[]{123, 456, 789, 0},
+ new int[]{254, 1, 255, 0}, /* repeat= */ -1);
+ VibrationEffect effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+
+ String xml = """
+ <vibration-effect>
+ <repeating-effect>
+ <preamble>
+ <waveform-entry durationMs="123" amplitude="254"/>
+ <waveform-entry durationMs="456" amplitude="1"/>
+ <waveform-entry durationMs="789" amplitude="255"/>
+ <waveform-entry durationMs="0" amplitude="0"/>
+ </preamble>
+ <repeating>
+ <waveform-entry durationMs="123" amplitude="254"/>
+ <waveform-entry durationMs="456" amplitude="1"/>
+ <waveform-entry durationMs="789" amplitude="255"/>
+ <waveform-entry durationMs="0" amplitude="0"/>
+ </repeating>
+ </repeating-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0");
+ assertPublicApisRoundTrip(effect);
+
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0");
+ assertHiddenApisRoundTrip(effect);
+
+ xml = """
+ <vibration-effect>
+ <repeating-effect>
+ <repeating>
+ <waveform-entry durationMs="123" amplitude="254"/>
+ <waveform-entry durationMs="456" amplitude="1"/>
+ <waveform-entry durationMs="789" amplitude="255"/>
+ <waveform-entry durationMs="0" amplitude="0"/>
+ </repeating>
+ </repeating-effect>
+ </vibration-effect>
+ """;
+
+ effect = VibrationEffect.createRepeatingEffect(repeating);
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0");
+ assertPublicApisRoundTrip(effect);
+
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0");
+ assertHiddenApisRoundTrip(effect);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testRepeating_withPrimitives_allSucceed() throws Exception {
+ VibrationEffect preamble = VibrationEffect.startComposition()
+ .addPrimitive(PRIMITIVE_CLICK)
+ .addPrimitive(PRIMITIVE_TICK, 0.2497f)
+ .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
+ .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
+ .compose();
+ VibrationEffect repeating = VibrationEffect.startComposition()
+ .addPrimitive(PRIMITIVE_CLICK)
+ .addPrimitive(PRIMITIVE_TICK, 0.2497f)
+ .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
+ .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
+ .compose();
+ VibrationEffect effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+
+ String xml = """
+ <vibration-effect>
+ <repeating-effect>
+ <preamble>
+ <primitive-effect name="click" />
+ <primitive-effect name="tick" scale="0.2497" />
+ <primitive-effect name="low_tick" delayMs="356" />
+ <primitive-effect name="spin" scale="0.6364" delayMs="7" />
+ </preamble>
+ <repeating>
+ <primitive-effect name="click" />
+ <primitive-effect name="tick" scale="0.2497" />
+ <primitive-effect name="low_tick" delayMs="356" />
+ <primitive-effect name="spin" scale="0.6364" delayMs="7" />
+ </repeating>
+ </repeating-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin");
+ assertPublicApisRoundTrip(effect);
+
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin");
+ assertHiddenApisRoundTrip(effect);
+
+ repeating = VibrationEffect.startComposition()
+ .addPrimitive(PRIMITIVE_CLICK)
+ .addPrimitive(PRIMITIVE_TICK, 0.2497f)
+ .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
+ .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
+ .compose();
+ effect = VibrationEffect.createRepeatingEffect(repeating);
+
+ xml = """
+ <vibration-effect>
+ <repeating-effect>
+ <repeating>
+ <primitive-effect name="click" />
+ <primitive-effect name="tick" scale="0.2497" />
+ <primitive-effect name="low_tick" delayMs="356" />
+ <primitive-effect name="spin" scale="0.6364" delayMs="7" />
+ </repeating>
+ </repeating-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin");
+ assertPublicApisRoundTrip(effect);
+
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin");
+ assertHiddenApisRoundTrip(effect);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testRepeating_withMixedVibrations_allSucceed() throws Exception {
+ VibrationEffect preamble = new VibrationEffect.WaveformEnvelopeBuilder()
+ .addControlPoint(0.1f, 50f, 10)
+ .build();
+ VibrationEffect repeating = VibrationEffect.get(VibrationEffect.EFFECT_TICK);
+ VibrationEffect effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+ String xml = """
+ <vibration-effect>
+ <repeating-effect>
+ <preamble>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.1" frequencyHz="50.0" durationMs="10"/>
+ </waveform-envelope-effect>
+ </preamble>
+ <repeating>
+ <predefined-effect name="tick"/>
+ </repeating>
+ </repeating-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, "0.1", "50.0", "10", "tick");
+ assertPublicApisRoundTrip(effect);
+
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, "0.1", "50.0", "10", "tick");
+ assertHiddenApisRoundTrip(effect);
+
+ preamble = VibrationEffect.createWaveform(new long[]{123, 456},
+ new int[]{254, 1}, /* repeat= */ -1);
+ repeating = new VibrationEffect.BasicEnvelopeBuilder()
+ .addControlPoint(0.3f, 0.4f, 25)
+ .addControlPoint(0.0f, 0.5f, 30)
+ .build();
+ effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+
+ xml = """
+ <vibration-effect>
+ <repeating-effect>
+ <preamble>
+ <waveform-entry durationMs="123" amplitude="254"/>
+ <waveform-entry durationMs="456" amplitude="1"/>
+ </preamble>
+ <repeating>
+ <basic-envelope-effect>
+ <control-point intensity="0.3" sharpness="0.4" durationMs="25" />
+ <control-point intensity="0.0" sharpness="0.5" durationMs="30" />
+ </basic-envelope-effect>
+ </repeating>
+ </repeating-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, "123", "456", "254", "1", "0.3", "0.0", "0.4",
+ "0.5", "25", "30");
+ assertPublicApisRoundTrip(effect);
+
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, "123", "456", "254", "1", "0.3", "0.0", "0.4",
+ "0.5", "25", "30");
+ assertHiddenApisRoundTrip(effect);
+
+ preamble = VibrationEffect.startComposition()
+ .addPrimitive(PRIMITIVE_CLICK)
+ .compose();
+ effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+
+ xml = """
+ <vibration-effect>
+ <repeating-effect>
+ <preamble>
+ <primitive-effect name="click" />
+ </preamble>
+ <repeating>
+ <basic-envelope-effect>
+ <control-point intensity="0.3" sharpness="0.4" durationMs="25" />
+ <control-point intensity="0.0" sharpness="0.5" durationMs="30" />
+ </basic-envelope-effect>
+ </repeating>
+ </repeating-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, "click", "0.3", "0.4", "0.0", "0.5", "25", "30");
+ assertPublicApisRoundTrip(effect);
+
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, "click", "0.3", "0.4", "0.0", "0.5", "25", "30");
+ assertHiddenApisRoundTrip(effect);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testRepeating_badXml_throwsException() throws IOException {
+ // Incomplete XML
+ assertParseElementFails("""
+ <vibration-effect>
+ <repeating-effect>
+ <preamble>
+ <primitive-effect name="click" />
+ </preamble>
+ <repeating>
+ <primitive-effect name="click" />
+ """);
+
+ assertParseElementFails("""
+ <vibration-effect>
+ <repeating-effect>
+ <primitive-effect name="click" />
+ <repeating>
+ <primitive-effect name="click" />
+ </repeating>
+ </repeating-effect>
+ </vibration-effect>
+ """);
+
+ assertParseElementFails("""
+ <vibration-effect>
+ <repeating-effect>
+ <preamble>
+ <primitive-effect name="click" />
+ </preamble>
+ <primitive-effect name="click" />
+ </repeating-effect>
+ </vibration-effect>
+ """);
+
+ // Bad vibration XML
+ assertParseElementFails("""
+ <vibration-effect>
+ <repeating-effect>
+ <repeating>
+ <primitive-effect name="click" />
+ </repeating>
+ <preamble>
+ <primitive-effect name="click" />
+ </preamble>
+ </repeating-effect>
+ </vibration-effect>
+ """);
+
+ assertParseElementFails("""
+ <vibration-effect>
+ <repeating-effect>
+ <repeating>
+ <preamble>
+ <primitive-effect name="click" />
+ </preamble>
+ <primitive-effect name="click" />
+ </repeating>
+ </repeating-effect>
+ </vibration-effect>
+ """);
+
+ assertParseElementFails("""
+ <vibration-effect>
+ <repeating-effect>
+ <preamble>
+ <primitive-effect name="click" />
+ <repeating>
+ <primitive-effect name="click" />
+ </repeating>
+ </preamble>
+ </repeating-effect>
+ </vibration-effect>
+ """);
+
+ assertParseElementFails("""
+ <vibration-effect>
+ <repeating-effect>
+ <primitive-effect name="click" />
+ <primitive-effect name="click" />
+ </repeating-effect>
+ </vibration-effect>
+ """);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testRepeatingEffect_featureFlagDisabled_allFail() throws Exception {
+ VibrationEffect repeating = VibrationEffect.startComposition()
+ .addPrimitive(PRIMITIVE_CLICK)
+ .addPrimitive(PRIMITIVE_TICK, 0.2497f)
+ .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
+ .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
+ .compose();
+ VibrationEffect effect = VibrationEffect.createRepeatingEffect(repeating);
+
+ String xml = """
+ <vibration-effect>
+ <repeating-effect>
+ <repeating>
+ <primitive-effect name="click" />
+ <primitive-effect name="tick" scale="0.2497" />
+ <primitive-effect name="low_tick" delayMs="356" />
+ <primitive-effect name="spin" scale="0.6364" delayMs="7" />
+ </repeating>
+ </repeating-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 29f8d199c1d1..89ca04432fa7 100644
--- a/core/xsd/vibrator/vibration/schema/current.txt
+++ b/core/xsd/vibrator/vibration/schema/current.txt
@@ -62,17 +62,41 @@ package com.android.internal.vibrator.persistence {
enum_constant public static final com.android.internal.vibrator.persistence.PrimitiveEffectName tick;
}
+ public class RepeatingEffect {
+ ctor public RepeatingEffect();
+ method public com.android.internal.vibrator.persistence.RepeatingEffectEntry getPreamble();
+ method public com.android.internal.vibrator.persistence.RepeatingEffectEntry getRepeating();
+ method public void setPreamble(com.android.internal.vibrator.persistence.RepeatingEffectEntry);
+ method public void setRepeating(com.android.internal.vibrator.persistence.RepeatingEffectEntry);
+ }
+
+ public class RepeatingEffectEntry {
+ ctor public RepeatingEffectEntry();
+ 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 com.android.internal.vibrator.persistence.WaveformEntry getWaveformEntry_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 setWaveformEntry_optional(com.android.internal.vibrator.persistence.WaveformEntry);
+ method public void setWaveformEnvelopeEffect_optional(com.android.internal.vibrator.persistence.WaveformEnvelopeEffect);
+ }
+
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 com.android.internal.vibrator.persistence.RepeatingEffect getRepeatingEffect_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 setRepeatingEffect_optional(com.android.internal.vibrator.persistence.RepeatingEffect);
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);
diff --git a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
index b4df2d187702..57bcde7c97d4 100644
--- a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
+++ b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
@@ -60,6 +60,30 @@
<!-- Basic envelope effect -->
<xs:element name="basic-envelope-effect" type="BasicEnvelopeEffect"/>
+ <!-- Repeating vibration effect -->
+ <xs:element name="repeating-effect" type="RepeatingEffect"/>
+
+ </xs:choice>
+ </xs:complexType>
+
+ <xs:complexType name="RepeatingEffect">
+ <xs:sequence>
+ <xs:element name="preamble" maxOccurs="1" minOccurs="0" type="RepeatingEffectEntry" />
+ <xs:element name="repeating" maxOccurs="1" minOccurs="1" type="RepeatingEffectEntry" />
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="RepeatingEffectEntry">
+ <xs:choice>
+ <xs:element name="predefined-effect" type="PredefinedEffect" />
+ <xs:element name="waveform-envelope-effect" type="WaveformEnvelopeEffect" />
+ <xs:element name="basic-envelope-effect" type="BasicEnvelopeEffect" />
+ <xs:sequence>
+ <xs:element name="waveform-entry" type="WaveformEntry" />
+ </xs:sequence>
+ <xs:sequence>
+ <xs:element name="primitive-effect" type="PrimitiveEffect" />
+ </xs:sequence>
</xs:choice>
</xs:complexType>
diff --git a/core/xsd/vibrator/vibration/vibration.xsd b/core/xsd/vibrator/vibration/vibration.xsd
index fba966faa9c9..c11fb667e709 100644
--- a/core/xsd/vibrator/vibration/vibration.xsd
+++ b/core/xsd/vibrator/vibration/vibration.xsd
@@ -58,9 +58,34 @@
<!-- Basic envelope effect -->
<xs:element name="basic-envelope-effect" type="BasicEnvelopeEffect"/>
+ <!-- Repeating vibration effect -->
+ <xs:element name="repeating-effect" type="RepeatingEffect"/>
+
+ </xs:choice>
+ </xs:complexType>
+
+ <xs:complexType name="RepeatingEffect">
+ <xs:sequence>
+ <xs:element name="preamble" maxOccurs="1" minOccurs="0" type="RepeatingEffectEntry" />
+ <xs:element name="repeating" maxOccurs="1" minOccurs="1" type="RepeatingEffectEntry" />
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="RepeatingEffectEntry">
+ <xs:choice>
+ <xs:element name="predefined-effect" type="PredefinedEffect" />
+ <xs:element name="waveform-envelope-effect" type="WaveformEnvelopeEffect" />
+ <xs:element name="basic-envelope-effect" type="BasicEnvelopeEffect" />
+ <xs:sequence>
+ <xs:element name="waveform-entry" type="WaveformEntry" />
+ </xs:sequence>
+ <xs:sequence>
+ <xs:element name="primitive-effect" type="PrimitiveEffect" />
+ </xs:sequence>
</xs:choice>
</xs:complexType>
+
<xs:complexType name="WaveformEffect">
<xs:sequence>