From e8075ef937c933672f3523b2bc72b4653e90b9f5 Mon Sep 17 00:00:00 2001 From: Lais Andrade Date: Fri, 16 Jun 2023 19:18:15 +0100 Subject: [PATCH] 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 --- .../persistence/VibrationXmlParser.java | 57 ++++- .../persistence/VibrationXmlSerializer.java | 81 +++++- .../SerializedCompositionPrimitive.java | 6 +- .../SerializedPredefinedEffect.java | 61 +++-- .../persistence/VibrationEffectXmlParser.java | 39 ++- .../VibrationEffectXmlSerializer.java | 82 ++++-- .../vibrator/persistence/XmlConstants.java | 68 ++++- .../vibrator/persistence/XmlReader.java | 53 ++-- .../VibrationEffectXmlSerializationTest.java | 234 ++++++++++++++++++ .../vibration/vibration-plus-hidden-apis.xsd | 157 ++++++++++++ core/xsd/vibrator/vibration/vibration.xsd | 4 +- 11 files changed, 708 insertions(+), 134 deletions(-) create mode 100644 core/tests/coretests/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java create mode 100644 core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlParser.java b/core/java/android/os/vibrator/persistence/VibrationXmlParser.java index 88e039c9e6201..22676276f748c 100644 --- a/core/java/android/os/vibrator/persistence/VibrationXmlParser.java +++ b/core/java/android/os/vibrator/persistence/VibrationXmlParser.java @@ -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 * - *
VibrationEffect
+ * 
  *   {@code
  *     
- *       
+ *       
  *     
  *   }
  * 
@@ -75,10 +78,10 @@ import java.io.Reader; *
  *   {@code
  *     
- *       
- *       
- *       
- *       
+ *       
+ *       
+ *       
+ *       
  *     
  *   }
  * 
@@ -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. + * + *

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}. + * + *

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 serializable = - VibrationEffectXmlParser.parseTag(parser); + XmlSerializedVibration 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; diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java index 32eeaad189e5e..554d70339b90e 100644 --- a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java +++ b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java @@ -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. + * + *

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. + * + *

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 serializableEffect; + serialize(effect, writer, /* flags= */ 0); + } + + /** + * Serializes a {@link VibrationEffect} to XML and writes output to given {@link Writer}. + * + *

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 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 toSerializedVibration( + VibrationEffect effect, @Flags int flags) throws SerializationFailedException { + XmlSerializedVibration 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; } /** diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java b/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java index 79f9e5a22767b..db5c7ff830b50 100644 --- a/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java +++ b/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java @@ -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 + '}'; } diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java index 210125f6e1914..8924311f9c339 100644 --- a/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java +++ b/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java @@ -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)); + boolean allowHidden = (flags & XmlConstants.FLAG_ALLOW_HIDDEN_APIS) != 0; + if (allowHidden) { + XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_NAME, + ATTRIBUTE_FALLBACK); + } else { + XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_NAME); + } + + String nameAttr = parser.getAttributeValue(NAMESPACE, ATTRIBUTE_NAME); + if (nameAttr == null) { + throw new XmlParserException("Missing predefined effect name"); + } + PredefinedEffectName effectName = PredefinedEffectName.findByName(nameAttr, flags); + if (effectName == null) { + throw new XmlParserException("Unexpected predefined effect name " + nameAttr); + } + + 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); - } - - @NonNull - private static PredefinedEffectName parseEffectName(@Nullable String name) - throws XmlParserException { - if (name == null) { - throw new XmlParserException("Missing predefined effect name"); - } - PredefinedEffectName effectName = PredefinedEffectName.findByName(name); - if (effectName == null) { - throw new XmlParserException("Unexpected predefined effect name " + name); - } - return effectName; + return new SerializedPredefinedEffect(effectName, fallback); } } } diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java index 6475377cb669c..3561fe458085d 100644 --- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java +++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java @@ -34,16 +34,18 @@ import java.util.List; /** * Parser implementation for {@link VibrationEffect}. * + *

This parser supports the schema defined by services/core/xsd/vibrator/vibration/vibration.xsd. + * *

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 * - *

VibrationEffect
+ * 
  *   {@code
  *     
- *       
+ *       
  *     
  *   }
  * 
@@ -72,8 +74,8 @@ import java.util.List; *
  *   {@code
  *     
- *       
- *       
+ *       
+ *       
  *     
  *   }
  * 
@@ -90,10 +92,12 @@ public class VibrationEffectXmlParser { */ @NonNull public static XmlSerializedVibration 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 { *

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 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 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()]); - } } diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java index c3cc761f1d397..f561c1485f1d6 100644 --- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java +++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java @@ -54,41 +54,34 @@ public final class VibrationEffectXmlSerializer { */ @NonNull public static XmlSerializedVibration serialize( - @NonNull VibrationEffect vibration) throws XmlSerializerException { + @NonNull VibrationEffect vibration, @XmlConstants.Flags int flags) + throws XmlSerializerException { XmlValidator.checkSerializerCondition(vibration instanceof VibrationEffect.Composed, "Unsupported VibrationEffect type %s", vibration); - VibrationEffect.Composed composedEffect = (VibrationEffect.Composed) vibration; - XmlValidator.checkSerializerCondition(!composedEffect.getSegments().isEmpty(), + VibrationEffect.Composed composed = (VibrationEffect.Composed) vibration; + XmlValidator.checkSerializerCondition(!composed.getSegments().isEmpty(), "Unsupported empty VibrationEffect %s", vibration); - VibrationEffectSegment firstSegment = composedEffect.getSegments().get(0); + VibrationEffectSegment firstSegment = composed.getSegments().get(0); if (firstSegment instanceof PrebakedSegment) { - return serializePredefinedEffect(composedEffect); + return serializePredefinedEffect(composed, flags); } if (firstSegment instanceof PrimitiveSegment) { - return serializePrimitiveEffect(composedEffect); + return serializePrimitiveEffect(composed); } - return serializeWaveformEffect(composedEffect); + return serializeWaveformEffect(composed); } private static SerializedVibrationEffect serializePredefinedEffect( - VibrationEffect.Composed effect) throws XmlSerializerException { + VibrationEffect.Composed effect, @XmlConstants.Flags int flags) + throws XmlSerializerException { List segments = effect.getSegments(); XmlValidator.checkSerializerCondition(effect.getRepeatIndex() == -1, "Unsupported repeating predefined effect %s", effect); XmlValidator.checkSerializerCondition(segments.size() == 1, "Unsupported multiple segments in predefined effect %s", effect); - XmlValidator.checkSerializerCondition(segments.get(0) instanceof PrebakedSegment, - "Unsupported segment for predefined effect %s", segments.get(0)); - - PrebakedSegment segment = (PrebakedSegment) segments.get(0); - PredefinedEffectName effectName = PredefinedEffectName.findById(segment.getEffectId()); - - XmlValidator.checkSerializerCondition(effectName != null, - "Unsupported predefined effect id %s", segment.getEffectId()); - - return new SerializedVibrationEffect(new SerializedPredefinedEffect(effectName)); + return new SerializedVibrationEffect(serializePrebakedSegment(segments.get(0), flags)); } private static SerializedVibrationEffect serializePrimitiveEffect( @@ -99,14 +92,7 @@ public final class VibrationEffectXmlSerializer { SerializedSegment[] primitives = new SerializedSegment[segments.size()]; for (int i = 0; i < segments.size(); i++) { - XmlValidator.checkSerializerCondition(segments.get(i) instanceof PrimitiveSegment, - "Unsupported segment for primitive composition %s", segments.get(i)); - - PrimitiveSegment segment = (PrimitiveSegment) segments.get(i); - PrimitiveEffectName primitiveName = - PrimitiveEffectName.findById(segment.getPrimitiveId()); - primitives[i] = new SerializedCompositionPrimitive( - primitiveName, segment.getScale(), segment.getDelay()); + primitives[i] = serializePrimitiveSegment(segments.get(i)); } return new SerializedVibrationEffect(primitives); @@ -126,6 +112,10 @@ public final class VibrationEffectXmlSerializer { if (effect.getRepeatIndex() == i) { serializedWaveformBuilder.setRepeatIndexToCurrentEntry(); } + + XmlValidator.checkSerializerCondition(Float.compare(segment.getFrequencyHz(), 0) == 0, + "Unsupported segment with non-default frequency %f", segment.getFrequencyHz()); + serializedWaveformBuilder.addDurationAndAmplitude( segment.getDuration(), toAmplitudeInt(segment.getAmplitude())); } @@ -133,6 +123,46 @@ public final class VibrationEffectXmlSerializer { return new SerializedVibrationEffect(serializedWaveformBuilder.build()); } + 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; + PredefinedEffectName effectName = 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; + PrimitiveEffectName primitiveName = + PrimitiveEffectName.findById(primitive.getPrimitiveId()); + + XmlValidator.checkSerializerCondition(primitiveName != null, + "Unsupported primitive effect id %s", primitive.getPrimitiveId()); + + return new SerializedCompositionPrimitive( + primitiveName, primitive.getScale(), primitive.getDelay()); + } + private static int toAmplitudeInt(float amplitude) { return Float.compare(amplitude, VibrationEffect.DEFAULT_AMPLITUDE) == 0 ? VibrationEffect.DEFAULT_AMPLITUDE diff --git a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java index a4ceb606571ef..d1c78f0b54eae 100644 --- a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java +++ b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java @@ -16,12 +16,14 @@ package com.android.internal.vibrator.persistence; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.VibrationEffect; import android.os.VibrationEffect.Composition.PrimitiveType; -import android.os.VibrationEffect.EffectType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Locale; /** @@ -42,6 +44,7 @@ public final class XmlConstants { public static final String TAG_REPEATING = "repeating"; 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_SCALE = "scale"; @@ -49,6 +52,20 @@ public final class XmlConstants { public static final String VALUE_AMPLITUDE_DEFAULT = "default"; + /** + * Allow {@link VibrationEffect} hidden APIs to be used during parsing/serializing. + * + *

Use the schema at services/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd. + */ + public static final int FLAG_ALLOW_HIDDEN_APIS = 1 << 0; + + /** @hide */ + @IntDef(prefix = { "FLAG_" }, flag = true, value = { + FLAG_ALLOW_HIDDEN_APIS + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Flags {} + /** Represent supported values for attribute name in {@link #TAG_PRIMITIVE_EFFECT} */ public enum PrimitiveEffectName { LOW_TICK(VibrationEffect.Composition.PRIMITIVE_LOW_TICK), @@ -106,15 +123,38 @@ public final class XmlConstants { /** Represent supported values for attribute name in {@link #TAG_PREDEFINED_EFFECT} */ public enum PredefinedEffectName { - TICK(VibrationEffect.EFFECT_TICK), - CLICK(VibrationEffect.EFFECT_CLICK), - HEAVY_CLICK(VibrationEffect.EFFECT_HEAVY_CLICK), - DOUBLE_CLICK(VibrationEffect.EFFECT_DOUBLE_CLICK); + // Public effects + TICK(VibrationEffect.EFFECT_TICK, true), + CLICK(VibrationEffect.EFFECT_CLICK, true), + HEAVY_CLICK(VibrationEffect.EFFECT_HEAVY_CLICK, true), + DOUBLE_CLICK(VibrationEffect.EFFECT_DOUBLE_CLICK, true), - @EffectType private final int mEffectId; + // Hidden effects + TEXTURE_TICK(VibrationEffect.EFFECT_TEXTURE_TICK, false), + THUD(VibrationEffect.EFFECT_THUD, false), + POP(VibrationEffect.EFFECT_POP, false), + RINGTONE_1(VibrationEffect.RINGTONES[0], false), + RINGTONE_2(VibrationEffect.RINGTONES[1], false), + RINGTONE_3(VibrationEffect.RINGTONES[2], false), + RINGTONE_4(VibrationEffect.RINGTONES[3], false), + RINGTONE_5(VibrationEffect.RINGTONES[4], false), + RINGTONE_6(VibrationEffect.RINGTONES[5], false), + RINGTONE_7(VibrationEffect.RINGTONES[6], false), + RINGTONE_8(VibrationEffect.RINGTONES[7], false), + RINGTONE_9(VibrationEffect.RINGTONES[8], false), + RINGTONE_10(VibrationEffect.RINGTONES[9], false), + RINGTONE_11(VibrationEffect.RINGTONES[10], false), + RINGTONE_12(VibrationEffect.RINGTONES[11], false), + RINGTONE_13(VibrationEffect.RINGTONES[12], false), + RINGTONE_14(VibrationEffect.RINGTONES[13], false), + RINGTONE_15(VibrationEffect.RINGTONES[14], false); - PredefinedEffectName(@EffectType int id) { + private final int mEffectId; + private final boolean mIsPublic; + + PredefinedEffectName(int id, boolean isPublic) { mEffectId = id; + mIsPublic = isPublic; } /** @@ -122,10 +162,11 @@ public final class XmlConstants { * none of the available names maps to the given id. */ @Nullable - public static PredefinedEffectName findById(int effectId) { + public static PredefinedEffectName findById(int effectId, @XmlConstants.Flags int flags) { + boolean allowHidden = (flags & XmlConstants.FLAG_ALLOW_HIDDEN_APIS) != 0; for (PredefinedEffectName name : PredefinedEffectName.values()) { if (name.mEffectId == effectId) { - return name; + return (name.mIsPublic || allowHidden) ? name : null; } } return null; @@ -136,15 +177,18 @@ public final class XmlConstants { * none of the available names maps to the given name. */ @Nullable - public static PredefinedEffectName findByName(@NonNull String effectName) { + public static PredefinedEffectName findByName(@NonNull String effectName, + @XmlConstants.Flags int flags) { + boolean allowHidden = (flags & XmlConstants.FLAG_ALLOW_HIDDEN_APIS) != 0; try { - return PredefinedEffectName.valueOf(effectName.toUpperCase(Locale.ROOT)); + PredefinedEffectName name = PredefinedEffectName.valueOf( + effectName.toUpperCase(Locale.ROOT)); + return (name.mIsPublic || allowHidden) ? name : null; } catch (IllegalArgumentException e) { return null; } } - @EffectType public int getEffectId() { return mEffectId; } diff --git a/core/java/com/android/internal/vibrator/persistence/XmlReader.java b/core/java/com/android/internal/vibrator/persistence/XmlReader.java index 2b0b0b4156ef9..75073387c1435 100644 --- a/core/java/com/android/internal/vibrator/persistence/XmlReader.java +++ b/core/java/com/android/internal/vibrator/persistence/XmlReader.java @@ -149,25 +149,19 @@ public final class XmlReader { * attribute is missing. */ public static int readAttributeIntNonNegative( - TypedXmlPullParser parser, String attributeName, int defaultValue) + TypedXmlPullParser parser, String attrName, int defaultValue) throws XmlParserException { - if (parser.getAttributeIndex(NAMESPACE, attributeName) < 0) { + if (parser.getAttributeIndex(NAMESPACE, attrName) < 0) { return defaultValue; } - return readAttributeIntNonNegative(parser, attributeName); + return readAttributeIntNonNegative(parser, attrName); } /** Read attribute from current tag as a non-negative integer. */ public static int readAttributeIntNonNegative(TypedXmlPullParser parser, String attrName) throws XmlParserException { String tagName = parser.getName(); - int value; - try { - value = parser.getAttributeInt(NAMESPACE, attrName); - } catch (XmlPullParserException e) { - String rawValue = parser.getAttributeValue(NAMESPACE, attrName); - throw XmlParserException.createFromPullParserException(tagName, attrName, rawValue, e); - } + int value = readAttributeInt(parser, attrName); XmlValidator.checkParserCondition(value >= 0, "Unexpected %s = %d in tag %s, expected %s >= 0", @@ -180,13 +174,7 @@ public final class XmlReader { TypedXmlPullParser parser, String attrName, int lowerInclusive, int upperInclusive) throws XmlParserException { String tagName = parser.getName(); - int value; - try { - value = parser.getAttributeInt(NAMESPACE, attrName); - } catch (XmlPullParserException e) { - String rawValue = parser.getAttributeValue(NAMESPACE, attrName); - throw XmlParserException.createFromPullParserException(tagName, attrName, rawValue, e); - } + int value = readAttributeInt(parser, attrName); XmlValidator.checkParserCondition( value >= lowerInclusive && value <= upperInclusive, @@ -206,18 +194,33 @@ public final class XmlReader { return defaultValue; } String tagName = parser.getName(); - float value; - try { - // The method getAttributeFloat with default catches any Exception and returns default. - value = parser.getAttributeFloat(NAMESPACE, attrName); - } catch (XmlPullParserException e) { - String rawValue = parser.getAttributeValue(NAMESPACE, attrName); - throw XmlParserException.createFromPullParserException(tagName, attrName, rawValue, e); - } + float value = readAttributeFloat(parser, attrName); XmlValidator.checkParserCondition(value >= lowerInclusive && value <= upperInclusive, "Unexpected %s = %f in tag %s, expected %s in [%f, %f]", attrName, value, tagName, attrName, lowerInclusive, upperInclusive); return value; } + + private static int readAttributeInt(TypedXmlPullParser parser, String attrName) + throws XmlParserException { + String tagName = parser.getName(); + try { + return parser.getAttributeInt(NAMESPACE, attrName); + } catch (XmlPullParserException e) { + String rawValue = parser.getAttributeValue(NAMESPACE, attrName); + throw XmlParserException.createFromPullParserException(tagName, attrName, rawValue, e); + } + } + + private static float readAttributeFloat(TypedXmlPullParser parser, String attrName) + throws XmlParserException { + String tagName = parser.getName(); + try { + return parser.getAttributeFloat(NAMESPACE, attrName); + } catch (XmlPullParserException e) { + String rawValue = parser.getAttributeValue(NAMESPACE, attrName); + throw XmlParserException.createFromPullParserException(tagName, attrName, rawValue, e); + } + } } diff --git a/core/tests/coretests/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java b/core/tests/coretests/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java new file mode 100644 index 0000000000000..fb3a099b0ac86 --- /dev/null +++ b/core/tests/coretests/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2023 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 android.os.vibrator.persistence; + +import static android.os.VibrationEffect.Composition.PRIMITIVE_CLICK; +import static android.os.VibrationEffect.Composition.PRIMITIVE_LOW_TICK; +import static android.os.VibrationEffect.Composition.PRIMITIVE_SPIN; +import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.os.VibrationEffect; +import android.os.vibrator.PrebakedSegment; +import android.platform.test.annotations.Presubmit; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; + +/** + * Unit tests for {@link VibrationXmlParser} and {@link VibrationXmlSerializer}. + * + *

The {@link VibrationEffect} public APIs are covered by CTS to enforce the schema defined at + * services/core/xsd/vibrator/vibration/vibration.xsd. + */ +@Presubmit +@RunWith(JUnit4.class) +public class VibrationEffectXmlSerializationTest { + + @Test + public void testPrimitives_allSucceed() throws IOException { + VibrationEffect effect = VibrationEffect.startComposition() + .addPrimitive(PRIMITIVE_CLICK) + .addPrimitive(PRIMITIVE_TICK, 0.2497f) + .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356) + .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7) + .compose(); + String xml = "" + + "" + + "" + + "" + + "" + + ""; + + 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 + public void testWaveforms_allSucceed() throws IOException { + VibrationEffect effect = VibrationEffect.createWaveform(new long[]{123, 456, 789, 0}, + new int[]{254, 1, 255, 0}, /* repeat= */ 0); + String xml = "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + + 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 + public void testPredefinedEffects_publicEffectsWithDefaultFallback_allSucceed() + throws IOException { + for (Map.Entry entry : createPublicPredefinedEffectsMap().entrySet()) { + VibrationEffect effect = VibrationEffect.get(entry.getValue()); + String xml = String.format( + "", entry.getKey()); + + assertPublicApisParserSucceeds(xml, effect); + assertPublicApisSerializerSucceeds(effect, entry.getKey()); + assertPublicApisRoundTrip(effect); + + assertHiddenApisParserSucceeds(xml, effect); + assertHiddenApisSerializerSucceeds(effect, entry.getKey()); + assertHiddenApisRoundTrip(effect); + } + } + + @Test + public void testPredefinedEffects_hiddenEffects_onlySucceedsWithFlag() throws IOException { + for (Map.Entry entry : createHiddenPredefinedEffectsMap().entrySet()) { + VibrationEffect effect = VibrationEffect.get(entry.getValue()); + String xml = String.format( + "", entry.getKey()); + + assertPublicApisParserFails(xml); + assertPublicApisSerializerFails(effect); + + assertHiddenApisParserSucceeds(xml, effect); + assertHiddenApisSerializerSucceeds(effect, entry.getKey()); + assertHiddenApisRoundTrip(effect); + } + } + + @Test + public void testPredefinedEffects_allEffectsWithNonDefaultFallback_onlySucceedsWithFlag() + throws IOException { + for (Map.Entry entry : createAllPredefinedEffectsMap().entrySet()) { + boolean nonDefaultFallback = !PrebakedSegment.DEFAULT_SHOULD_FALLBACK; + VibrationEffect effect = VibrationEffect.get(entry.getValue(), nonDefaultFallback); + String xml = String.format( + "", + entry.getKey(), nonDefaultFallback); + + assertPublicApisParserFails(xml); + assertPublicApisSerializerFails(effect); + + assertHiddenApisParserSucceeds(xml, effect); + assertHiddenApisSerializerSucceeds(effect, entry.getKey()); + assertHiddenApisRoundTrip(effect); + } + } + + private void assertPublicApisParserFails(String xml) throws IOException { + assertThat(parse(xml, /* flags= */ 0)).isNull(); + } + + private void assertPublicApisParserSucceeds(String xml, VibrationEffect effect) + throws IOException { + assertThat(parse(xml, /* flags= */ 0)).isEqualTo(effect); + } + + private void assertHiddenApisParserSucceeds(String xml, VibrationEffect effect) + throws IOException { + assertThat(parse(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS)).isEqualTo(effect); + } + + private void assertPublicApisSerializerFails(VibrationEffect effect) { + assertThrows("Expected serialization to fail for " + effect, + VibrationXmlSerializer.SerializationFailedException.class, + () -> serialize(effect, /* flags= */ 0)); + } + + private void assertPublicApisSerializerSucceeds(VibrationEffect effect, + String... expectedSegments) throws IOException { + assertSerializationContainsSegments(serialize(effect, /* flags= */ 0), expectedSegments); + } + + private void assertHiddenApisSerializerSucceeds(VibrationEffect effect, + String... expectedSegments) throws IOException { + assertSerializationContainsSegments( + serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS), expectedSegments); + } + + private void assertSerializationContainsSegments(String xml, String[] expectedSegments) { + for (String expectedSegment : expectedSegments) { + assertThat(xml).contains(expectedSegment); + } + } + + private void assertPublicApisRoundTrip(VibrationEffect effect) throws IOException { + assertThat(parse(serialize(effect, /* flags= */ 0), /* flags= */ 0)).isEqualTo(effect); + } + + private void assertHiddenApisRoundTrip(VibrationEffect effect) throws IOException { + String xml = serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS); + assertThat(parse(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS)).isEqualTo(effect); + } + + private static VibrationEffect parse(String xml, @VibrationXmlParser.Flags int flags) + throws IOException { + return VibrationXmlParser.parse(new StringReader(xml), flags); + } + + private static String serialize(VibrationEffect effect, @VibrationXmlSerializer.Flags int flags) + throws IOException { + StringWriter writer = new StringWriter(); + VibrationXmlSerializer.serialize(effect, writer, flags); + return writer.toString(); + } + + private static Map createAllPredefinedEffectsMap() { + Map map = createHiddenPredefinedEffectsMap(); + map.putAll(createPublicPredefinedEffectsMap()); + return map; + } + + private static Map createPublicPredefinedEffectsMap() { + Map map = new HashMap<>(); + map.put("tick", VibrationEffect.EFFECT_TICK); + map.put("click", VibrationEffect.EFFECT_CLICK); + map.put("heavy_click", VibrationEffect.EFFECT_HEAVY_CLICK); + map.put("double_click", VibrationEffect.EFFECT_DOUBLE_CLICK); + return map; + } + + private static Map createHiddenPredefinedEffectsMap() { + Map map = new HashMap<>(); + map.put("texture_tick", VibrationEffect.EFFECT_TEXTURE_TICK); + map.put("pop", VibrationEffect.EFFECT_POP); + map.put("thud", VibrationEffect.EFFECT_THUD); + for (int i = 0; i < VibrationEffect.RINGTONES.length; i++) { + map.put(String.format("ringtone_%d", i + 1), VibrationEffect.RINGTONES[i]); + } + return map; + } +} diff --git a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd new file mode 100644 index 0000000000000..cca1359da5966 --- /dev/null +++ b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/xsd/vibrator/vibration/vibration.xsd b/core/xsd/vibrator/vibration/vibration.xsd index 6d16c9f59d03b..b1a815a5eb8a9 100644 --- a/core/xsd/vibrator/vibration/vibration.xsd +++ b/core/xsd/vibrator/vibration/vibration.xsd @@ -90,7 +90,7 @@ - + @@ -104,7 +104,7 @@ - +