Merge "Add flags to VibrationEffect XML parser/serializer"

This commit is contained in:
TreeHugger Robot
2023-06-26 17:01:13 +00:00
committed by Android (Google) Code Review
11 changed files with 708 additions and 134 deletions

View File

@@ -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;

View File

@@ -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;
}
/**

View File

@@ -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
+ '}';
}

View File

@@ -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);
}
}
}

View File

@@ -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()]);
}
}

View File

@@ -54,41 +54,34 @@ public final class VibrationEffectXmlSerializer {
*/
@NonNull
public static XmlSerializedVibration<VibrationEffect> 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<VibrationEffectSegment> 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

View File

@@ -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.
*
* <p>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;
}

View File

@@ -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);
}
}
}

View File

@@ -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}.
*
* <p>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 = "<vibration>"
+ "<primitive-effect name=\"click\"/>"
+ "<primitive-effect name=\"tick\" scale=\"0.2497\"/>"
+ "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
+ "<primitive-effect name=\"spin\" scale=\"0.6364\" delayMs=\"7\"/>"
+ "</vibration>";
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 = "<vibration>"
+ "<waveform-effect><repeating>"
+ "<waveform-entry durationMs=\"123\" amplitude=\"254\"/>"
+ "<waveform-entry durationMs=\"456\" amplitude=\"1\"/>"
+ "<waveform-entry durationMs=\"789\" amplitude=\"255\"/>"
+ "<waveform-entry durationMs=\"0\" amplitude=\"0\"/>"
+ "</repeating></waveform-effect>"
+ "</vibration>";
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<String, Integer> entry : createPublicPredefinedEffectsMap().entrySet()) {
VibrationEffect effect = VibrationEffect.get(entry.getValue());
String xml = String.format(
"<vibration><predefined-effect name=\"%s\"/></vibration>", 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<String, Integer> entry : createHiddenPredefinedEffectsMap().entrySet()) {
VibrationEffect effect = VibrationEffect.get(entry.getValue());
String xml = String.format(
"<vibration><predefined-effect name=\"%s\"/></vibration>", 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<String, Integer> entry : createAllPredefinedEffectsMap().entrySet()) {
boolean nonDefaultFallback = !PrebakedSegment.DEFAULT_SHOULD_FALLBACK;
VibrationEffect effect = VibrationEffect.get(entry.getValue(), nonDefaultFallback);
String xml = String.format(
"<vibration><predefined-effect name=\"%s\" fallback=\"%s\"/></vibration>",
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<String, Integer> createAllPredefinedEffectsMap() {
Map<String, Integer> map = createHiddenPredefinedEffectsMap();
map.putAll(createPublicPredefinedEffectsMap());
return map;
}
private static Map<String, Integer> createPublicPredefinedEffectsMap() {
Map<String, Integer> 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<String, Integer> createHiddenPredefinedEffectsMap() {
Map<String, Integer> 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;
}
}

View File

@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<!--
This defines the format of the XML file used to define VibrationEffect created via public and
hidden APIs. This format is not enforced in a backwards compatible way, and should only be used
on device-specific vibration files.
-->
<xs:schema version="2.0"
elementFormDefault="qualified"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="vibration" type="Vibration"/>
<!-- Type definitions -->
<xs:complexType name="Vibration">
<xs:choice>
<!-- Waveform vibration effect -->
<xs:element name="waveform-effect" type="WaveformEffect"/>
<!-- Predefined vibration effect -->
<xs:element name="predefined-effect" type="PredefinedEffect"/>
<!-- Primitive composition effect -->
<xs:sequence>
<xs:element name="primitive-effect" type="PrimitiveEffect"/>
</xs:sequence>
</xs:choice>
</xs:complexType>
<xs:complexType name="WaveformEffect">
<xs:sequence>
<!-- Optional preamble, zero or more entries -->
<xs:element name="waveform-entry" type="WaveformEntry"
minOccurs="0" maxOccurs="unbounded"/>
<!-- Repeating element, with one or more entries -->
<xs:element name="repeating" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="waveform-entry" type="WaveformEntry"
maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<!-- Unsupported by xsd_config, but will be used in validation:
<xs:assert test="fn:count(./waveform-entry) > 0 or fn:count(./repeating) > 0)"/>
-->
</xs:complexType>
<xs:complexType name="WaveformEntry">
<xs:attribute name="durationMs" type="xs:nonNegativeInteger" use="required"/>
<xs:attribute name="amplitude" type="WaveformAmplitude" use="required"/>
</xs:complexType>
<xs:simpleType name="WaveformAmplitude">
<xs:union memberTypes="WaveformAmplitudeInt WaveformAmplitudeDefault"/>
</xs:simpleType>
<!-- Amplitude int in [0,255] -->
<xs:simpleType name="WaveformAmplitudeInt">
<xs:restriction base="xs:int">
<xs:minInclusive value="0"/>
<xs:maxInclusive value="255"/>
</xs:restriction>
</xs:simpleType>
<!-- Default amplitude as defined by VibrationEffect.DEFAULT_AMPLITUDE -->
<xs:simpleType name="WaveformAmplitudeDefault">
<xs:restriction base="xs:string">
<xs:enumeration value="default"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="PredefinedEffect">
<xs:attribute name="name" type="PredefinedEffectName" use="required"/>
<xs:attribute name="fallback" type="xs:boolean"/><!-- hidden -->
</xs:complexType>
<!-- Predefined effect names as defined by VibrationEffect.EFFECT_* -->
<xs:simpleType name="PredefinedEffectName">
<xs:restriction base="xs:string">
<!-- Public effects -->
<xs:enumeration value="tick"/>
<xs:enumeration value="click"/>
<xs:enumeration value="heavy_click"/>
<xs:enumeration value="double_click"/>
<!-- Hidden effects -->
<xs:enumeration value="texture_tick"/>
<xs:enumeration value="thud"/>
<xs:enumeration value="pop"/>
<xs:enumeration value="ringtone_1"/>
<xs:enumeration value="ringtone_2"/>
<xs:enumeration value="ringtone_3"/>
<xs:enumeration value="ringtone_4"/>
<xs:enumeration value="ringtone_5"/>
<xs:enumeration value="ringtone_6"/>
<xs:enumeration value="ringtone_7"/>
<xs:enumeration value="ringtone_8"/>
<xs:enumeration value="ringtone_9"/>
<xs:enumeration value="ringtone_10"/>
<xs:enumeration value="ringtone_11"/>
<xs:enumeration value="ringtone_12"/>
<xs:enumeration value="ringtone_13"/>
<xs:enumeration value="ringtone_14"/>
<xs:enumeration value="ringtone_15"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="PrimitiveEffect">
<xs:attribute name="name" type="PrimitiveEffectName" use="required"/>
<xs:attribute name="scale" type="PrimitiveScale"/>
<xs:attribute name="delayMs" type="xs:nonNegativeInteger"/>
</xs:complexType>
<!-- Primitive names as defined by VibrationEffect.Composition.PRIMITIVE_* -->
<xs:simpleType name="PrimitiveEffectName">
<xs:restriction base="xs:string">
<xs:enumeration value="low_tick"/>
<xs:enumeration value="tick"/>
<xs:enumeration value="click"/>
<xs:enumeration value="slow_rise"/>
<xs:enumeration value="quick_rise"/>
<xs:enumeration value="quick_fall"/>
<xs:enumeration value="spin"/>
<xs:enumeration value="thud"/>
</xs:restriction>
</xs:simpleType>
<!-- Primitive scale float in [0,1] -->
<xs:simpleType name="PrimitiveScale">
<xs:restriction base="xs:float">
<xs:minInclusive value="0"/>
<xs:maxInclusive value="1"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>

View File

@@ -90,7 +90,7 @@
</xs:simpleType>
<xs:complexType name="PredefinedEffect">
<xs:attribute name="name" type="PredefinedEffectName"/>
<xs:attribute name="name" type="PredefinedEffectName" use="required"/>
</xs:complexType>
<!-- Predefined effect names as defined by VibrationEffect.EFFECT_* -->
@@ -104,7 +104,7 @@
</xs:simpleType>
<xs:complexType name="PrimitiveEffect">
<xs:attribute name="name" type="PrimitiveEffectName"/>
<xs:attribute name="name" type="PrimitiveEffectName" use="required"/>
<xs:attribute name="scale" type="PrimitiveScale"/>
<xs:attribute name="delayMs" type="xs:nonNegativeInteger"/>
</xs:complexType>