Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 77a809f1 authored by Ahmad Khalil's avatar Ahmad Khalil
Browse files

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
parent fb3baa77
Loading
Loading
Loading
Loading
+9 −2
Original line number Diff line number Diff line
@@ -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.
+1 −1
Original line number Diff line number Diff line
@@ -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}.
+22 −21
Original line number Diff line number Diff line
@@ -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);
        }
    }
}
+215 −0
Original line number Diff line number Diff line
/*
 * 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());
        }
    }
}
+121 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}
Loading