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

Commit 5651b1ce authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add xml serialization of envelope vibration effects" into main

parents 19eaae56 7dba6f41
Loading
Loading
Loading
Loading
+184 −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_DURATION_MS;
import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_INITIAL_SHARPNESS;
import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_INTENSITY;
import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_SHARPNESS;
import static com.android.internal.vibrator.persistence.XmlConstants.NAMESPACE;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_BASIC_ENVELOPE_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_CONTROL_POINT;

import android.annotation.NonNull;
import android.os.VibrationEffect;

import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

/**
 * Serialized representation of a basic envelope effect created via
 * {@link VibrationEffect.BasicEnvelopeBuilder}.
 *
 * @hide
 */
final class SerializedBasicEnvelopeEffect implements SerializedComposedEffect.SerializedSegment {
    private final BasicControlPoint[] mControlPoints;
    private final float mInitialSharpness;

    SerializedBasicEnvelopeEffect(BasicControlPoint[] controlPoints, float initialSharpness) {
        mControlPoints = controlPoints;
        mInitialSharpness = initialSharpness;
    }

    @Override
    public void write(@NonNull TypedXmlSerializer serializer) throws IOException {
        serializer.startTag(NAMESPACE, TAG_BASIC_ENVELOPE_EFFECT);

        if (!Float.isNaN(mInitialSharpness)) {
            serializer.attributeFloat(NAMESPACE, ATTRIBUTE_INITIAL_SHARPNESS, mInitialSharpness);
        }

        for (BasicControlPoint point : mControlPoints) {
            serializer.startTag(NAMESPACE, TAG_CONTROL_POINT);
            serializer.attributeFloat(NAMESPACE, ATTRIBUTE_INTENSITY, point.mIntensity);
            serializer.attributeFloat(NAMESPACE, ATTRIBUTE_SHARPNESS, point.mSharpness);
            serializer.attributeLong(NAMESPACE, ATTRIBUTE_DURATION_MS, point.mDurationMs);
            serializer.endTag(NAMESPACE, TAG_CONTROL_POINT);
        }

        serializer.endTag(NAMESPACE, TAG_BASIC_ENVELOPE_EFFECT);
    }

    @Override
    public void deserializeIntoComposition(@NonNull VibrationEffect.Composition composition) {
        VibrationEffect.BasicEnvelopeBuilder builder = new VibrationEffect.BasicEnvelopeBuilder();

        if (!Float.isNaN(mInitialSharpness)) {
            builder.setInitialSharpness(mInitialSharpness);
        }

        for (BasicControlPoint point : mControlPoints) {
            builder.addControlPoint(point.mIntensity, point.mSharpness, point.mDurationMs);
        }
        composition.addEffect(builder.build());
    }

    @Override
    public String toString() {
        return "SerializedBasicEnvelopeEffect{"
                + "initialSharpness=" + (Float.isNaN(mInitialSharpness) ? "" : mInitialSharpness)
                + ", controlPoints=" + Arrays.toString(mControlPoints)
                + '}';
    }

    static final class Builder {
        private final List<BasicControlPoint> mControlPoints;
        private float mInitialSharpness = Float.NaN;

        Builder() {
            mControlPoints = new ArrayList<>();
        }

        void setInitialSharpness(float sharpness) {
            mInitialSharpness = sharpness;
        }

        void addControlPoint(float intensity, float sharpness, long durationMs) {
            mControlPoints.add(new BasicControlPoint(intensity, sharpness, durationMs));
        }

        SerializedBasicEnvelopeEffect build() {
            return new SerializedBasicEnvelopeEffect(
                    mControlPoints.toArray(new BasicControlPoint[0]), mInitialSharpness);
        }
    }

    /** Parser implementation for {@link SerializedBasicEnvelopeEffect}. */
    static final class Parser {

        @NonNull
        static SerializedBasicEnvelopeEffect parseNext(@NonNull TypedXmlPullParser parser,
                @XmlConstants.Flags int flags) throws XmlParserException, IOException {
            XmlValidator.checkStartTag(parser, TAG_BASIC_ENVELOPE_EFFECT);
            XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_INITIAL_SHARPNESS);

            Builder builder = new Builder();
            builder.setInitialSharpness(
                    XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_INITIAL_SHARPNESS, 0f, 1f,
                            Float.NaN));

            int outerDepth = parser.getDepth();

            // Read all nested tags
            while (XmlReader.readNextTagWithin(parser, outerDepth)) {
                parseControlPoint(parser, builder);
                // Consume tag
                XmlReader.readEndTag(parser);
            }

            // Check schema assertions about <basic-envelope-effect>
            XmlValidator.checkParserCondition(!builder.mControlPoints.isEmpty(),
                    "Expected tag %s to have at least one control point",
                    TAG_BASIC_ENVELOPE_EFFECT);
            XmlValidator.checkParserCondition(builder.mControlPoints.getLast().mIntensity == 0,
                    "Basic envelope effects must end at a zero intensity control point");

            return builder.build();
        }

        private static void parseControlPoint(TypedXmlPullParser parser, Builder builder)
                throws XmlParserException {
            XmlValidator.checkStartTag(parser, TAG_CONTROL_POINT);
            XmlValidator.checkTagHasNoUnexpectedAttributes(
                    parser, ATTRIBUTE_DURATION_MS, ATTRIBUTE_INTENSITY,
                    ATTRIBUTE_SHARPNESS);
            float intensity = XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_INTENSITY, 0,
                    1);
            float sharpness = XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_SHARPNESS, 0,
                    1);
            long durationMs = XmlReader.readAttributePositiveLong(parser, ATTRIBUTE_DURATION_MS);

            builder.addControlPoint(intensity, sharpness, durationMs);
        }
    }

    private static final class BasicControlPoint {
        private final float mIntensity;
        private final float mSharpness;
        private final long mDurationMs;

        BasicControlPoint(float intensity, float sharpness, long durationMs) {
            mIntensity = intensity;
            mSharpness = sharpness;
            mDurationMs = durationMs;
        }

        @Override
        public String toString() {
            return String.format(Locale.ROOT, "(%.2f, %.2f, %dms)", mIntensity, mSharpness,
                    mDurationMs);
        }
    }
}
+182 −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.ATTRIBUTE_FREQUENCY_HZ;
import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_INITIAL_FREQUENCY_HZ;
import static com.android.internal.vibrator.persistence.XmlConstants.NAMESPACE;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_CONTROL_POINT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_ENVELOPE_EFFECT;

import android.annotation.NonNull;
import android.os.VibrationEffect;

import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

/**
 * Serialized representation of a waveform envelope effect created via
 * {@link VibrationEffect.WaveformEnvelopeBuilder}.
 *
 * @hide
 */
final class SerializedWaveformEnvelopeEffect implements SerializedComposedEffect.SerializedSegment {

    private final WaveformControlPoint[] mControlPoints;
    private final float mInitialFrequency;

    SerializedWaveformEnvelopeEffect(WaveformControlPoint[] controlPoints, float initialFrequency) {
        mControlPoints = controlPoints;
        mInitialFrequency = initialFrequency;
    }

    @Override
    public void write(@NonNull TypedXmlSerializer serializer) throws IOException {
        serializer.startTag(NAMESPACE, TAG_WAVEFORM_ENVELOPE_EFFECT);

        if (!Float.isNaN(mInitialFrequency)) {
            serializer.attributeFloat(NAMESPACE, ATTRIBUTE_INITIAL_FREQUENCY_HZ, mInitialFrequency);
        }

        for (WaveformControlPoint point : mControlPoints) {
            serializer.startTag(NAMESPACE, TAG_CONTROL_POINT);
            serializer.attributeFloat(NAMESPACE, ATTRIBUTE_AMPLITUDE, point.mAmplitude);
            serializer.attributeFloat(NAMESPACE, ATTRIBUTE_FREQUENCY_HZ, point.mFrequency);
            serializer.attributeLong(NAMESPACE, ATTRIBUTE_DURATION_MS, point.mDurationMs);
            serializer.endTag(NAMESPACE, TAG_CONTROL_POINT);
        }

        serializer.endTag(NAMESPACE, TAG_WAVEFORM_ENVELOPE_EFFECT);
    }

    @Override
    public void deserializeIntoComposition(@NonNull VibrationEffect.Composition composition) {
        VibrationEffect.WaveformEnvelopeBuilder builder =
                new VibrationEffect.WaveformEnvelopeBuilder();

        if (!Float.isNaN(mInitialFrequency)) {
            builder.setInitialFrequencyHz(mInitialFrequency);
        }

        for (WaveformControlPoint point : mControlPoints) {
            builder.addControlPoint(point.mAmplitude, point.mFrequency, point.mDurationMs);
        }
        composition.addEffect(builder.build());
    }

    @Override
    public String toString() {
        return "SerializedWaveformEnvelopeEffect{"
                + "InitialFrequency=" + (Float.isNaN(mInitialFrequency) ? "" : mInitialFrequency)
                + ", controlPoints=" + Arrays.toString(mControlPoints)
                + '}';
    }

    static final class Builder {
        private final List<WaveformControlPoint> mControlPoints;
        private float mInitialFrequencyHz = Float.NaN;

        Builder() {
            mControlPoints = new ArrayList<>();
        }

        void setInitialFrequencyHz(float frequencyHz) {
            mInitialFrequencyHz = frequencyHz;
        }

        void addControlPoint(float amplitude, float frequencyHz, long durationMs) {
            mControlPoints.add(new WaveformControlPoint(amplitude, frequencyHz, durationMs));
        }

        SerializedWaveformEnvelopeEffect build() {
            return new SerializedWaveformEnvelopeEffect(
                    mControlPoints.toArray(new WaveformControlPoint[0]), mInitialFrequencyHz);
        }
    }

    /** Parser implementation for {@link SerializedWaveformEnvelopeEffect}. */
    static final class Parser {

        @NonNull
        static SerializedWaveformEnvelopeEffect parseNext(@NonNull TypedXmlPullParser parser,
                @XmlConstants.Flags int flags) throws XmlParserException, IOException {
            XmlValidator.checkStartTag(parser, TAG_WAVEFORM_ENVELOPE_EFFECT);
            XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_INITIAL_FREQUENCY_HZ);

            Builder builder = new Builder();
            builder.setInitialFrequencyHz(
                    XmlReader.readAttributePositiveFloat(parser, ATTRIBUTE_INITIAL_FREQUENCY_HZ,
                            Float.NaN));

            int outerDepth = parser.getDepth();

            while (XmlReader.readNextTagWithin(parser, outerDepth)) {
                parseControlPoint(parser, builder);
                // Consume tag
                XmlReader.readEndTag(parser);
            }

            // Check schema assertions about <waveform-envelope-effect>
            XmlValidator.checkParserCondition(!builder.mControlPoints.isEmpty(),
                    "Expected tag %s to have at least one control point",
                    TAG_WAVEFORM_ENVELOPE_EFFECT);

            return builder.build();
        }

        private static void parseControlPoint(TypedXmlPullParser parser, Builder builder)
                throws XmlParserException {
            XmlValidator.checkStartTag(parser, TAG_CONTROL_POINT);
            XmlValidator.checkTagHasNoUnexpectedAttributes(
                    parser, ATTRIBUTE_DURATION_MS, ATTRIBUTE_AMPLITUDE,
                    ATTRIBUTE_FREQUENCY_HZ);
            float amplitude = XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_AMPLITUDE, 0,
                    1);
            float frequencyHz = XmlReader.readAttributePositiveFloat(parser,
                    ATTRIBUTE_FREQUENCY_HZ);
            long durationMs = XmlReader.readAttributePositiveLong(parser, ATTRIBUTE_DURATION_MS);

            builder.addControlPoint(amplitude, frequencyHz, durationMs);
        }
    }

    private static final class WaveformControlPoint {
        private final float mAmplitude;
        private final float mFrequency;
        private final long mDurationMs;

        WaveformControlPoint(float amplitude, float frequency, long durationMs) {
            mAmplitude = amplitude;
            mFrequency = frequency;
            mDurationMs = durationMs;
        }

        @Override
        public String toString() {
            return String.format(Locale.ROOT, "(%.2f, %.2f, %dms)", mAmplitude, mFrequency,
                    mDurationMs);
        }
    }
}
+40 −0
Original line number Diff line number Diff line
@@ -16,11 +16,13 @@

package com.android.internal.vibrator.persistence;

import static com.android.internal.vibrator.persistence.XmlConstants.TAG_BASIC_ENVELOPE_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PREDEFINED_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PRIMITIVE_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VENDOR_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VIBRATION_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_ENVELOPE_EFFECT;

import android.annotation.NonNull;
import android.os.VibrationEffect;
@@ -92,6 +94,32 @@ import java.util.List;
 *   }
 * </pre>
 *
 * * Waveform Envelope effects
 *
 * <pre>
 *     {@code
 *       <vibration-effect>
 *         <waveform-envelope-effect initialFrequencyHz="20.0">
 *           <control-point amplitude="0.2" frequencyHz="80.0" durationMs="50" />
 *           <control-point amplitude="0.5" frequencyHz="150.0" durationMs="50" />
 *         </envelope-effect>
 *       </vibration-effect>
 *     }
 * </pre>
 *
 * * Basic Envelope effects
 *
 * <pre>
 *     {@code
 *       <vibration-effect>
 *         <basic-envelope-effect initialSharpness="0.3">
 *            <control-point intensity="0.2" sharpness="0.5" durationMs="50" />
 *            <control-point intensity="0.0" sharpness="1.0" durationMs="50" />
 *          </envelope-effect>
 *       </vibration-effect>
 *     }
 * </pre>
 *
 * @hide
 */
public class VibrationEffectXmlParser {
@@ -151,6 +179,18 @@ public class VibrationEffectXmlParser {
                serializedVibration = new SerializedComposedEffect(
                        SerializedAmplitudeStepWaveform.Parser.parseNext(parser));
                break;
            case TAG_WAVEFORM_ENVELOPE_EFFECT:
                if (Flags.normalizedPwleEffects()) {
                    serializedVibration = new SerializedComposedEffect(
                            SerializedWaveformEnvelopeEffect.Parser.parseNext(parser, flags));
                    break;
                } // else fall through
            case TAG_BASIC_ENVELOPE_EFFECT:
                if (Flags.normalizedPwleEffects()) {
                    serializedVibration = new SerializedComposedEffect(
                            SerializedBasicEnvelopeEffect.Parser.parseNext(parser, flags));
                    break;
                } // else fall through
            default:
                throw new XmlParserException("Unexpected tag " + parser.getName()
                        + " in vibration tag " + vibrationTagName);
+57 −0
Original line number Diff line number Diff line
@@ -19,9 +19,11 @@ package com.android.internal.vibrator.persistence;
import android.annotation.NonNull;
import android.os.PersistableBundle;
import android.os.VibrationEffect;
import android.os.vibrator.BasicPwleSegment;
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.PwleSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;

@@ -45,6 +47,8 @@ import java.util.List;
 *     <li>A composition created exclusively via
 *         {@link VibrationEffect.Composition#addPrimitive(int, float, int)}
 *     <li>{@link VibrationEffect#createVendorEffect(PersistableBundle)}
 *     <li>{@link VibrationEffect.WaveformEnvelopeBuilder}
 *     <li>{@link VibrationEffect.BasicEnvelopeBuilder}
 * </ul>
 *
 * @hide
@@ -77,6 +81,12 @@ public final class VibrationEffectXmlSerializer {
        if (firstSegment instanceof PrimitiveSegment) {
            return serializePrimitiveEffect(composed);
        }
        if (Flags.normalizedPwleEffects() && firstSegment instanceof PwleSegment) {
            return serializeWaveformEnvelopeEffect(composed);
        }
        if (Flags.normalizedPwleEffects() && firstSegment instanceof BasicPwleSegment) {
            return serializeBasicEnvelopeEffect(composed);
        }
        return serializeWaveformEffect(composed);
    }

@@ -110,6 +120,53 @@ public final class VibrationEffectXmlSerializer {
        return new SerializedComposedEffect(primitives);
    }

    private static SerializedComposedEffect serializeWaveformEnvelopeEffect(
            VibrationEffect.Composed effect) throws XmlSerializerException {
        SerializedWaveformEnvelopeEffect.Builder builder =
                new SerializedWaveformEnvelopeEffect.Builder();
        List<VibrationEffectSegment> segments = effect.getSegments();
        XmlValidator.checkSerializerCondition(effect.getRepeatIndex() == -1,
                "Unsupported repeating waveform envelope effect %s", effect);
        for (int i = 0; i < segments.size(); i++) {
            XmlValidator.checkSerializerCondition(segments.get(i) instanceof PwleSegment,
                    "Unsupported segment for waveform envelope effect %s", segments.get(i));
            PwleSegment segment = (PwleSegment) segments.get(i);

            if (i == 0 && segment.getStartFrequencyHz() != segment.getEndFrequencyHz()) {
                // Initial frequency explicitly defined.
                builder.setInitialFrequencyHz(segment.getStartFrequencyHz());
            }

            builder.addControlPoint(segment.getEndAmplitude(), segment.getEndFrequencyHz(),
                    segment.getDuration());
        }

        return new SerializedComposedEffect(builder.build());
    }

    private static SerializedComposedEffect serializeBasicEnvelopeEffect(
            VibrationEffect.Composed effect) throws XmlSerializerException {
        SerializedBasicEnvelopeEffect.Builder builder = new SerializedBasicEnvelopeEffect.Builder();
        List<VibrationEffectSegment> segments = effect.getSegments();
        XmlValidator.checkSerializerCondition(effect.getRepeatIndex() == -1,
                "Unsupported repeating basic envelope effect %s", effect);
        for (int i = 0; i < segments.size(); i++) {
            XmlValidator.checkSerializerCondition(segments.get(i) instanceof BasicPwleSegment,
                    "Unsupported segment for basic envelope effect %s", segments.get(i));
            BasicPwleSegment segment = (BasicPwleSegment) segments.get(i);

            if (i == 0 && segment.getStartSharpness() != segment.getEndSharpness()) {
                // Initial sharpness explicitly defined.
                builder.setInitialSharpness(segment.getStartSharpness());
            }

            builder.addControlPoint(segment.getEndIntensity(), segment.getEndSharpness(),
                    segment.getDuration());
        }

        return new SerializedComposedEffect(builder.build());
    }

    private static SerializedComposedEffect serializeWaveformEffect(
            VibrationEffect.Composed effect) throws XmlSerializerException {
        SerializedAmplitudeStepWaveform.Builder serializedWaveformBuilder =
+8 −0
Original line number Diff line number Diff line
@@ -42,14 +42,22 @@ public final class XmlConstants {
    public static final String TAG_PREDEFINED_EFFECT = "predefined-effect";
    public static final String TAG_PRIMITIVE_EFFECT = "primitive-effect";
    public static final String TAG_VENDOR_EFFECT = "vendor-effect";
    public static final String TAG_WAVEFORM_ENVELOPE_EFFECT = "waveform-envelope-effect";
    public static final String TAG_BASIC_ENVELOPE_EFFECT = "basic-envelope-effect";
    public static final String TAG_WAVEFORM_EFFECT = "waveform-effect";
    public static final String TAG_WAVEFORM_ENTRY = "waveform-entry";
    public static final String TAG_REPEATING = "repeating";
    public static final String TAG_CONTROL_POINT = "control-point";

    public static final String ATTRIBUTE_NAME = "name";
    public static final String ATTRIBUTE_FALLBACK = "fallback";
    public static final String ATTRIBUTE_DURATION_MS = "durationMs";
    public static final String ATTRIBUTE_AMPLITUDE = "amplitude";
    public static final String ATTRIBUTE_FREQUENCY_HZ = "frequencyHz";
    public static final String ATTRIBUTE_INITIAL_FREQUENCY_HZ = "initialFrequencyHz";
    public static final String ATTRIBUTE_INTENSITY = "intensity";
    public static final String ATTRIBUTE_SHARPNESS = "sharpness";
    public static final String ATTRIBUTE_INITIAL_SHARPNESS = "initialSharpness";
    public static final String ATTRIBUTE_SCALE = "scale";
    public static final String ATTRIBUTE_DELAY_MS = "delayMs";
    public static final String ATTRIBUTE_DELAY_TYPE = "delayType";
Loading