diff options
| author | 2023-08-24 21:16:06 +0000 | |
|---|---|---|
| committer | 2023-08-24 21:16:06 +0000 | |
| commit | dca03676602c194e31227c1b37696e90d2294b4b (patch) | |
| tree | 4aa25d0072f75b9224844e06d578dbcc872e57b4 | |
| parent | 653581a0499ba49a6738cbb4778cc80c02c3302d (diff) | |
| parent | c908e136318b67538c4c1c9a1f7e2cca67b5d9e0 (diff) | |
Merge "Support vibration-select parsing" into main
22 files changed, 881 insertions, 124 deletions
diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 773d720d73fb..e34ddafc9e64 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -2704,8 +2704,14 @@ package android.os.vibrator { package android.os.vibrator.persistence { + public class ParsedVibration { + method @NonNull public java.util.List<android.os.VibrationEffect> getVibrationEffectListForTesting(); + method @Nullable public android.os.VibrationEffect resolve(@NonNull android.os.Vibrator); + } + public final class VibrationXmlParser { - method @Nullable public static android.os.VibrationEffect parse(@NonNull java.io.Reader) throws java.io.IOException; + method @Nullable public static android.os.vibrator.persistence.ParsedVibration parseDocument(@NonNull java.io.Reader) throws java.io.IOException; + method @Nullable public static android.os.VibrationEffect parseVibrationEffect(@NonNull java.io.Reader) throws java.io.IOException; } public final class VibrationXmlSerializer { diff --git a/core/java/android/hardware/input/InputDeviceVibrator.java b/core/java/android/hardware/input/InputDeviceVibrator.java index 9c1826071822..4577e1d7d4d3 100644 --- a/core/java/android/hardware/input/InputDeviceVibrator.java +++ b/core/java/android/hardware/input/InputDeviceVibrator.java @@ -82,7 +82,7 @@ final class InputDeviceVibrator extends Vibrator { } @Override - protected VibratorInfo getInfo() { + public VibratorInfo getInfo() { return mVibratorInfo; } diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java index 2cda787082c7..1cd0f3b156c2 100644 --- a/core/java/android/os/SystemVibrator.java +++ b/core/java/android/os/SystemVibrator.java @@ -62,7 +62,7 @@ public class SystemVibrator extends Vibrator { } @Override - protected VibratorInfo getInfo() { + public VibratorInfo getInfo() { synchronized (mLock) { if (mVibratorInfo != null) { return mVibratorInfo; diff --git a/core/java/android/os/SystemVibratorManager.java b/core/java/android/os/SystemVibratorManager.java index eb2a712c8575..284b2464c468 100644 --- a/core/java/android/os/SystemVibratorManager.java +++ b/core/java/android/os/SystemVibratorManager.java @@ -194,7 +194,7 @@ public class SystemVibratorManager extends VibratorManager { } @Override - protected VibratorInfo getInfo() { + public VibratorInfo getInfo() { return mVibratorInfo; } diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java index 79e0ca87eade..aafa5018af10 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -153,7 +153,7 @@ public abstract class Vibrator { * * @hide */ - protected VibratorInfo getInfo() { + public VibratorInfo getInfo() { return VibratorInfo.EMPTY_VIBRATOR_INFO; } diff --git a/core/java/android/os/vibrator/persistence/ParsedVibration.java b/core/java/android/os/vibrator/persistence/ParsedVibration.java new file mode 100644 index 000000000000..a76f597252ec --- /dev/null +++ b/core/java/android/os/vibrator/persistence/ParsedVibration.java @@ -0,0 +1,92 @@ +/* + * Copyright 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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TestApi; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.os.VibratorInfo; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Collections; +import java.util.List; + +/** + * The result of parsing a serialized vibration, which can be define by one or more + * {@link VibrationEffect} and a resolution method. + * + * @hide + */ +@TestApi +public class ParsedVibration { + private final List<VibrationEffect> mEffects; + + /** @hide */ + public ParsedVibration(@NonNull List<VibrationEffect> effects) { + mEffects = effects; + } + + /** @hide */ + public ParsedVibration(@NonNull VibrationEffect effect) { + mEffects = List.of(effect); + } + /** + * Returns the first parsed vibration supported by {@code vibrator}, or {@code null} if none of + * the parsed vibrations are supported. + * + * @hide + */ + @TestApi + @Nullable + public VibrationEffect resolve(@NonNull Vibrator vibrator) { + return resolve(vibrator.getInfo()); + } + + /** + * Returns the parsed vibrations for testing purposes. + * + * <p>Real callers should not use this method. Instead, they should resolve to a + * {@link VibrationEffect} via {@link #resolve(Vibrator)}. + * + * @hide + */ + @TestApi + @VisibleForTesting + @NonNull + public List<VibrationEffect> getVibrationEffectListForTesting() { + return Collections.unmodifiableList(mEffects); + } + + /** + * Same as {@link #resolve(Vibrator)}, but uses {@link VibratorInfo} instead for resolving. + * + * @hide + */ + @Nullable + public final VibrationEffect resolve(@NonNull VibratorInfo info) { + for (int i = 0; i < mEffects.size(); i++) { + VibrationEffect effect = mEffects.get(i); + if (info.areVibrationFeaturesSupported(effect)) { + return effect; + } + } + return null; + } +} diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlParser.java b/core/java/android/os/vibrator/persistence/VibrationXmlParser.java index e91e04ec9cd1..e08cc4262bed 100644 --- a/core/java/android/os/vibrator/persistence/VibrationXmlParser.java +++ b/core/java/android/os/vibrator/persistence/VibrationXmlParser.java @@ -28,6 +28,7 @@ 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.XmlValidator; import com.android.modules.utils.TypedXmlPullParser; import org.xmlpull.v1.XmlPullParser; @@ -37,11 +38,18 @@ import java.io.IOException; import java.io.Reader; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; /** * Parses XML into a {@link VibrationEffect}. * - * <p>This parser supports a root element that represent a single vibration effect as follows: + * <p>This parser supports a root element that represent a single vibration effect or a selection + * list of vibration effects. + * + * <p>Use the schema at core/xsd/vibrator/vibration/vibration.xsd. + * + * <p>When the root element represents a single vibration effect, the format is as follows: * * * Predefined vibration effects * @@ -85,6 +93,26 @@ import java.lang.annotation.RetentionPolicy; * } * </pre> * + * <p>When the root element represents a selection list of vibration effects, the root tag should be + * a <vibration-select> tag. The root element should contain a list of vibration serializations. + * Each vibration within the root-element should follow the format discussed for the <vibration> tag + * above. See example below: + * + * <pre> + * {@code + * <vibration-select> + * <vibration> + * <predefined-effect name="click" /> + * </vibration> + * <vibration> + * <waveform-effect> + * <waveform-entry amplitude="default" durationMs="10" /> + * </waveform-effect> + * </vibration> + * </vibration-select> + * } + * </pre> + * * @hide */ @TestApi @@ -140,6 +168,9 @@ public final class VibrationXmlParser { /** * Parses XML content from given input stream into a {@link VibrationEffect}. * + * <p>This method parses an XML content that contains a single, complete {@link VibrationEffect} + * serialization. As such, the root tag must be a "vibration" tag. + * * <p>This parser fails silently and returns {@code null} if the content of the input stream * does not follow the schema or has unsupported values. * @@ -150,75 +181,106 @@ public final class VibrationXmlParser { */ @TestApi @Nullable - public static VibrationEffect parse(@NonNull Reader reader) throws IOException { - return parse(reader, /* flags= */ 0); + public static VibrationEffect parseVibrationEffect(@NonNull Reader reader) throws IOException { + return parseVibrationEffect(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. + * <p>This method parses an XML content that contains a single, complete {@link VibrationEffect} + * serialization. As such, the root tag must be a "vibration" tag. + * + * <p>Same as {@link #parseVibrationEffect(Reader)}, with extra flags to control the parsing + * behavior. * * @hide */ @Nullable - public static VibrationEffect parse(@NonNull Reader reader, @Flags int flags) + public static VibrationEffect parseVibrationEffect(@NonNull Reader reader, @Flags int flags) throws IOException { - TypedXmlPullParser parser = Xml.newFastPullParser(); - try { - parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); - parser.setInput(reader); - } catch (XmlPullParserException e) { - throw new RuntimeException("An error occurred while setting up the XML parser", e); + return parseDocumentInternal( + reader, flags, VibrationXmlParser::parseVibrationEffectInternal); + } catch (XmlParserException | XmlPullParserException e) { + Slog.w(TAG, "Error parsing vibration XML", e); + return null; } + } - try { - // Ensure XML starts with expected root tag. - XmlReader.readDocumentStartTag(parser, XmlConstants.TAG_VIBRATION); - - // Parse root tag as a vibration effect. - VibrationEffect effect = parseTag(parser, flags); - - // Ensure XML ends after root tag is consumed. - XmlReader.readDocumentEndTag(parser); + /** + * Parses XML content from given input stream into a {@link ParsedVibration}. + * + * <p>It supports both the "vibration" and "vibration-select" root tags. + * <ul> + * <li>If "vibration" is the root tag, the serialization provided through {@code reader} + * should contain a valid serialization for a single vibration. + * <li>If "vibration-select" is the root tag, the serialization may contain one or more + * valid vibration serializations. + * </ul> + * + * <p>After parsing, it returns a {@link ParsedVibration} that opaquely represents the parsed + * vibration(s), and the caller can get a concrete {@link VibrationEffect} by resolving this + * result to a specific vibrator. + * + * <p>This parser fails silently and returns {@code null} if the content of the input does not + * follow the schema or has unsupported values. + * + * @return a {@link ParsedVibration} + * @throws IOException error reading from given {@link Reader} + * + * @hide + */ + @TestApi + @Nullable + public static ParsedVibration parseDocument(@NonNull Reader reader) throws IOException { + return parseDocument(reader, /* flags= */ 0); + } - return effect; - } catch (XmlParserException | VibrationXmlParserException e) { - Slog.w(TAG, "Error parsing vibration XML", e); + /** + * Parses XML content from given input stream into a {@link ParsedVibration}. + * + * <p>Same as {@link #parseDocument(Reader)}, with extra flags to control the parsing behavior. + * + * @hide + */ + @Nullable + public static ParsedVibration parseDocument(@NonNull Reader reader, @Flags int flags) + throws IOException { + try { + return parseDocumentInternal(reader, flags, VibrationXmlParser::parseElementInternal); + } catch (XmlParserException | XmlPullParserException e) { + Slog.w(TAG, "Error parsing vibration/vibration-select XML", e); return null; } } /** - * Parses XML content from given open {@link TypedXmlPullParser} into a {@link VibrationEffect}. + * Parses XML content from a given open {@link TypedXmlPullParser} into a + * {@link ParsedVibration}. * - * <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>Same as {@link #parseDocument(Reader, int)}, but, instead of parsing the full XML content, + * it takes a parser that points to either a <vibration> or a <vibration-select> start 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). + * <p>This method parses until an end "vibration" or "vibration-select" tag (depending on the + * start tag found at the start of parsing). After a successful parsing, the parser will point + * to the end 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 + * {@link ParsedVibration}. 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) + public static ParsedVibration parseElement(@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(); + return parseElementInternal(parser, flags); } catch (XmlParserException e) { - throw new VibrationXmlParserException("Error parsing vibration effect.", e); + throw new VibrationXmlParserException("Error parsing vibration-select.", e); } } @@ -231,6 +293,82 @@ public final class VibrationXmlParser { private VibrationXmlParserException(String message, Throwable cause) { super(message, cause); } + + private VibrationXmlParserException(String message) { + super(message); + } + } + + private static ParsedVibration parseElementInternal( + @NonNull TypedXmlPullParser parser, @Flags int flags) + throws IOException, XmlParserException { + XmlValidator.checkStartTag(parser); + + String tagName = parser.getName(); + switch(tagName) { + case XmlConstants.TAG_VIBRATION: + return new ParsedVibration(parseVibrationEffectInternal(parser, flags)); + case XmlConstants.TAG_VIBRATION_SELECT: + return parseVibrationSelectInternal(parser, flags); + default: + throw new XmlParserException( + "Unexpected tag name when parsing element: " + tagName); + } + } + + private static ParsedVibration parseVibrationSelectInternal( + @NonNull TypedXmlPullParser parser, @Flags int flags) + throws IOException, XmlParserException { + XmlValidator.checkStartTag(parser, XmlConstants.TAG_VIBRATION_SELECT); + XmlValidator.checkTagHasNoUnexpectedAttributes(parser); + + int rootDepth = parser.getDepth(); + List<VibrationEffect> effects = new ArrayList<>(); + while (XmlReader.readNextTagWithin(parser, rootDepth)) { + effects.add(parseVibrationEffectInternal(parser, flags)); + } + return new ParsedVibration(effects); + } + + /** Parses a single XML element for "vibration" tag into a {@link VibrationEffect}. */ + private static VibrationEffect parseVibrationEffectInternal( + @NonNull TypedXmlPullParser parser, @Flags int flags) + throws IOException, XmlParserException { + int parserFlags = 0; + if ((flags & VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS) != 0) { + parserFlags |= XmlConstants.FLAG_ALLOW_HIDDEN_APIS; + } + return VibrationEffectXmlParser.parseTag(parser, parserFlags).deserialize(); + } + + /** + * This method parses a whole XML document (provided through a {@link Reader}). The root tag is + * parsed as per a provided {@link ElementParser}. + */ + private static <T> T parseDocumentInternal( + @NonNull Reader reader, @Flags int flags, ElementParser<T> parseLogic) + throws IOException, XmlParserException, XmlPullParserException { + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(reader); + + // Ensure XML starts with a document start tag. + XmlReader.readDocumentStart(parser); + + // Parse root tag. + T result = parseLogic.parse(parser, flags); + + // Ensure XML ends after root tag is consumed. + XmlReader.readDocumentEndTag(parser); + + return result; + } + + /** Encapsulate a logic to parse an XML element from an open parser. */ + private interface ElementParser<T> { + /** Parses a single XML element starting from the current position of the {@code parser}. */ + T parse(@NonNull TypedXmlPullParser parser, @Flags int flags) + throws IOException, XmlParserException; } private VibrationXmlParser() { diff --git a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java index d1c78f0b54ea..6b69a158220c 100644 --- a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java +++ b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java @@ -36,6 +36,7 @@ public final class XmlConstants { public static final String NAMESPACE = null; public static final String TAG_VIBRATION = "vibration"; + public static final String TAG_VIBRATION_SELECT = "vibration-select"; public static final String TAG_PREDEFINED_EFFECT = "predefined-effect"; public static final String TAG_PRIMITIVE_EFFECT = "primitive-effect"; diff --git a/core/java/com/android/internal/vibrator/persistence/XmlReader.java b/core/java/com/android/internal/vibrator/persistence/XmlReader.java index 75073387c143..a5ace8438142 100644 --- a/core/java/com/android/internal/vibrator/persistence/XmlReader.java +++ b/core/java/com/android/internal/vibrator/persistence/XmlReader.java @@ -41,17 +41,26 @@ public final class XmlReader { */ public static void readDocumentStartTag(TypedXmlPullParser parser, String expectedRootTag) throws XmlParserException, IOException { - try { - int type = parser.getEventType(); - checkArgument(type == XmlPullParser.START_DOCUMENT, "Document already started"); + readDocumentStart(parser); - type = parser.nextTag(); // skips comments, instruction tokens and whitespace only - XmlValidator.checkParserCondition(type == XmlPullParser.START_TAG, - "Unexpected element at document start, expected root tag %s", expectedRootTag); + String tagName = parser.getName(); + XmlValidator.checkParserCondition(expectedRootTag.equals(tagName), + "Unexpected root tag found %s, expected %s", tagName, expectedRootTag); + } - String tagName = parser.getName(); - XmlValidator.checkParserCondition(expectedRootTag.equals(tagName), - "Unexpected root tag found %s, expected %s", tagName, expectedRootTag); + /** + * Check parser is currently at {@link XmlPullParser#START_DOCUMENT}. + * + * <p>The parser will be pointing to the first tag in the document. + */ + public static void readDocumentStart(TypedXmlPullParser parser) + throws XmlParserException, IOException { + try { + int type = parser.getEventType(); + checkArgument( + type == XmlPullParser.START_DOCUMENT, + "Unexpected type, expected %d", type); + parser.nextTag(); // skips comments, instruction tokens and whitespace only } catch (XmlPullParserException e) { throw XmlParserException.createFromPullParserException("document start tag", e); } diff --git a/core/java/com/android/internal/vibrator/persistence/XmlValidator.java b/core/java/com/android/internal/vibrator/persistence/XmlValidator.java index ba95e35d2a35..84d4f3f49e8a 100644 --- a/core/java/com/android/internal/vibrator/persistence/XmlValidator.java +++ b/core/java/com/android/internal/vibrator/persistence/XmlValidator.java @@ -42,13 +42,20 @@ public final class XmlValidator { */ public static void checkStartTag(TypedXmlPullParser parser, String expectedTag) throws XmlParserException { - String tagName = parser.getName(); + checkStartTag(parser); + checkParserCondition( + expectedTag.equals(parser.getName()), + "Unexpected start tag found %s, expected %s", parser.getName(), expectedTag); + } + + /** Check parser is currently at {@link XmlPullParser#START_TAG}. */ + public static void checkStartTag(TypedXmlPullParser parser) throws XmlParserException { try { checkParserCondition( - parser.getEventType() == parser.START_TAG && expectedTag.equals(tagName), - "Unexpected tag found %s, expected %s", tagName, expectedTag); + parser.getEventType() == parser.START_TAG, + "Expected start tag, got " + parser.getEventType()); } catch (XmlPullParserException e) { - throw XmlParserException.createFromPullParserException(tagName, e); + throw XmlParserException.createFromPullParserException(parser.getName(), e); } } diff --git a/core/tests/vibrator/src/android/os/vibrator/persistence/ParsedVibrationTest.java b/core/tests/vibrator/src/android/os/vibrator/persistence/ParsedVibrationTest.java new file mode 100644 index 000000000000..274c25a17f58 --- /dev/null +++ b/core/tests/vibrator/src/android/os/vibrator/persistence/ParsedVibrationTest.java @@ -0,0 +1,117 @@ +/* + * 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 com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.os.VibratorInfo; + +import com.google.common.truth.Subject; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.MockitoRule; + +import java.util.List; + +/** Unit tests for {@link ParsedVibration}. */ +@RunWith(MockitoJUnitRunner.class) +public class ParsedVibrationTest { + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock Vibrator mVibratorMock; + @Mock VibratorInfo mVibratorInfoMock; + + @Mock VibrationEffect mEffect1; + @Mock VibrationEffect mEffect2; + @Mock VibrationEffect mEffect3; + + @Before + public void setUp() { + when(mVibratorMock.getInfo()).thenReturn(mVibratorInfoMock); + } + + @Test + public void empty() { + assertThat(new ParsedVibration(List.of()).resolve(mVibratorMock)).isNull(); + } + + @Test + public void testResolve_allUnsupportedVibrations() { + when(mVibratorInfoMock.areVibrationFeaturesSupported(any())).thenReturn(false); + + assertThatResolution(mVibratorMock, mEffect1).isNull(); + assertThatResolution(mVibratorMock, List.of(mEffect1, mEffect2)).isNull(); + } + + @Test + public void testResolve_allSupportedVibrations() { + when(mVibratorInfoMock.areVibrationFeaturesSupported(any())).thenReturn(true); + + assertThatResolution(mVibratorMock, mEffect1).isEqualTo(mEffect1); + assertThatResolution(mVibratorMock, List.of(mEffect1, mEffect2)).isEqualTo(mEffect1); + } + + @Test + public void testResolve_mixedSupportedAndUnsupportedVibrations() { + when(mVibratorInfoMock.areVibrationFeaturesSupported(mEffect1)).thenReturn(true); + when(mVibratorInfoMock.areVibrationFeaturesSupported(mEffect2)).thenReturn(true); + when(mVibratorInfoMock.areVibrationFeaturesSupported(mEffect3)).thenReturn(false); + + assertThatResolution(mVibratorMock, List.of(mEffect1, mEffect3)).isEqualTo(mEffect1); + assertThatResolution(mVibratorMock, List.of(mEffect3, mEffect1, mEffect2)) + .isEqualTo(mEffect1); + assertThatResolution(mVibratorMock, List.of(mEffect1, mEffect2, mEffect3)) + .isEqualTo(mEffect1); + } + + @Test + public void testGetVibrationEffectListForTesting() { + ParsedVibration parsedVibration = + new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3)); + assertThat(parsedVibration.getVibrationEffectListForTesting()) + .containsExactly(mEffect1, mEffect2, mEffect3) + .inOrder(); + + parsedVibration = new ParsedVibration(List.of(mEffect1)); + assertThat(parsedVibration.getVibrationEffectListForTesting()).containsExactly(mEffect1); + + parsedVibration = new ParsedVibration(List.of()); + assertThat(parsedVibration.getVibrationEffectListForTesting()).isEmpty(); + } + + private Subject assertThatResolution( + Vibrator vibrator, List<VibrationEffect> componentVibrations) { + return assertThat(new ParsedVibration(componentVibrations).resolve(vibrator)); + } + + private Subject assertThatResolution(Vibrator vibrator, VibrationEffect vibration) { + return assertThat(new ParsedVibration(vibration).resolve(vibrator)); + } +} diff --git a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java index ce1717057a1c..d73b5cb5713b 100644 --- a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java +++ b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java @@ -42,6 +42,7 @@ import java.io.StringReader; import java.io.StringWriter; import java.util.HashMap; import java.util.Map; +import java.util.Set; /** * Unit tests for {@link VibrationXmlParser} and {@link VibrationXmlSerializer}. @@ -67,7 +68,8 @@ public class VibrationEffectXmlSerializationTest { } @Test - public void testParseTag_succeedAndParserPointsToEndVibrationTag() throws Exception { + public void testParseElement_fromVibrationTag_succeedAndParserPointsToEndVibrationTag() + throws Exception { VibrationEffect effect = VibrationEffect.startComposition() .addPrimitive(PRIMITIVE_CLICK) .addPrimitive(PRIMITIVE_TICK, 0.2497f) @@ -86,7 +88,7 @@ public class VibrationEffectXmlSerializationTest { + "</vibration>"; TypedXmlPullParser parser = createXmlPullParser(xml); - assertParseTagSucceeds(parser, effect); + assertParseElementSucceeds(parser, effect); parser.next(); assertEndOfDocument(parser); @@ -95,30 +97,134 @@ public class VibrationEffectXmlSerializationTest { parser = createXmlPullParser("<next-tag>" + xml + "</next-tag>"); // Move the parser once to point to the "<vibration> tag. parser.next(); - assertParseTagSucceeds(parser, effect); + assertParseElementSucceeds(parser, effect); parser.next(); assertEndTag(parser, "next-tag"); parser = createXmlPullParser(xml + "<next-tag>"); - assertParseTagSucceeds(parser, effect); + assertParseElementSucceeds(parser, effect); parser.next(); assertStartTag(parser, "next-tag"); parser = createXmlPullParser(xml + xml2); - assertParseTagSucceeds(parser, effect); + assertParseElementSucceeds(parser, effect); + parser.next(); + assertParseElementSucceeds(parser, effect2); + parser.next(); + assertEndOfDocument(parser); + + // Check when there is comment before the end tag. + xml = "<vibration><primitive-effect name=\"tick\"/><!-- comment --></vibration>"; + parser = createXmlPullParser(xml); + assertParseElementSucceeds( + parser, VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK).compose()); + } + + @Test + public void + testParseElement_fromVibrationSelectTag_succeedAndParserPointsToEndVibrationSelectTag() + throws Exception { + VibrationEffect effect1 = VibrationEffect.startComposition() + .addPrimitive(PRIMITIVE_CLICK) + .addPrimitive(PRIMITIVE_TICK, 0.2497f) + .compose(); + String vibrationXml1 = "<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 vibrationXml2 = "<vibration>" + + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>" + + "<primitive-effect name=\"spin\" scale=\"0.6364\" delayMs=\"7\"/>" + + "</vibration>"; + + String xml = "<vibration-select>" + vibrationXml1 + vibrationXml2 + "</vibration-select>"; + TypedXmlPullParser parser = createXmlPullParser(xml); + assertParseElementSucceeds(parser, effect1, effect2); + 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(); + assertParseElementSucceeds(parser, effect1, effect2); + parser.next(); + assertEndTag(parser, "next-tag"); + + parser = createXmlPullParser(xml + "<next-tag>"); + assertParseElementSucceeds(parser, effect1, effect2); parser.next(); - assertParseTagSucceeds(parser, effect2); + assertStartTag(parser, "next-tag"); + + xml = "<vibration-select>" + vibrationXml1 + vibrationXml2 + "</vibration-select>" + + "<vibration-select>" + vibrationXml2 + vibrationXml1 + "</vibration-select>" + + vibrationXml1; + parser = createXmlPullParser(xml); + assertParseElementSucceeds(parser, effect1, effect2); + parser.next(); + assertParseElementSucceeds(parser, effect2, effect1); + parser.next(); + assertParseElementSucceeds(parser, effect1); parser.next(); assertEndOfDocument(parser); + + // Check when there is comment before the end tag. + xml = "<vibration-select>" + vibrationXml1 + "<!-- comment --></vibration-select>"; + parser = createXmlPullParser(xml); + parser.next(); + assertParseElementSucceeds(parser, effect1); + } + + @Test + public void testParseElement_withHiddenApis_onlySucceedsWithFlag() throws Exception { + // Check when the root tag is "vibration". + String xml = "<vibration><predefined-effect name=\"texture_tick\"/></vibration>"; + assertParseElementSucceeds(createXmlPullParser(xml), + VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS, + VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK)); + assertParseElementFails(xml); + + // Check when the root tag is "vibration-select". + xml = "<vibration-select>" + xml + "</vibration-select>"; + assertParseElementSucceeds(createXmlPullParser(xml), + VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS, + VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK)); + assertParseElementFails(xml); } @Test - public void testParseTag_badXml_throwsException() throws Exception { - assertParseTagFails( + public void testParseElement_badXml_throwsException() throws Exception { + // No "vibration-select" tag. + assertParseElementFails( "<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\"/>"); + assertParseElementFails("<bad-tag><primitive-effect name=\"click\"/></vibration>"); + assertParseElementFails("<primitive-effect name=\"click\"/></vibration>"); + assertParseElementFails("<vibration><primitive-effect name=\"click\"/>"); + + // Incomplete XML. + assertParseElementFails("<vibration-select><primitive-effect name=\"click\"/>"); + assertParseElementFails("<vibration-select>" + + "<vibration>" + + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>" + + "</vibration>"); + + // Bad vibration XML. + assertParseElementFails("<vibration-select>" + + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>" + + "</vibration>" + + "</vibration-select>"); + + // "vibration-select" tag should have no attributes. + assertParseElementFails("<vibration-select bad_attr=\"123\">" + + "<vibration>" + + "<predefined-effect name=\"tick\"/>" + + "</vibration>" + + "</vibration-select>"); } @Test @@ -140,12 +246,30 @@ public class VibrationEffectXmlSerializationTest { assertPublicApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin"); assertPublicApisRoundTrip(effect); - assertHiddenApisParserSucceeds(xml, effect); + assertHiddenApisParseVibrationEffectSucceeds(xml, effect); assertHiddenApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin"); assertHiddenApisRoundTrip(effect); } @Test + public void testParseDocument_withVibrationSelectTag_withHiddenApis_onlySucceedsWithFlag() + throws Exception { + // Check when the root tag is "vibration". + String xml = "<vibration><predefined-effect name=\"texture_tick\"/></vibration>"; + assertParseDocumentSucceeds(xml, + VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS, + VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK)); + assertThat(parseDocument(xml, /* flags= */ 0)).isNull(); + + // Check when the root tag is "vibration-select". + xml = "<vibration-select>" + xml + "</vibration-select>"; + assertParseDocumentSucceeds(xml, + VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS, + VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK)); + assertThat(parseDocument(xml, /* flags= */ 0)).isNull(); + } + + @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); @@ -162,7 +286,7 @@ public class VibrationEffectXmlSerializationTest { assertPublicApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0"); assertPublicApisRoundTrip(effect); - assertHiddenApisParserSucceeds(xml, effect); + assertHiddenApisParseVibrationEffectSucceeds(xml, effect); assertHiddenApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0"); assertHiddenApisRoundTrip(effect); } @@ -179,7 +303,7 @@ public class VibrationEffectXmlSerializationTest { assertPublicApisSerializerSucceeds(effect, entry.getKey()); assertPublicApisRoundTrip(effect); - assertHiddenApisParserSucceeds(xml, effect); + assertHiddenApisParseVibrationEffectSucceeds(xml, effect); assertHiddenApisSerializerSucceeds(effect, entry.getKey()); assertHiddenApisRoundTrip(effect); } @@ -195,7 +319,7 @@ public class VibrationEffectXmlSerializationTest { assertPublicApisParserFails(xml); assertPublicApisSerializerFails(effect); - assertHiddenApisParserSucceeds(xml, effect); + assertHiddenApisParseVibrationEffectSucceeds(xml, effect); assertHiddenApisSerializerSucceeds(effect, entry.getKey()); assertHiddenApisRoundTrip(effect); } @@ -214,19 +338,19 @@ public class VibrationEffectXmlSerializationTest { assertPublicApisParserFails(xml); assertPublicApisSerializerFails(effect); - assertHiddenApisParserSucceeds(xml, effect); + assertHiddenApisParseVibrationEffectSucceeds(xml, effect); assertHiddenApisSerializerSucceeds(effect, entry.getKey()); assertHiddenApisRoundTrip(effect); } } private void assertPublicApisParserFails(String xml) throws IOException { - assertThat(parse(xml, /* flags= */ 0)).isNull(); + assertThat(parseVibrationEffect(xml, /* flags= */ 0)).isNull(); } private void assertPublicApisParserSucceeds(String xml, VibrationEffect effect) throws IOException { - assertThat(parse(xml, /* flags= */ 0)).isEqualTo(effect); + assertThat(parseVibrationEffect(xml, /* flags= */ 0)).isEqualTo(effect); } private TypedXmlPullParser createXmlPullParser(String xml) throws Exception { @@ -237,16 +361,30 @@ public class VibrationEffectXmlSerializationTest { return parser; } + private void assertParseDocumentSucceeds(String xml, int flags, VibrationEffect... effects) + throws Exception { + assertThat(parseDocument(xml, flags).getVibrationEffectListForTesting()) + .containsExactly(effects); + } + /** * Asserts parsing vibration from an open TypedXmlPullParser succeeds, and that the parser - * points to the end "vibration" tag. + * points to the end "vibration" or "vibration-select" tag. */ - private void assertParseTagSucceeds( - TypedXmlPullParser parser, VibrationEffect effect) throws Exception { - assertThat(parseTag(parser)).isEqualTo(effect); + private void assertParseElementSucceeds( + TypedXmlPullParser parser, VibrationEffect... effects) throws Exception { + assertParseElementSucceeds(parser, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS, effects); + } + private void assertParseElementSucceeds( + TypedXmlPullParser parser, int flags, VibrationEffect... effects) throws Exception { + String tagName = parser.getName(); + assertThat(Set.of("vibration", "vibration-select")).contains(tagName); + + assertThat(parseElement(parser, flags).getVibrationEffectListForTesting()) + .containsExactly(effects); assertThat(parser.getEventType()).isEqualTo(XmlPullParser.END_TAG); - assertThat(parser.getName()).isEqualTo("vibration"); + assertThat(parser.getName()).isEqualTo(tagName); } private void assertEndTag(TypedXmlPullParser parser, String expectedTagName) throws Exception { @@ -264,9 +402,10 @@ public class VibrationEffectXmlSerializationTest { assertThat(parser.getEventType()).isEqualTo(parser.END_DOCUMENT); } - private void assertHiddenApisParserSucceeds(String xml, VibrationEffect effect) + private void assertHiddenApisParseVibrationEffectSucceeds(String xml, VibrationEffect effect) throws IOException { - assertThat(parse(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS)).isEqualTo(effect); + assertThat(parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS)) + .isEqualTo(effect); } private void assertPublicApisSerializerFails(VibrationEffect effect) { @@ -275,10 +414,10 @@ public class VibrationEffectXmlSerializationTest { () -> serialize(effect, /* flags= */ 0)); } - private void assertParseTagFails(String xml) { + private void assertParseElementFails(String xml) { assertThrows("Expected parsing to fail for " + xml, VibrationXmlParser.VibrationXmlParserException.class, - () -> parseTag(createXmlPullParser(xml))); + () -> parseElement(createXmlPullParser(xml), /* flags= */ 0)); } private void assertPublicApisSerializerSucceeds(VibrationEffect effect, @@ -299,22 +438,29 @@ public class VibrationEffectXmlSerializationTest { } private void assertPublicApisRoundTrip(VibrationEffect effect) throws IOException { - assertThat(parse(serialize(effect, /* flags= */ 0), /* flags= */ 0)).isEqualTo(effect); + assertThat(parseVibrationEffect(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); + assertThat(parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS)) + .isEqualTo(effect); + } + + private static VibrationEffect parseVibrationEffect( + String xml, @VibrationXmlParser.Flags int flags) throws IOException { + return VibrationXmlParser.parseVibrationEffect(new StringReader(xml), flags); } - private static VibrationEffect parse(String xml, @VibrationXmlParser.Flags int flags) + private static ParsedVibration parseDocument(String xml, int flags) throws IOException { - return VibrationXmlParser.parse(new StringReader(xml), flags); + return VibrationXmlParser.parseDocument(new StringReader(xml), flags); } - private static VibrationEffect parseTag(TypedXmlPullParser parser) + private static ParsedVibration parseElement(TypedXmlPullParser parser, int flags) throws IOException, VibrationXmlParser.VibrationXmlParserException { - return VibrationXmlParser.parseTag(parser, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS); + return VibrationXmlParser.parseElement(parser, flags); } private static String serialize(VibrationEffect effect, @VibrationXmlSerializer.Flags int flags) diff --git a/core/xsd/Android.bp b/core/xsd/Android.bp index f49a159585a5..4e418d6cc0f0 100644 --- a/core/xsd/Android.bp +++ b/core/xsd/Android.bp @@ -19,4 +19,8 @@ xsd_config { srcs: ["vibrator/vibration/vibration.xsd"], api_dir: "vibrator/vibration/schema", package_name: "com.android.internal.vibrator.persistence", + root_elements: [ + "vibration", + "vibration-select", + ], } diff --git a/core/xsd/vibrator/vibration/schema/current.txt b/core/xsd/vibrator/vibration/schema/current.txt index 121a2285d7f6..176638462397 100644 --- a/core/xsd/vibrator/vibration/schema/current.txt +++ b/core/xsd/vibrator/vibration/schema/current.txt @@ -47,6 +47,11 @@ package com.android.internal.vibrator.persistence { method public void setWaveformEffect_optional(com.android.internal.vibrator.persistence.WaveformEffect); } + public class VibrationSelect { + ctor public VibrationSelect(); + method public java.util.List<com.android.internal.vibrator.persistence.Vibration> getVibration(); + } + public enum WaveformAmplitudeDefault { method public String getRawName(); enum_constant public static final com.android.internal.vibrator.persistence.WaveformAmplitudeDefault _default; @@ -74,8 +79,9 @@ package com.android.internal.vibrator.persistence { public class XmlParser { ctor public XmlParser(); - method public static com.android.internal.vibrator.persistence.Vibration read(java.io.InputStream) throws javax.xml.datatype.DatatypeConfigurationException, java.io.IOException, org.xmlpull.v1.XmlPullParserException; method public static String readText(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException; + method public static com.android.internal.vibrator.persistence.Vibration readVibration(java.io.InputStream) throws javax.xml.datatype.DatatypeConfigurationException, java.io.IOException, org.xmlpull.v1.XmlPullParserException; + method public static com.android.internal.vibrator.persistence.VibrationSelect readVibrationSelect(java.io.InputStream) throws javax.xml.datatype.DatatypeConfigurationException, java.io.IOException, org.xmlpull.v1.XmlPullParserException; method public static void skip(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException; } diff --git a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd index cca1359da596..679b9fa019ac 100644 --- a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd +++ b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd @@ -23,10 +23,20 @@ elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <!-- Root tag definitions --> + <xs:element name="vibration" type="Vibration"/> + <xs:element name="vibration-select" type="VibrationSelect"/> + <!-- Type definitions --> + <xs:complexType name="VibrationSelect"> + <xs:sequence> + <xs:element name="vibration" type="Vibration" minOccurs="0" maxOccurs="unbounded" /> + </xs:sequence> + </xs:complexType> + <xs:complexType name="Vibration"> <xs:choice> diff --git a/core/xsd/vibrator/vibration/vibration.xsd b/core/xsd/vibrator/vibration/vibration.xsd index b1a815a5eb8a..8406562b8579 100644 --- a/core/xsd/vibrator/vibration/vibration.xsd +++ b/core/xsd/vibrator/vibration/vibration.xsd @@ -21,10 +21,20 @@ elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <!-- Root tag definitions --> + <xs:element name="vibration" type="Vibration"/> + <xs:element name="vibration-select" type="VibrationSelect"/> + <!-- Type definitions --> + <xs:complexType name="VibrationSelect"> + <xs:sequence> + <xs:element name="vibration" type="Vibration" minOccurs="0" maxOccurs="unbounded" /> + </xs:sequence> + </xs:complexType> + <xs:complexType name="Vibration"> <xs:choice> diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java index 862948e3f41d..27811e9567af 100644 --- a/services/core/java/com/android/server/power/ShutdownThread.java +++ b/services/core/java/com/android/server/power/ShutdownThread.java @@ -851,9 +851,10 @@ public final class ShutdownThread extends Thread { */ private VibrationEffect getValidShutdownVibration(Context context, Vibrator vibrator) { VibrationEffect parsedEffect = parseVibrationEffectFromFile( - mInjector.getDefaultShutdownVibrationEffectFilePath(context)); + mInjector.getDefaultShutdownVibrationEffectFilePath(context), + vibrator); - if (parsedEffect == null || !vibrator.areVibrationFeaturesSupported(parsedEffect)) { + if (parsedEffect == null) { return createDefaultVibrationEffect(); } @@ -869,11 +870,12 @@ public final class ShutdownThread extends Thread { return parsedEffect; } - private static VibrationEffect parseVibrationEffectFromFile(String filePath) { + private static VibrationEffect parseVibrationEffectFromFile( + String filePath, Vibrator vibrator) { if (!TextUtils.isEmpty(filePath)) { try { - return VibrationXmlParser.parse(new FileReader(filePath)); - } catch (IOException e) { + return VibrationXmlParser.parseDocument(new FileReader(filePath)).resolve(vibrator); + } catch (Exception e) { Log.e(TAG, "Error parsing default shutdown vibration effect.", e); } } diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java b/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java index 8be3b2de4adf..3fb845f064e3 100644 --- a/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java +++ b/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java @@ -19,6 +19,8 @@ package com.android.server.vibrator; import android.annotation.Nullable; import android.content.res.Resources; import android.os.VibrationEffect; +import android.os.Vibrator; +import android.os.vibrator.persistence.ParsedVibration; import android.os.vibrator.persistence.VibrationXmlParser; import android.text.TextUtils; import android.util.Slog; @@ -105,10 +107,10 @@ final class HapticFeedbackCustomization { * @hide */ @Nullable - static SparseArray<VibrationEffect> loadVibrations(Resources res) + static SparseArray<VibrationEffect> loadVibrations(Resources res, Vibrator vibrator) throws CustomizationParserException, IOException { try { - return loadVibrationsInternal(res); + return loadVibrationsInternal(res, vibrator); } catch (VibrationXmlParser.VibrationXmlParserException | XmlParserException | XmlPullParserException e) { @@ -118,12 +120,13 @@ final class HapticFeedbackCustomization { } @Nullable - private static SparseArray<VibrationEffect> loadVibrationsInternal(Resources res) throws - CustomizationParserException, - IOException, - VibrationXmlParser.VibrationXmlParserException, - XmlParserException, - XmlPullParserException { + private static SparseArray<VibrationEffect> loadVibrationsInternal( + Resources res, Vibrator vibrator) throws + CustomizationParserException, + IOException, + VibrationXmlParser.VibrationXmlParserException, + XmlParserException, + XmlPullParserException { String customizationFile = res.getString( com.android.internal.R.string.config_hapticFeedbackCustomizationFile); @@ -164,16 +167,23 @@ final class HapticFeedbackCustomization { // Move the parser one step into the `<constant>` tag. XmlValidator.checkParserCondition( XmlReader.readNextTagWithin(parser, customizationDepth), - "Unsupported empty customization tag"); + "Unsupported empty customization tag for effect " + effectId); - VibrationEffect effect = VibrationXmlParser.parseTag( + ParsedVibration parsedVibration = VibrationXmlParser.parseElement( parser, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS); - if (effect.getDuration() == Long.MAX_VALUE) { - throw new CustomizationParserException(String.format( - "Vibration for effect ID %d is repeating, which is not allowed as a" - + " haptic feedback: %s", effectId, effect)); + if (parsedVibration == null) { + throw new CustomizationParserException( + "Unable to parse vibration element for effect " + effectId); + } + VibrationEffect effect = parsedVibration.resolve(vibrator); + if (effect != null) { + if (effect.getDuration() == Long.MAX_VALUE) { + throw new CustomizationParserException(String.format( + "Vibration for effect ID %d is repeating, which is not allowed as a" + + " haptic feedback: %s", effectId, effect)); + } + mapping.put(effectId, effect); } - mapping.put(effectId, effect); XmlReader.readEndTag(parser, TAG_CONSTANT, customizationDepth); } diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java index 19dd0b26c50f..7c9954391d8c 100644 --- a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java +++ b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java @@ -58,7 +58,7 @@ public final class HapticFeedbackVibrationProvider { /** @hide */ public HapticFeedbackVibrationProvider(Resources res, Vibrator vibrator) { - this(res, vibrator, loadHapticCustomizations(res)); + this(res, vibrator, loadHapticCustomizations(res, vibrator)); } /** @hide */ @@ -288,9 +288,10 @@ public final class HapticFeedbackVibrationProvider { } @Nullable - private static SparseArray<VibrationEffect> loadHapticCustomizations(Resources res) { + private static SparseArray<VibrationEffect> loadHapticCustomizations( + Resources res, Vibrator vibrator) { try { - return HapticFeedbackCustomization.loadVibrations(res); + return HapticFeedbackCustomization.loadVibrations(res, vibrator); } catch (IOException | HapticFeedbackCustomization.CustomizationParserException e) { Slog.e(TAG, "Unable to load haptic customizations.", e); return null; diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 2fef09299494..e296c7b764e5 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -54,6 +54,8 @@ import android.os.VibrationEffect; import android.os.VibratorInfo; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.VibrationEffectSegment; +import android.os.vibrator.VibratorInfoFactory; +import android.os.vibrator.persistence.ParsedVibration; import android.os.vibrator.persistence.VibrationXmlParser; import android.text.TextUtils; import android.util.IndentingPrintWriter; @@ -158,6 +160,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private final InputDeviceDelegate mInputDeviceDelegate; private final DeviceAdapter mDeviceAdapter; + @GuardedBy("mLock") + @Nullable private VibratorInfo mCombinedVibratorInfo; + private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -1826,6 +1831,36 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + private VibratorInfo getCombinedVibratorInfo() { + synchronized (mLock) { + // Used a cached resolving vibrator if one exists. + if (mCombinedVibratorInfo != null) { + return mCombinedVibratorInfo; + } + + // Return an empty resolving vibrator if the service has no vibrator. + if (mVibratorIds.length == 0) { + return mCombinedVibratorInfo = VibratorInfo.EMPTY_VIBRATOR_INFO; + } + + // Combine the vibrator infos of all the service's vibrator to create a single resolving + // vibrator that is based on the combined info. + VibratorInfo[] infos = new VibratorInfo[mVibratorIds.length]; + for (int i = 0; i < mVibratorIds.length; i++) { + VibratorInfo info = getVibratorInfo(mVibratorIds[i]); + // If any one of the service's vibrator does not have a valid vibrator info, stop + // trying to create and cache a combined resolving vibrator. Combine the infos only + // when infos for all vibrators are available. + if (info == null) { + return null; + } + infos[i] = info; + } + + return mCombinedVibratorInfo = VibratorInfoFactory.create(/* id= */ -1, infos); + } + } + /** Implementation of {@link IExternalVibratorService} to be triggered on external control. */ @VisibleForTesting final class ExternalVibratorService extends IExternalVibratorService.Stub { @@ -2308,10 +2343,21 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private CombinedVibration parseXml(String xml) { try { - VibrationEffect effect = VibrationXmlParser.parse(new StringReader(xml)); - if (effect == null) { + ParsedVibration parsedVibration = + VibrationXmlParser.parseDocument(new StringReader(xml)); + if (parsedVibration == null) { throw new IllegalArgumentException("Error parsing vibration XML " + xml); } + VibratorInfo combinedVibratorInfo = getCombinedVibratorInfo(); + if (combinedVibratorInfo == null) { + throw new IllegalStateException( + "No combined vibrator info to parse vibration XML " + xml); + } + VibrationEffect effect = parsedVibration.resolve(combinedVibratorInfo); + if (effect == null) { + throw new IllegalArgumentException( + "Parsed vibration cannot be resolved for vibration XML " + xml); + } return CombinedVibration.createParallel(effect); } catch (IOException e) { throw new RuntimeException("Error parsing vibration XML " + xml, e); @@ -2335,6 +2381,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { pw.println(" sequential [options] (-v <vibrator-id> <effect>...)..."); pw.println(" Vibrates different effects on each vibrator in sequence."); pw.println(" xml [options] <xml>"); + pw.println(" Vibrates using combined vibration described in given XML string"); + pw.println(" on all vibrators in sync. The XML could be:"); + pw.println(" XML containing a single effect, or"); + pw.println(" A vibration select XML containing multiple effects."); pw.println(" Vibrates using combined vibration described in given XML string."); pw.println(" XML containing a single effect it runs on all vibrators in sync."); pw.println(" cancel"); diff --git a/services/tests/powerservicetests/src/com/android/server/power/ShutdownThreadTest.java b/services/tests/powerservicetests/src/com/android/server/power/ShutdownThreadTest.java index 6041e916ffc0..2d1b545a9e6d 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/ShutdownThreadTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/ShutdownThreadTest.java @@ -30,6 +30,7 @@ import android.content.Context; import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; +import android.os.VibratorInfo; import android.util.AtomicFile; import androidx.test.InstrumentationRegistry; @@ -84,6 +85,7 @@ public class ShutdownThreadTest { @Mock private Context mContextMock; @Mock private Vibrator mVibratorMock; + @Mock private VibratorInfo mVibratorInfoMock; private String mDefaultShutdownVibrationFilePath; private long mLastSleepDurationMs; @@ -94,8 +96,9 @@ public class ShutdownThreadTest { public void setUp() { MockitoAnnotations.initMocks(this); when(mVibratorMock.hasVibrator()).thenReturn(true); + when(mVibratorMock.getInfo()).thenReturn(mVibratorInfoMock); - when(mVibratorMock.areVibrationFeaturesSupported(any())).thenReturn(true); + when(mVibratorInfoMock.areVibrationFeaturesSupported(any())).thenReturn(true); mShutdownThread = new ShutdownThread(new TestInjector()); } @@ -130,7 +133,7 @@ public class ShutdownThreadTest { @Test public void testVibratorUnsupportedShutdownVibrationEffect() throws Exception { setShutdownVibrationFileContent(WAVEFORM_VIB_10MS_SERIALIZATION); - when(mVibratorMock.areVibrationFeaturesSupported(any())).thenReturn(false); + when(mVibratorInfoMock.areVibrationFeaturesSupported(any())).thenReturn(false); mShutdownThread.playShutdownVibration(mContextMock); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java index a81898df9235..10b49c67e8bb 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java @@ -25,10 +25,13 @@ import static com.android.server.vibrator.HapticFeedbackCustomization.Customizat import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import android.content.res.Resources; import android.os.VibrationEffect; +import android.os.Vibrator; +import android.os.VibratorInfo; import android.util.AtomicFile; import android.util.SparseArray; @@ -36,6 +39,7 @@ import androidx.test.InstrumentationRegistry; import com.android.internal.R; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; @@ -60,7 +64,23 @@ public class HapticFeedbackCustomizationTest { private static final VibrationEffect PREDEFINED_VIBRATION = VibrationEffect.createPredefined(EFFECT_CLICK); + private static final String WAVEFORM_VIBRATION_XML = "<vibration>" + + "<waveform-effect>" + + "<waveform-entry durationMs=\"123\" amplitude=\"254\"/>" + + "</waveform-effect>" + + "</vibration>"; + private static final VibrationEffect WAVEFORM_VIBARTION = + VibrationEffect.createWaveform(new long[] {123}, new int[] {254}, -1); + @Mock private Resources mResourcesMock; + @Mock private Vibrator mVibratorMock; + @Mock private VibratorInfo mVibratorInfoMock; + + @Before + public void setUp() { + when(mVibratorMock.getInfo()).thenReturn(mVibratorInfoMock); + when(mVibratorInfoMock.areVibrationFeaturesSupported(any())).thenReturn(true); + } @Test public void testParseCustomizations_noCustomization_success() throws Exception { @@ -70,7 +90,7 @@ public class HapticFeedbackCustomizationTest { } @Test - public void testParseCustomizations_oneCustomization_success() throws Exception { + public void testParseCustomizations_oneVibrationCustomization_success() throws Exception { String xml = "<haptic-feedback-constants>" + "<constant id=\"10\">" + COMPOSITION_VIBRATION_XML @@ -83,22 +103,115 @@ public class HapticFeedbackCustomizationTest { } @Test + public void testParseCustomizations_oneVibrationSelectCustomization_success() throws Exception { + String xml = "<haptic-feedback-constants>" + + "<constant id=\"10\">" + + "<vibration-select>" + + COMPOSITION_VIBRATION_XML + + "</vibration-select>" + + "</constant>" + + "</haptic-feedback-constants>"; + SparseArray<VibrationEffect> expectedMapping = new SparseArray<>(); + expectedMapping.put(10, COMPOSITION_VIBRATION); + + assertParseCustomizationsSucceeds(xml, expectedMapping); + } + + @Test public void testParseCustomizations_multipleCustomizations_success() throws Exception { String xml = "<haptic-feedback-constants>" + "<constant id=\"1\">" + COMPOSITION_VIBRATION_XML + "</constant>" + "<constant id=\"12\">" + + "<vibration-select>" + PREDEFINED_VIBRATION_XML + + WAVEFORM_VIBRATION_XML + + "</vibration-select>" + "</constant>" + "<constant id=\"150\">" + PREDEFINED_VIBRATION_XML + "</constant>" + + "<constant id=\"10\">" + + "<vibration-select>" + + WAVEFORM_VIBRATION_XML + + COMPOSITION_VIBRATION_XML + + "</vibration-select>" + + "</constant>" + "</haptic-feedback-constants>"; SparseArray<VibrationEffect> expectedMapping = new SparseArray<>(); expectedMapping.put(1, COMPOSITION_VIBRATION); expectedMapping.put(12, PREDEFINED_VIBRATION); expectedMapping.put(150, PREDEFINED_VIBRATION); + expectedMapping.put(10, WAVEFORM_VIBARTION); + + assertParseCustomizationsSucceeds(xml, expectedMapping); + } + + @Test + public void testParseCustomizations_multipleCustomizations_noSupportedVibration_success() + throws Exception { + makeUnsupported(COMPOSITION_VIBRATION, PREDEFINED_VIBRATION, WAVEFORM_VIBARTION); + String xml = "<haptic-feedback-constants>" + + "<constant id=\"1\">" + + COMPOSITION_VIBRATION_XML + + "</constant>" + + "<constant id=\"12\">" + + "<vibration-select>" + + PREDEFINED_VIBRATION_XML + + WAVEFORM_VIBRATION_XML + + "</vibration-select>" + + "</constant>" + + "<constant id=\"150\">" + + PREDEFINED_VIBRATION_XML + + "</constant>" + + "<constant id=\"10\">" + + "<vibration-select>" + + WAVEFORM_VIBRATION_XML + + COMPOSITION_VIBRATION_XML + + "</vibration-select>" + + "</constant>" + + "</haptic-feedback-constants>"; + + assertParseCustomizationsSucceeds(xml, new SparseArray<>()); + } + + @Test + public void testParseCustomizations_multipleCustomizations_someUnsupportedVibration_success() + throws Exception { + makeSupported(PREDEFINED_VIBRATION, WAVEFORM_VIBARTION); + makeUnsupported(COMPOSITION_VIBRATION); + String xml = "<haptic-feedback-constants>" + + "<constant id=\"1\">" // No supported customization. + + COMPOSITION_VIBRATION_XML + + "</constant>" + + "<constant id=\"12\">" // PREDEFINED_VIBRATION is the first/only supported. + + "<vibration-select>" + + PREDEFINED_VIBRATION_XML + + COMPOSITION_VIBRATION_XML + + "</vibration-select>" + + "</constant>" + + "<constant id=\"14\">" // WAVEFORM_VIBARTION is the first/only supported. + + "<vibration-select>" + + COMPOSITION_VIBRATION_XML + + WAVEFORM_VIBRATION_XML + + "</vibration-select>" + + "</constant>" + + "<constant id=\"150\">" // PREDEFINED_VIBRATION is the first/only supported. + + PREDEFINED_VIBRATION_XML + + "</constant>" + + "<constant id=\"10\">" // PREDEFINED_VIBRATION is the first supported. + + "<vibration-select>" + + PREDEFINED_VIBRATION_XML + + WAVEFORM_VIBRATION_XML + + "</vibration-select>" + + "</constant>" + + "</haptic-feedback-constants>"; + SparseArray<VibrationEffect> expectedMapping = new SparseArray<>(); + expectedMapping.put(12, PREDEFINED_VIBRATION); + expectedMapping.put(14, WAVEFORM_VIBARTION); + expectedMapping.put(150, PREDEFINED_VIBRATION); + expectedMapping.put(10, PREDEFINED_VIBRATION); assertParseCustomizationsSucceeds(xml, expectedMapping); } @@ -107,15 +220,18 @@ public class HapticFeedbackCustomizationTest { public void testParseCustomizations_noCustomizationFile_returnsNull() throws Exception { setCustomizationFilePath(""); - assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock)).isNull(); + assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorMock)) + .isNull(); setCustomizationFilePath(null); - assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock)).isNull(); + assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorMock)) + .isNull(); setCustomizationFilePath("non_existent_file.xml"); - assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock)).isNull(); + assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorMock)) + .isNull(); } @Test @@ -216,6 +332,22 @@ public class HapticFeedbackCustomizationTest { + "<vibration><predefined-effect name=\"bad-effect-name\"/></vibration>" + "</constant>" + "</haptic-feedback-constants>"); + + assertParseCustomizationsFails( + "<haptic-feedback-constants>" + + "<constant id=\"10\">" + + "<vibration-select>" + + "<vibration><predefined-effect name=\"bad-effect-name\"/></vibration>" + + "</constant>" + + "</haptic-feedback-constants>"); + + assertParseCustomizationsFails( + "<haptic-feedback-constants>" + + "<constant id=\"10\">" + + "<vibration><predefined-effect name=\"bad-effect-name\"/></vibration>" + + "</vibration-select>" + + "</constant>" + + "</haptic-feedback-constants>"); } @Test @@ -255,20 +387,21 @@ public class HapticFeedbackCustomizationTest { String xml, SparseArray<VibrationEffect> expectedCustomizations) throws Exception { setupCustomizationFile(xml); assertThat(expectedCustomizations.contentEquals( - HapticFeedbackCustomization.loadVibrations(mResourcesMock))).isTrue(); + HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorMock))) + .isTrue(); } private void assertParseCustomizationsFails(String xml) throws Exception { setupCustomizationFile(xml); assertThrows("Expected haptic feedback customization to fail for " + xml, CustomizationParserException.class, - () -> HapticFeedbackCustomization.loadVibrations(mResourcesMock)); + () -> HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorMock)); } private void assertParseCustomizationsFails() throws Exception { assertThrows("Expected haptic feedback customization to fail", CustomizationParserException.class, - () -> HapticFeedbackCustomization.loadVibrations(mResourcesMock)); + () -> HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorMock)); } private void setupCustomizationFile(String xml) throws Exception { @@ -281,6 +414,18 @@ public class HapticFeedbackCustomizationTest { .thenReturn(path); } + private void makeSupported(VibrationEffect... effects) { + for (VibrationEffect effect : effects) { + when(mVibratorInfoMock.areVibrationFeaturesSupported(effect)).thenReturn(true); + } + } + + private void makeUnsupported(VibrationEffect... effects) { + for (VibrationEffect effect : effects) { + when(mVibratorInfoMock.areVibrationFeaturesSupported(effect)).thenReturn(false); + } + } + private static File createFile(String contents) throws Exception { File file = new File(InstrumentationRegistry.getContext().getCacheDir(), "test.xml"); file.createNewFile(); |