diff options
author | 2024-12-08 21:16:32 +0000 | |
---|---|---|
committer | 2024-12-13 17:09:58 +0000 | |
commit | 77a809f1d7b59749fad219ffb46ed1f63d6c8ca5 (patch) | |
tree | b347331cf50e18f501e6a0fdc45ba61e15981d8a | |
parent | fb3baa77b66a8a1a4cd4e92d6336c7f0ad0bce59 (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
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> |