1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.vibrator.persistence; 18 19 import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_AMPLITUDE; 20 import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_DURATION_MS; 21 import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_FREQUENCY_HZ; 22 import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_INITIAL_FREQUENCY_HZ; 23 import static com.android.internal.vibrator.persistence.XmlConstants.NAMESPACE; 24 import static com.android.internal.vibrator.persistence.XmlConstants.TAG_CONTROL_POINT; 25 import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_ENVELOPE_EFFECT; 26 27 import android.annotation.NonNull; 28 import android.os.VibrationEffect; 29 30 import com.android.modules.utils.TypedXmlPullParser; 31 import com.android.modules.utils.TypedXmlSerializer; 32 33 import java.io.IOException; 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.List; 37 import java.util.Locale; 38 39 /** 40 * Serialized representation of a waveform envelope effect created via 41 * {@link VibrationEffect.WaveformEnvelopeBuilder}. 42 * 43 * @hide 44 */ 45 final class SerializedWaveformEnvelopeEffect implements SerializedComposedEffect.SerializedSegment { 46 47 private final WaveformControlPoint[] mControlPoints; 48 private final float mInitialFrequency; 49 SerializedWaveformEnvelopeEffect(WaveformControlPoint[] controlPoints, float initialFrequency)50 SerializedWaveformEnvelopeEffect(WaveformControlPoint[] controlPoints, float initialFrequency) { 51 mControlPoints = controlPoints; 52 mInitialFrequency = initialFrequency; 53 } 54 55 @Override write(@onNull TypedXmlSerializer serializer)56 public void write(@NonNull TypedXmlSerializer serializer) throws IOException { 57 serializer.startTag(NAMESPACE, TAG_WAVEFORM_ENVELOPE_EFFECT); 58 59 if (!Float.isNaN(mInitialFrequency)) { 60 serializer.attributeFloat(NAMESPACE, ATTRIBUTE_INITIAL_FREQUENCY_HZ, mInitialFrequency); 61 } 62 63 for (WaveformControlPoint point : mControlPoints) { 64 serializer.startTag(NAMESPACE, TAG_CONTROL_POINT); 65 serializer.attributeFloat(NAMESPACE, ATTRIBUTE_AMPLITUDE, point.mAmplitude); 66 serializer.attributeFloat(NAMESPACE, ATTRIBUTE_FREQUENCY_HZ, point.mFrequency); 67 serializer.attributeLong(NAMESPACE, ATTRIBUTE_DURATION_MS, point.mDurationMs); 68 serializer.endTag(NAMESPACE, TAG_CONTROL_POINT); 69 } 70 71 serializer.endTag(NAMESPACE, TAG_WAVEFORM_ENVELOPE_EFFECT); 72 } 73 74 @Override deserializeIntoComposition(@onNull VibrationEffect.Composition composition)75 public void deserializeIntoComposition(@NonNull VibrationEffect.Composition composition) { 76 VibrationEffect.WaveformEnvelopeBuilder builder = 77 new VibrationEffect.WaveformEnvelopeBuilder(); 78 79 if (!Float.isNaN(mInitialFrequency)) { 80 builder.setInitialFrequencyHz(mInitialFrequency); 81 } 82 83 for (WaveformControlPoint point : mControlPoints) { 84 builder.addControlPoint(point.mAmplitude, point.mFrequency, point.mDurationMs); 85 } 86 composition.addEffect(builder.build()); 87 } 88 89 @Override toString()90 public String toString() { 91 return "SerializedWaveformEnvelopeEffect{" 92 + "InitialFrequency=" + (Float.isNaN(mInitialFrequency) ? "" : mInitialFrequency) 93 + ", controlPoints=" + Arrays.toString(mControlPoints) 94 + '}'; 95 } 96 97 static final class Builder { 98 private final List<WaveformControlPoint> mControlPoints; 99 private float mInitialFrequencyHz = Float.NaN; 100 Builder()101 Builder() { 102 mControlPoints = new ArrayList<>(); 103 } 104 setInitialFrequencyHz(float frequencyHz)105 void setInitialFrequencyHz(float frequencyHz) { 106 mInitialFrequencyHz = frequencyHz; 107 } 108 addControlPoint(float amplitude, float frequencyHz, long durationMs)109 void addControlPoint(float amplitude, float frequencyHz, long durationMs) { 110 mControlPoints.add(new WaveformControlPoint(amplitude, frequencyHz, durationMs)); 111 } 112 build()113 SerializedWaveformEnvelopeEffect build() { 114 return new SerializedWaveformEnvelopeEffect( 115 mControlPoints.toArray(new WaveformControlPoint[0]), mInitialFrequencyHz); 116 } 117 } 118 119 /** Parser implementation for {@link SerializedWaveformEnvelopeEffect}. */ 120 static final class Parser { 121 122 @NonNull parseNext(@onNull TypedXmlPullParser parser, @XmlConstants.Flags int flags)123 static SerializedWaveformEnvelopeEffect parseNext(@NonNull TypedXmlPullParser parser, 124 @XmlConstants.Flags int flags) throws XmlParserException, IOException { 125 XmlValidator.checkStartTag(parser, TAG_WAVEFORM_ENVELOPE_EFFECT); 126 XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_INITIAL_FREQUENCY_HZ); 127 128 Builder builder = new Builder(); 129 builder.setInitialFrequencyHz( 130 XmlReader.readAttributePositiveFloat(parser, ATTRIBUTE_INITIAL_FREQUENCY_HZ, 131 Float.NaN)); 132 133 int outerDepth = parser.getDepth(); 134 135 while (XmlReader.readNextTagWithin(parser, outerDepth)) { 136 parseControlPoint(parser, builder); 137 // Consume tag 138 XmlReader.readEndTag(parser); 139 } 140 141 // Check schema assertions about <waveform-envelope-effect> 142 XmlValidator.checkParserCondition(!builder.mControlPoints.isEmpty(), 143 "Expected tag %s to have at least one control point", 144 TAG_WAVEFORM_ENVELOPE_EFFECT); 145 146 return builder.build(); 147 } 148 parseControlPoint(TypedXmlPullParser parser, Builder builder)149 private static void parseControlPoint(TypedXmlPullParser parser, Builder builder) 150 throws XmlParserException { 151 XmlValidator.checkStartTag(parser, TAG_CONTROL_POINT); 152 XmlValidator.checkTagHasNoUnexpectedAttributes( 153 parser, ATTRIBUTE_DURATION_MS, ATTRIBUTE_AMPLITUDE, 154 ATTRIBUTE_FREQUENCY_HZ); 155 float amplitude = XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_AMPLITUDE, 0, 156 1); 157 float frequencyHz = XmlReader.readAttributePositiveFloat(parser, 158 ATTRIBUTE_FREQUENCY_HZ); 159 long durationMs = XmlReader.readAttributePositiveLong(parser, ATTRIBUTE_DURATION_MS); 160 161 builder.addControlPoint(amplitude, frequencyHz, durationMs); 162 } 163 } 164 165 private static final class WaveformControlPoint { 166 private final float mAmplitude; 167 private final float mFrequency; 168 private final long mDurationMs; 169 WaveformControlPoint(float amplitude, float frequency, long durationMs)170 WaveformControlPoint(float amplitude, float frequency, long durationMs) { 171 mAmplitude = amplitude; 172 mFrequency = frequency; 173 mDurationMs = durationMs; 174 } 175 176 @Override toString()177 public String toString() { 178 return String.format(Locale.ROOT, "(%.2f, %.2f, %dms)", mAmplitude, mFrequency, 179 mDurationMs); 180 } 181 } 182 } 183