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

Commit e8075ef9 authored by Lais Andrade's avatar Lais Andrade
Browse files

Add flags to VibrationEffect XML parser/serializer

Add flags to allow platform code to parse/serialize VibrationEffect
instances created with non-public effects.

No support for WaveformBuilder is introduced by this change.

Bug: 245129509
Test: VibrationEffectXmlSerializationTest
Change-Id: I6e7248467b501641a18e310134f5f807fb22a354
parent fad59212
Loading
Loading
Loading
Loading
+48 −9
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package android.os.vibrator.persistence;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
@@ -35,6 +36,8 @@ import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.io.Reader;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Parses XML into a {@link VibrationEffect}.
@@ -43,10 +46,10 @@ import java.io.Reader;
 *
 * * Predefined vibration effects
 *
 * <pre>VibrationEffect
 * <pre>
 *   {@code
 *     <vibration>
 *       <predefined-effect id="0" />
 *       <predefined-effect name="click" />
 *     </vibration>
 *   }
 * </pre>
@@ -75,10 +78,10 @@ import java.io.Reader;
 * <pre>
 *   {@code
 *     <vibration>
 *       <primitive-effect id="1" />
 *       <primitive-effect id="2" scale="0.8" />
 *       <primitive-effect id="3" delayMs="50" />
 *       <primitive-effect id="2" scale="0.5" delayMs="100" />
 *       <primitive-effect name="click" />
 *       <primitive-effect name="slow_rise" scale="0.8" />
 *       <primitive-effect name="quick_fall" delayMs="50" />
 *       <primitive-effect name="tick" scale="0.5" delayMs="100" />
 *     </vibration>
 *   }
 * </pre>
@@ -89,6 +92,24 @@ import java.io.Reader;
public final class VibrationXmlParser {
    private static final String TAG = "VibrationXmlParser";

    /**
     * Allows {@link VibrationEffect} instances created via non-public APIs to be parsed/serialized.
     *
     * <p>Note that the XML schema for non-public APIs is not backwards compatible. This is intended
     * for loading custom {@link VibrationEffect} configured per device and platform version, not
     * to be restored from old platform versions.
     *
     * @hide
     */
    public static final int FLAG_ALLOW_HIDDEN_APIS = 1 << 0; // Same as VibrationXmlSerializer

    /** @hide */
    @IntDef(prefix = { "FLAG_" }, flag = true, value = {
            FLAG_ALLOW_HIDDEN_APIS
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Flags {}

    /**
     * Parses XML content from given input stream into a {@link VibrationEffect}.
     *
@@ -103,6 +124,19 @@ public final class VibrationXmlParser {
    @TestApi
    @Nullable
    public static VibrationEffect parse(@NonNull Reader reader) throws IOException {
        return parse(reader, /* flags= */ 0);
    }

    /**
     * Parses XML content from given input stream into a {@link VibrationEffect}.
     *
     * <p>Same as {@link #parse(Reader)}, with extra flags to control the parsing behavior.
     *
     * @hide
     */
    @Nullable
    public static VibrationEffect parse(@NonNull Reader reader, @Flags int flags)
            throws IOException {
        TypedXmlPullParser parser = Xml.newFastPullParser();

        try {
@@ -116,14 +150,19 @@ public final class VibrationXmlParser {
            // Ensure XML starts with expected root tag.
            XmlReader.readDocumentStartTag(parser, XmlConstants.TAG_VIBRATION);

            int parserFlags = 0;
            if ((flags & FLAG_ALLOW_HIDDEN_APIS) != 0) {
                parserFlags |= XmlConstants.FLAG_ALLOW_HIDDEN_APIS;
            }

            // Parse root tag as a vibration effect.
            XmlSerializedVibration<VibrationEffect> serializable =
                    VibrationEffectXmlParser.parseTag(parser);
            XmlSerializedVibration<VibrationEffect> serializedVibration =
                    VibrationEffectXmlParser.parseTag(parser, parserFlags);

            // Ensure XML ends after root tag is consumed.
            XmlReader.readDocumentEndTag(parser);

            return serializable.deserialize();
            return serializedVibration.deserialize();
        } catch (XmlParserException e) {
            Slog.w(TAG, "Error parsing vibration XML", e);
            return null;
+69 −12
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package android.os.vibrator.persistence;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.TestApi;
import android.os.CombinedVibration;
@@ -23,6 +24,7 @@ import android.os.VibrationEffect;
import android.util.Xml;

import com.android.internal.vibrator.persistence.VibrationEffectXmlSerializer;
import com.android.internal.vibrator.persistence.XmlConstants;
import com.android.internal.vibrator.persistence.XmlSerializedVibration;
import com.android.internal.vibrator.persistence.XmlSerializerException;
import com.android.internal.vibrator.persistence.XmlValidator;
@@ -30,6 +32,8 @@ import com.android.modules.utils.TypedXmlSerializer;

import java.io.IOException;
import java.io.Writer;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Serializes {@link CombinedVibration} and {@link VibrationEffect} instances to XML.
@@ -40,10 +44,37 @@ import java.io.Writer;
 */
@TestApi
public final class VibrationXmlSerializer {
    private static final String TAG = "VibrationXmlSerializer";

    private static final String SERIALIZER_ENCODING = Xml.Encoding.UTF_8.name();
    private static final String SERIALIZER_FEATURE_INDENT_OUTPUT =
    /**
     * Allows {@link VibrationEffect} instances created via non-public APIs to be parsed/serialized.
     *
     * <p>Note that the XML schema for non-public APIs is not backwards compatible. This is intended
     * for loading custom {@link VibrationEffect} configured per device and platform version, not
     * to be restored from old platform versions or from different devices.
     *
     * @hide
     */
    public static final int FLAG_ALLOW_HIDDEN_APIS = 1 << 0;

    /**
     * Writes a more human-readable output XML.
     *
     * <p>This will be less compact as it includes extra whitespace for things like indentation.
     *
     * @hide
     */
    public static final int FLAG_PRETTY_PRINT = 1 << 1;

    /** @hide */
    @IntDef(prefix = { "FLAG_" }, flag = true, value = {
            FLAG_PRETTY_PRINT,
            FLAG_ALLOW_HIDDEN_APIS
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Flags {}

    private static final String XML_ENCODING = Xml.Encoding.UTF_8.name();
    private static final String XML_FEATURE_INDENT_OUTPUT =
            "http://xmlpull.org/v1/doc/features.html#indent-output";

    /**
@@ -62,21 +93,47 @@ public final class VibrationXmlSerializer {
    @TestApi
    public static void serialize(@NonNull VibrationEffect effect, @NonNull Writer writer)
            throws SerializationFailedException, IOException {
        XmlSerializedVibration<VibrationEffect> serializableEffect;
        serialize(effect, writer, /* flags= */ 0);
    }

    /**
     * Serializes a {@link VibrationEffect} to XML and writes output to given {@link Writer}.
     *
     * <p>Same as {@link #serialize(VibrationEffect, Writer)}, with extra flags to control the
     * serialization behavior.
     *
     * @hide
     */
    public static void serialize(@NonNull VibrationEffect effect, @NonNull Writer writer,
            @Flags int flags) throws SerializationFailedException, IOException {
        // Serialize effect first to fail early.
        XmlSerializedVibration<VibrationEffect> serializedVibration =
                toSerializedVibration(effect, flags);
        TypedXmlSerializer xmlSerializer = Xml.newFastSerializer();
        xmlSerializer.setFeature(XML_FEATURE_INDENT_OUTPUT, (flags & FLAG_PRETTY_PRINT) != 0);
        xmlSerializer.setOutput(writer);
        xmlSerializer.startDocument(XML_ENCODING, /* standalone= */ false);
        serializedVibration.write(xmlSerializer);
        xmlSerializer.endDocument();
    }

    private static XmlSerializedVibration<VibrationEffect> toSerializedVibration(
            VibrationEffect effect, @Flags int flags) throws SerializationFailedException {
        XmlSerializedVibration<VibrationEffect> serializedVibration;
        int serializerFlags = 0;
        if ((flags & FLAG_ALLOW_HIDDEN_APIS) != 0) {
            serializerFlags |= XmlConstants.FLAG_ALLOW_HIDDEN_APIS;
        }

        try {
            serializableEffect = VibrationEffectXmlSerializer.serialize(effect);
            XmlValidator.checkSerializedVibration(serializableEffect, effect);
            serializedVibration = VibrationEffectXmlSerializer.serialize(effect, serializerFlags);
            XmlValidator.checkSerializedVibration(serializedVibration, effect);
        } catch (XmlSerializerException e) {
            // Serialization failed or created incomplete representation, fail before writing.
            throw new SerializationFailedException(effect, e);
        }

        TypedXmlSerializer xmlSerializer = Xml.newFastSerializer();
        xmlSerializer.setFeature(SERIALIZER_FEATURE_INDENT_OUTPUT, false);
        xmlSerializer.setOutput(writer);
        xmlSerializer.startDocument(SERIALIZER_ENCODING, /* standalone= */ false);
        serializableEffect.write(xmlSerializer);
        xmlSerializer.endDocument();
        return serializedVibration;
    }

    /**
+3 −3
Original line number Diff line number Diff line
@@ -78,9 +78,9 @@ final class SerializedCompositionPrimitive implements SerializedSegment {
    @Override
    public String toString() {
        return "SerializedCompositionPrimitive{"
                + "primitiveName=" + mPrimitiveName
                + ", primitiveScale=" + mPrimitiveScale
                + ", primitiveDelayMs=" + mPrimitiveDelayMs
                + "name=" + mPrimitiveName
                + ", scale=" + mPrimitiveScale
                + ", delayMs=" + mPrimitiveDelayMs
                + '}';
    }

+36 −23
Original line number Diff line number Diff line
@@ -16,13 +16,14 @@

package com.android.internal.vibrator.persistence;

import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_FALLBACK;
import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_NAME;
import static com.android.internal.vibrator.persistence.XmlConstants.NAMESPACE;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PREDEFINED_EFFECT;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.VibrationEffect;
import android.os.vibrator.PrebakedSegment;

import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment;
import com.android.internal.vibrator.persistence.XmlConstants.PredefinedEffectName;
@@ -33,34 +34,41 @@ import java.io.IOException;

/**
 * Serialized representation of a predefined effect created via
 * {@link VibrationEffect#createPredefined(int)}.
 * {@link VibrationEffect#get(int, boolean)}.
 *
 * @hide
 */
final class SerializedPredefinedEffect implements SerializedSegment {

    @NonNull
    private final PredefinedEffectName mEffectName;
    private final boolean mShouldFallback;

    SerializedPredefinedEffect(PredefinedEffectName effectName) {
    SerializedPredefinedEffect(PredefinedEffectName effectName, boolean shouldFallback) {
        mEffectName = effectName;
        mShouldFallback = shouldFallback;
    }

    @Override
    public void deserializeIntoComposition(@NonNull VibrationEffect.Composition composition) {
        composition.addEffect(VibrationEffect.createPredefined(mEffectName.getEffectId()));
        composition.addEffect(VibrationEffect.get(mEffectName.getEffectId(), mShouldFallback));
    }

    @Override
    public void write(@NonNull TypedXmlSerializer serializer) throws IOException {
        serializer.startTag(NAMESPACE, TAG_PREDEFINED_EFFECT);
        serializer.attribute(NAMESPACE, ATTRIBUTE_NAME, mEffectName.toString());
        if (mShouldFallback != PrebakedSegment.DEFAULT_SHOULD_FALLBACK) {
            serializer.attributeBoolean(NAMESPACE, ATTRIBUTE_FALLBACK, mShouldFallback);
        }
        serializer.endTag(NAMESPACE, TAG_PREDEFINED_EFFECT);
    }

    @Override
    public String toString() {
        return "SerializedPredefinedEffect{"
                + "effectName=" + mEffectName
                + "name=" + mEffectName
                + ", fallback=" + mShouldFallback
                + '}';
    }

@@ -68,31 +76,36 @@ final class SerializedPredefinedEffect implements SerializedSegment {
    static final class Parser {

        @NonNull
        static SerializedPredefinedEffect parseNext(@NonNull TypedXmlPullParser parser)
                throws XmlParserException, IOException {
        static SerializedPredefinedEffect parseNext(@NonNull TypedXmlPullParser parser,
                @XmlConstants.Flags int flags) throws XmlParserException, IOException {
            XmlValidator.checkStartTag(parser, TAG_PREDEFINED_EFFECT);
            XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_NAME);

            PredefinedEffectName effectName = parseEffectName(
                    parser.getAttributeValue(NAMESPACE, ATTRIBUTE_NAME));

            // Consume tag
            XmlReader.readEndTag(parser);

            return new SerializedPredefinedEffect(effectName);
            boolean allowHidden = (flags & XmlConstants.FLAG_ALLOW_HIDDEN_APIS) != 0;
            if (allowHidden) {
                XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_NAME,
                        ATTRIBUTE_FALLBACK);
            } else {
                XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_NAME);
            }

        @NonNull
        private static PredefinedEffectName parseEffectName(@Nullable String name)
                throws XmlParserException {
            if (name == null) {
            String nameAttr = parser.getAttributeValue(NAMESPACE, ATTRIBUTE_NAME);
            if (nameAttr == null) {
                throw new XmlParserException("Missing predefined effect name");
            }
            PredefinedEffectName effectName = PredefinedEffectName.findByName(name);
            PredefinedEffectName effectName = PredefinedEffectName.findByName(nameAttr, flags);
            if (effectName == null) {
                throw new XmlParserException("Unexpected predefined effect name " + name);
                throw new XmlParserException("Unexpected predefined effect name " + nameAttr);
            }
            return effectName;

            boolean defaultFallback = PrebakedSegment.DEFAULT_SHOULD_FALLBACK;
            boolean fallback = allowHidden
                    ? parser.getAttributeBoolean(NAMESPACE, ATTRIBUTE_FALLBACK, defaultFallback)
                    : defaultFallback;

            // Consume tag
            XmlReader.readEndTag(parser);

            return new SerializedPredefinedEffect(effectName, fallback);
        }
    }
}
+18 −21
Original line number Diff line number Diff line
@@ -34,16 +34,18 @@ import java.util.List;
/**
 * Parser implementation for {@link VibrationEffect}.
 *
 * <p>This parser supports the schema defined by services/core/xsd/vibrator/vibration/vibration.xsd.
 *
 * <p>This parser does not support effects created with {@link VibrationEffect.WaveformBuilder} nor
 * {@link VibrationEffect.Composition#addEffect(VibrationEffect)}. It only supports vibration
 * effects defined as:
 *
 * * Predefined vibration effects
 *
 * <pre>VibrationEffect
 * <pre>
 *   {@code
 *     <vibration>
 *       <predefined-effect id="0" />
 *       <predefined-effect name="click" />
 *     </vibration>
 *   }
 * </pre>
@@ -72,8 +74,8 @@ import java.util.List;
 * <pre>
 *   {@code
 *     <vibration>
 *       <primitive-effect id="1" />
 *       <primitive-effect id="2" scale="0.5" delayMs="100" />
 *       <primitive-effect name="click" />
 *       <primitive-effect name="tick" scale="0.5" delayMs="100" />
 *     </vibration>
 *   }
 * </pre>
@@ -90,10 +92,12 @@ public class VibrationEffectXmlParser {
     */
    @NonNull
    public static XmlSerializedVibration<VibrationEffect> parseTag(
            @NonNull TypedXmlPullParser parser) throws XmlParserException, IOException {
            @NonNull TypedXmlPullParser parser, @XmlConstants.Flags int flags)
            throws XmlParserException, IOException {
        XmlValidator.checkStartTag(parser, TAG_VIBRATION);
        XmlValidator.checkTagHasNoUnexpectedAttributes(parser);
        return parseVibrationContent(parser);

        return parseVibrationContent(parser, flags);
    }

    /**
@@ -103,8 +107,8 @@ public class VibrationEffectXmlParser {
     * <p>This can be reused for reading a vibration from an XML root tag or from within a combined
     * vibration, but it should always be called from places that validates the top level tag.
     */
    static SerializedVibrationEffect parseVibrationContent(TypedXmlPullParser parser)
            throws XmlParserException, IOException {
    static SerializedVibrationEffect parseVibrationContent(TypedXmlPullParser parser,
            @XmlConstants.Flags int flags) throws XmlParserException, IOException {
        String vibrationTagName = parser.getName();
        int vibrationTagDepth = parser.getDepth();

@@ -117,11 +121,15 @@ public class VibrationEffectXmlParser {
        switch (parser.getName()) {
            case TAG_PREDEFINED_EFFECT:
                serializedVibration = new SerializedVibrationEffect(
                        SerializedPredefinedEffect.Parser.parseNext(parser));
                        SerializedPredefinedEffect.Parser.parseNext(parser, flags));
                break;
            case TAG_PRIMITIVE_EFFECT:
                List<SerializedSegment> primitives = new ArrayList<>();
                do { // First primitive tag already open
                    primitives.add(SerializedCompositionPrimitive.Parser.parseNext(parser));
                } while (XmlReader.readNextTagWithin(parser, vibrationTagDepth));
                serializedVibration = new SerializedVibrationEffect(
                        parsePrimitiveList(parser, vibrationTagDepth));
                        primitives.toArray(new SerializedSegment[primitives.size()]));
                break;
            case TAG_WAVEFORM_EFFECT:
                serializedVibration = new SerializedVibrationEffect(
@@ -137,15 +145,4 @@ public class VibrationEffectXmlParser {

        return serializedVibration;
    }

    private static SerializedSegment[] parsePrimitiveList(
            TypedXmlPullParser parser, int outerDepth) throws XmlParserException, IOException {
        List<SerializedSegment> segments = new ArrayList<>();

        do { // First primitive tag already open
            segments.add(SerializedCompositionPrimitive.Parser.parseNext(parser));
        } while (XmlReader.readNextTagWithin(parser, outerDepth));

        return segments.toArray(new SerializedSegment[segments.size()]);
    }
}
Loading