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

Commit 9388adb5 authored by Yeabkal Wubshit's avatar Yeabkal Wubshit
Browse files

Vibration parser for open TypedXmlPullParser

This allows clients to pass in an already-open parser to read a
vibration effect. This will be helpful in cases where a vibration XML
may be part of a larger XML.

Bug: 291128479
Bug: 185779088
Test: atest VibrationEffectXmlSerializationTest

Change-Id: I630589684a0b8361dfc9b7b5c1025d191b9b588c
parent 3e4808c8
Loading
Loading
Loading
Loading
+46 −10
Original line number Diff line number Diff line
@@ -28,7 +28,6 @@ import com.android.internal.vibrator.persistence.VibrationEffectXmlParser;
import com.android.internal.vibrator.persistence.XmlConstants;
import com.android.internal.vibrator.persistence.XmlParserException;
import com.android.internal.vibrator.persistence.XmlReader;
import com.android.internal.vibrator.persistence.XmlSerializedVibration;
import com.android.modules.utils.TypedXmlPullParser;

import org.xmlpull.v1.XmlPullParser;
@@ -178,25 +177,62 @@ 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> serializedVibration =
                    VibrationEffectXmlParser.parseTag(parser, parserFlags);
            VibrationEffect effect = parseTag(parser, flags);

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

            return serializedVibration.deserialize();
        } catch (XmlParserException e) {
            return effect;
        } catch (XmlParserException | VibrationXmlParserException e) {
            Slog.w(TAG, "Error parsing vibration XML", e);
            return null;
        }
    }

    /**
     * Parses XML content from given open {@link TypedXmlPullParser} into a {@link VibrationEffect}.
     *
     * <p>The provided parser should be pointing to a start of a valid vibration XML (i.e. to a
     * start <vibration> tag). No other parser position, including start of document, is considered
     * valid.
     *
     * <p>This method parses as long as it reads a valid vibration XML, and until an end vibration
     * tag. After a successful parsing, the parser will point to the end vibration tag (i.e. to a
     * </vibration> tag).
     *
     * @throws IOException error parsing from given {@link TypedXmlPullParser}.
     * @throws VibrationXmlParserException if the XML tag cannot be parsed into a
     *      {@link VibrationEffect}. The given {@code parser} might be pointing to a child XML tag
     *      that caused the parser failure.
     *
     * @hide
     */
    @NonNull
    public static VibrationEffect parseTag(@NonNull TypedXmlPullParser parser, @Flags int flags)
            throws IOException, VibrationXmlParserException {
        int parserFlags = 0;
        if ((flags & VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS) != 0) {
            parserFlags |= XmlConstants.FLAG_ALLOW_HIDDEN_APIS;
        }
        try {
            return VibrationEffectXmlParser.parseTag(parser, parserFlags).deserialize();
        } catch (XmlParserException e) {
            throw new VibrationXmlParserException("Error parsing vibration effect.", e);
        }
    }

    /**
     * Represents an error while parsing a vibration XML input.
     *
     * @hide
     */
    public static final class VibrationXmlParserException extends Exception {
        private VibrationXmlParserException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    private VibrationXmlParser() {
    }
}
+105 −0
Original line number Diff line number Diff line
@@ -29,10 +29,14 @@ import static org.junit.Assert.assertThrows;
import android.os.VibrationEffect;
import android.os.vibrator.PrebakedSegment;
import android.platform.test.annotations.Presubmit;
import android.util.Xml;

import com.android.modules.utils.TypedXmlPullParser;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.xmlpull.v1.XmlPullParser;

import java.io.IOException;
import java.io.StringReader;
@@ -64,6 +68,61 @@ public class VibrationEffectXmlSerializationTest {
        assertThat(isSupportedMimeType("application/vnd.android.vibration+xml")).isFalse();
    }

    @Test
    public void testParseTag_succeedAndParserPointsToEndVibrationTag() throws Exception {
        VibrationEffect effect = VibrationEffect.startComposition()
                .addPrimitive(PRIMITIVE_CLICK)
                .addPrimitive(PRIMITIVE_TICK, 0.2497f)
                .compose();
        String xml = "<vibration>"
                + "<primitive-effect name=\"click\"/>"
                + "<primitive-effect name=\"tick\" scale=\"0.2497\"/>"
                + "</vibration>";
        VibrationEffect effect2 = VibrationEffect.startComposition()
                .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
                .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
                .compose();
        String xml2 = "<vibration>"
                + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
                + "<primitive-effect name=\"spin\" scale=\"0.6364\" delayMs=\"7\"/>"
                + "</vibration>";

        TypedXmlPullParser parser = createXmlPullParser(xml);
        assertParseTagSucceeds(parser, effect);
        parser.next();
        assertEndOfDocument(parser);

        // Test no-issues when an end-tag follows the vibration XML.
        // To test this, starting with the corresponding "start-tag" is necessary.
        parser = createXmlPullParser("<next-tag>" + xml + "</next-tag>");
        // Move the parser once to point to the "<vibration> tag.
        parser.next();
        assertParseTagSucceeds(parser, effect);
        parser.next();
        assertEndTag(parser, "next-tag");

        parser = createXmlPullParser(xml + "<next-tag>");
        assertParseTagSucceeds(parser, effect);
        parser.next();
        assertStartTag(parser, "next-tag");

        parser = createXmlPullParser(xml + xml2);
        assertParseTagSucceeds(parser, effect);
        parser.next();
        assertParseTagSucceeds(parser, effect2);
        parser.next();
        assertEndOfDocument(parser);
    }

    @Test
    public void testParseTag_badXml_throwsException() throws Exception {
        assertParseTagFails(
                "<vibration>random text<primitive-effect name=\"click\"/></vibration>");
        assertParseTagFails("<bad-tag><primitive-effect name=\"click\"/></vibration>");
        assertParseTagFails("<primitive-effect name=\"click\"/></vibration>");
        assertParseTagFails("<vibration><primitive-effect name=\"click\"/>");
    }

    @Test
    public void testPrimitives_allSucceed() throws IOException {
        VibrationEffect effect = VibrationEffect.startComposition()
@@ -172,6 +231,41 @@ public class VibrationEffectXmlSerializationTest {
        assertThat(parse(xml, /* flags= */ 0)).isEqualTo(effect);
    }

    private TypedXmlPullParser createXmlPullParser(String xml) throws Exception {
        TypedXmlPullParser parser = Xml.newFastPullParser();
        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
        parser.setInput(new StringReader(xml));
        parser.next(); // read START_DOCUMENT
        return parser;
    }

    /**
     * Asserts parsing vibration from an open TypedXmlPullParser succeeds, and that the parser
     * points to the end "vibration" tag.
     */
    private void assertParseTagSucceeds(
            TypedXmlPullParser parser, VibrationEffect effect) throws Exception {
        assertThat(parseTag(parser)).isEqualTo(effect);

        assertThat(parser.getEventType()).isEqualTo(XmlPullParser.END_TAG);
        assertThat(parser.getName()).isEqualTo("vibration");
    }

    private void assertEndTag(TypedXmlPullParser parser, String expectedTagName) throws Exception {
        assertThat(parser.getName()).isEqualTo(expectedTagName);
        assertThat(parser.getEventType()).isEqualTo(parser.END_TAG);
    }

    private void assertStartTag(TypedXmlPullParser parser, String expectedTagName)
            throws Exception {
        assertThat(parser.getName()).isEqualTo(expectedTagName);
        assertThat(parser.getEventType()).isEqualTo(parser.START_TAG);
    }

    private void assertEndOfDocument(TypedXmlPullParser parser) throws Exception {
        assertThat(parser.getEventType()).isEqualTo(parser.END_DOCUMENT);
    }

    private void assertHiddenApisParserSucceeds(String xml, VibrationEffect effect)
            throws IOException {
        assertThat(parse(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS)).isEqualTo(effect);
@@ -183,6 +277,12 @@ public class VibrationEffectXmlSerializationTest {
                () -> serialize(effect, /* flags= */ 0));
    }

    private void assertParseTagFails(String xml) {
        assertThrows("Expected parsing to fail for " + xml,
                VibrationXmlParser.VibrationXmlParserException.class,
                () -> parseTag(createXmlPullParser(xml)));
    }

    private void assertPublicApisSerializerSucceeds(VibrationEffect effect,
            String... expectedSegments) throws IOException {
        assertSerializationContainsSegments(serialize(effect, /* flags= */ 0), expectedSegments);
@@ -214,6 +314,11 @@ public class VibrationEffectXmlSerializationTest {
        return VibrationXmlParser.parse(new StringReader(xml), flags);
    }

    private static VibrationEffect parseTag(TypedXmlPullParser parser)
            throws IOException, VibrationXmlParser.VibrationXmlParserException {
        return VibrationXmlParser.parseTag(parser, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS);
    }

    private static String serialize(VibrationEffect effect, @VibrationXmlSerializer.Flags int flags)
            throws IOException {
        StringWriter writer = new StringWriter();