| /* |
| * Copyright (C) 2020 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; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.hardware.vibrator.Braking; |
| import android.hardware.vibrator.IVibrator; |
| import android.util.IndentingPrintWriter; |
| import android.util.MathUtils; |
| import android.util.Range; |
| import android.util.SparseBooleanArray; |
| import android.util.SparseIntArray; |
| |
| import com.android.internal.util.Preconditions; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Objects; |
| |
| /** |
| * A VibratorInfo describes the capabilities of a {@link Vibrator}. |
| * |
| * <p>This description includes its capabilities, list of supported effects and composition |
| * primitives. |
| * |
| * @hide |
| */ |
| public class VibratorInfo implements Parcelable { |
| private static final String TAG = "VibratorInfo"; |
| |
| /** @hide */ |
| public static final VibratorInfo EMPTY_VIBRATOR_INFO = new VibratorInfo.Builder(-1).build(); |
| |
| private final int mId; |
| private final long mCapabilities; |
| @Nullable |
| private final SparseBooleanArray mSupportedEffects; |
| @Nullable |
| private final SparseBooleanArray mSupportedBraking; |
| private final SparseIntArray mSupportedPrimitives; |
| private final int mPrimitiveDelayMax; |
| private final int mCompositionSizeMax; |
| private final int mPwlePrimitiveDurationMax; |
| private final int mPwleSizeMax; |
| private final float mQFactor; |
| private final FrequencyProfile mFrequencyProfile; |
| |
| VibratorInfo(Parcel in) { |
| mId = in.readInt(); |
| mCapabilities = in.readLong(); |
| mSupportedEffects = in.readSparseBooleanArray(); |
| mSupportedBraking = in.readSparseBooleanArray(); |
| mSupportedPrimitives = in.readSparseIntArray(); |
| mPrimitiveDelayMax = in.readInt(); |
| mCompositionSizeMax = in.readInt(); |
| mPwlePrimitiveDurationMax = in.readInt(); |
| mPwleSizeMax = in.readInt(); |
| mQFactor = in.readFloat(); |
| mFrequencyProfile = FrequencyProfile.CREATOR.createFromParcel(in); |
| } |
| |
| public VibratorInfo(int id, @NonNull VibratorInfo baseVibratorInfo) { |
| this(id, baseVibratorInfo.mCapabilities, baseVibratorInfo.mSupportedEffects, |
| baseVibratorInfo.mSupportedBraking, baseVibratorInfo.mSupportedPrimitives, |
| baseVibratorInfo.mPrimitiveDelayMax, baseVibratorInfo.mCompositionSizeMax, |
| baseVibratorInfo.mPwlePrimitiveDurationMax, baseVibratorInfo.mPwleSizeMax, |
| baseVibratorInfo.mQFactor, baseVibratorInfo.mFrequencyProfile); |
| } |
| |
| /** |
| * Default constructor. |
| * |
| * @param id The vibrator id. |
| * @param capabilities All capability flags of the vibrator, defined in |
| * IVibrator.CAP_*. |
| * @param supportedEffects All supported predefined effects, enum values from |
| * {@link android.hardware.vibrator.Effect}. |
| * @param supportedBraking All supported braking types, enum values from {@link |
| * Braking}. |
| * @param supportedPrimitives All supported primitive effects, key are enum values from |
| * {@link android.hardware.vibrator.CompositePrimitive} and |
| * values are estimated durations in milliseconds. |
| * @param primitiveDelayMax The maximum delay that can be set to a composition primitive |
| * in milliseconds. |
| * @param compositionSizeMax The maximum number of primitives supported by a composition. |
| * @param pwlePrimitiveDurationMax The maximum duration of a PWLE primitive in milliseconds. |
| * @param pwleSizeMax The maximum number of primitives supported by a PWLE |
| * composition. |
| * @param qFactor The vibrator quality factor. |
| * @param frequencyProfile The description of the vibrator supported frequencies and max |
| * amplitude mappings. |
| * @hide |
| */ |
| public VibratorInfo(int id, long capabilities, @Nullable SparseBooleanArray supportedEffects, |
| @Nullable SparseBooleanArray supportedBraking, |
| @NonNull SparseIntArray supportedPrimitives, int primitiveDelayMax, |
| int compositionSizeMax, int pwlePrimitiveDurationMax, int pwleSizeMax, |
| float qFactor, @NonNull FrequencyProfile frequencyProfile) { |
| Preconditions.checkNotNull(supportedPrimitives); |
| Preconditions.checkNotNull(frequencyProfile); |
| mId = id; |
| mCapabilities = capabilities; |
| mSupportedEffects = supportedEffects == null ? null : supportedEffects.clone(); |
| mSupportedBraking = supportedBraking == null ? null : supportedBraking.clone(); |
| mSupportedPrimitives = supportedPrimitives.clone(); |
| mPrimitiveDelayMax = primitiveDelayMax; |
| mCompositionSizeMax = compositionSizeMax; |
| mPwlePrimitiveDurationMax = pwlePrimitiveDurationMax; |
| mPwleSizeMax = pwleSizeMax; |
| mQFactor = qFactor; |
| mFrequencyProfile = frequencyProfile; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| dest.writeInt(mId); |
| dest.writeLong(mCapabilities); |
| dest.writeSparseBooleanArray(mSupportedEffects); |
| dest.writeSparseBooleanArray(mSupportedBraking); |
| dest.writeSparseIntArray(mSupportedPrimitives); |
| dest.writeInt(mPrimitiveDelayMax); |
| dest.writeInt(mCompositionSizeMax); |
| dest.writeInt(mPwlePrimitiveDurationMax); |
| dest.writeInt(mPwleSizeMax); |
| dest.writeFloat(mQFactor); |
| mFrequencyProfile.writeToParcel(dest, flags); |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (!(o instanceof VibratorInfo)) { |
| return false; |
| } |
| VibratorInfo that = (VibratorInfo) o; |
| return mId == that.mId && equalContent(that); |
| } |
| |
| /** |
| * Returns {@code true} only if the properties and capabilities of the provided info, except for |
| * the ID, equals to this info. Returns {@code false} otherwise. |
| * |
| * @hide |
| */ |
| public boolean equalContent(VibratorInfo that) { |
| int supportedPrimitivesCount = mSupportedPrimitives.size(); |
| if (supportedPrimitivesCount != that.mSupportedPrimitives.size()) { |
| return false; |
| } |
| for (int i = 0; i < supportedPrimitivesCount; i++) { |
| if (mSupportedPrimitives.keyAt(i) != that.mSupportedPrimitives.keyAt(i)) { |
| return false; |
| } |
| if (mSupportedPrimitives.valueAt(i) != that.mSupportedPrimitives.valueAt(i)) { |
| return false; |
| } |
| } |
| return mCapabilities == that.mCapabilities |
| && mPrimitiveDelayMax == that.mPrimitiveDelayMax |
| && mCompositionSizeMax == that.mCompositionSizeMax |
| && mPwlePrimitiveDurationMax == that.mPwlePrimitiveDurationMax |
| && mPwleSizeMax == that.mPwleSizeMax |
| && Objects.equals(mSupportedEffects, that.mSupportedEffects) |
| && Objects.equals(mSupportedBraking, that.mSupportedBraking) |
| && Objects.equals(mQFactor, that.mQFactor) |
| && Objects.equals(mFrequencyProfile, that.mFrequencyProfile); |
| } |
| |
| @Override |
| public int hashCode() { |
| int hashCode = Objects.hash(mId, mCapabilities, mSupportedEffects, mSupportedBraking, |
| mQFactor, mFrequencyProfile); |
| for (int i = 0; i < mSupportedPrimitives.size(); i++) { |
| hashCode = 31 * hashCode + mSupportedPrimitives.keyAt(i); |
| hashCode = 31 * hashCode + mSupportedPrimitives.valueAt(i); |
| } |
| return hashCode; |
| } |
| |
| @Override |
| public String toString() { |
| return "VibratorInfo{" |
| + "mId=" + mId |
| + ", mCapabilities=" + Arrays.toString(getCapabilitiesNames()) |
| + ", mCapabilities flags=" + Long.toBinaryString(mCapabilities) |
| + ", mSupportedEffects=" + Arrays.toString(getSupportedEffectsNames()) |
| + ", mSupportedBraking=" + Arrays.toString(getSupportedBrakingNames()) |
| + ", mSupportedPrimitives=" + Arrays.toString(getSupportedPrimitivesNames()) |
| + ", mPrimitiveDelayMax=" + mPrimitiveDelayMax |
| + ", mCompositionSizeMax=" + mCompositionSizeMax |
| + ", mPwlePrimitiveDurationMax=" + mPwlePrimitiveDurationMax |
| + ", mPwleSizeMax=" + mPwleSizeMax |
| + ", mQFactor=" + mQFactor |
| + ", mFrequencyProfile=" + mFrequencyProfile |
| + '}'; |
| } |
| |
| /** @hide */ |
| public void dump(IndentingPrintWriter pw) { |
| pw.println("VibratorInfo:"); |
| pw.increaseIndent(); |
| pw.println("id = " + mId); |
| pw.println("capabilities = " + Arrays.toString(getCapabilitiesNames())); |
| pw.println("capabilitiesFlags = " + Long.toBinaryString(mCapabilities)); |
| pw.println("supportedEffects = " + Arrays.toString(getSupportedEffectsNames())); |
| pw.println("supportedPrimitives = " + Arrays.toString(getSupportedPrimitivesNames())); |
| pw.println("supportedBraking = " + Arrays.toString(getSupportedBrakingNames())); |
| pw.println("primitiveDelayMax = " + mPrimitiveDelayMax); |
| pw.println("compositionSizeMax = " + mCompositionSizeMax); |
| pw.println("pwlePrimitiveDurationMax = " + mPwlePrimitiveDurationMax); |
| pw.println("pwleSizeMax = " + mPwleSizeMax); |
| pw.println("q-factor = " + mQFactor); |
| pw.println("frequencyProfile = " + mFrequencyProfile); |
| pw.decreaseIndent(); |
| } |
| |
| /** Return the id of this vibrator. */ |
| public int getId() { |
| return mId; |
| } |
| |
| /** |
| * Check whether the vibrator has amplitude control. |
| * |
| * @return True if the hardware can control the amplitude of the vibrations, otherwise false. |
| */ |
| public boolean hasAmplitudeControl() { |
| return hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL); |
| } |
| |
| /** |
| * Check whether the vibrator has frequency control. |
| * |
| * @return True if the hardware can control the frequency of the vibrations, otherwise false. |
| */ |
| public boolean hasFrequencyControl() { |
| // We currently can only control frequency of the vibration using the compose PWLE method. |
| return hasCapability( |
| IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS); |
| } |
| |
| /** |
| * Returns a default value to be applied to composed PWLE effects for braking. |
| * |
| * @return a supported braking value, one of android.hardware.vibrator.Braking.* |
| * @hide |
| */ |
| public int getDefaultBraking() { |
| if (mSupportedBraking != null) { |
| int size = mSupportedBraking.size(); |
| for (int i = 0; i < size; i++) { |
| if (mSupportedBraking.keyAt(i) != Braking.NONE) { |
| return mSupportedBraking.keyAt(i); |
| } |
| } |
| } |
| return Braking.NONE; |
| } |
| |
| /** @hide */ |
| @Nullable |
| public SparseBooleanArray getSupportedBraking() { |
| if (mSupportedBraking == null) { |
| return null; |
| } |
| return mSupportedBraking.clone(); |
| } |
| |
| /** @hide */ |
| public boolean isBrakingSupportKnown() { |
| return mSupportedBraking != null; |
| } |
| |
| /** @hide */ |
| public boolean hasBrakingSupport(@Braking int braking) { |
| return (mSupportedBraking != null) && mSupportedBraking.get(braking); |
| } |
| |
| /** @hide */ |
| public boolean isEffectSupportKnown() { |
| return mSupportedEffects != null; |
| } |
| |
| /** |
| * Query whether the vibrator supports the given effect. |
| * |
| * @param effectId Which effects to query for. |
| * @return {@link Vibrator#VIBRATION_EFFECT_SUPPORT_YES} if the effect is supported, |
| * {@link Vibrator#VIBRATION_EFFECT_SUPPORT_NO} if it isn't supported, or |
| * {@link Vibrator#VIBRATION_EFFECT_SUPPORT_UNKNOWN} if the system can't determine whether it's |
| * supported or not. |
| */ |
| @Vibrator.VibrationEffectSupport |
| public int isEffectSupported(@VibrationEffect.EffectType int effectId) { |
| if (mSupportedEffects == null) { |
| return Vibrator.VIBRATION_EFFECT_SUPPORT_UNKNOWN; |
| } |
| return mSupportedEffects.get(effectId) ? Vibrator.VIBRATION_EFFECT_SUPPORT_YES |
| : Vibrator.VIBRATION_EFFECT_SUPPORT_NO; |
| } |
| |
| /** @hide */ |
| @Nullable |
| public SparseBooleanArray getSupportedEffects() { |
| if (mSupportedEffects == null) { |
| return null; |
| } |
| return mSupportedEffects.clone(); |
| } |
| |
| /** |
| * Query whether the vibrator supports the given primitive. |
| * |
| * @param primitiveId Which primitives to query for. |
| * @return Whether the primitive is supported. |
| */ |
| public boolean isPrimitiveSupported( |
| @VibrationEffect.Composition.PrimitiveType int primitiveId) { |
| return hasCapability(IVibrator.CAP_COMPOSE_EFFECTS) |
| && (mSupportedPrimitives.indexOfKey(primitiveId) >= 0); |
| } |
| |
| /** |
| * Query whether or not the vibrator supports all components of a given {@link VibrationEffect} |
| * (i.e. the vibrator can play the given effect as intended). |
| * |
| * <p>See {@link Vibrator#areVibrationFeaturesSupported(VibrationEffect)} for more |
| * information on how the vibrator support is determined. |
| * |
| * @param effect the {@link VibrationEffect} to check if it is supported |
| * @return {@code true} if the vibrator can play the given {@code effect} as intended, |
| * {@code false} otherwise. |
| * |
| * @hide |
| */ |
| public boolean areVibrationFeaturesSupported(@NonNull VibrationEffect effect) { |
| return effect.areVibrationFeaturesSupported(this); |
| } |
| |
| /** |
| * Query the estimated duration of given primitive. |
| * |
| * @param primitiveId Which primitives to query for. |
| * @return The duration in milliseconds estimated for the primitive, or zero if primitive not |
| * supported. |
| */ |
| public int getPrimitiveDuration( |
| @VibrationEffect.Composition.PrimitiveType int primitiveId) { |
| return mSupportedPrimitives.get(primitiveId); |
| } |
| |
| /** @hide */ |
| public SparseIntArray getSupportedPrimitives() { |
| return mSupportedPrimitives.clone(); |
| } |
| |
| /** |
| * Query the maximum delay supported for a primitive in a composed effect. |
| * |
| * @return The max delay in milliseconds, or zero if unlimited. |
| */ |
| public int getPrimitiveDelayMax() { |
| return mPrimitiveDelayMax; |
| } |
| |
| /** |
| * Query the maximum number of primitives supported in a composed effect. |
| * |
| * @return The max number of primitives supported, or zero if unlimited. |
| */ |
| public int getCompositionSizeMax() { |
| return mCompositionSizeMax; |
| } |
| |
| /** |
| * Query the maximum duration supported for a primitive in a PWLE composition. |
| * |
| * @return The max duration in milliseconds, or zero if unlimited. |
| */ |
| public int getPwlePrimitiveDurationMax() { |
| return mPwlePrimitiveDurationMax; |
| } |
| |
| /** |
| * Query the maximum number of primitives supported in a PWLE composition. |
| * |
| * @return The max number of primitives supported, or zero if unlimited. |
| */ |
| public int getPwleSizeMax() { |
| return mPwleSizeMax; |
| } |
| |
| /** |
| * Check against this vibrator capabilities. |
| * |
| * @param capability one of IVibrator.CAP_* |
| * @return true if this vibrator has this capability, false otherwise |
| * @hide |
| */ |
| public boolean hasCapability(long capability) { |
| return (mCapabilities & capability) == capability; |
| } |
| |
| /** |
| * Gets the resonant frequency of the vibrator. |
| * |
| * @return the resonant frequency of the vibrator, or {@link Float#NaN NaN} if it's unknown or |
| * this vibrator is a composite of multiple physical devices. |
| */ |
| public float getResonantFrequencyHz() { |
| return mFrequencyProfile.mResonantFrequencyHz; |
| } |
| |
| /** |
| * Gets the <a href="https://en.wikipedia.org/wiki/Q_factor">Q factor</a> of the vibrator. |
| * |
| * @return the Q factor of the vibrator, or {@link Float#NaN NaN} if it's unknown or |
| * this vibrator is a composite of multiple physical devices. |
| */ |
| public float getQFactor() { |
| return mQFactor; |
| } |
| |
| /** |
| * Gets the profile of supported frequencies, including the measurements of maximum relative |
| * output acceleration for supported vibration frequencies. |
| * |
| * <p>If the devices does not have frequency control then the profile should be empty. |
| */ |
| @NonNull |
| public FrequencyProfile getFrequencyProfile() { |
| return mFrequencyProfile; |
| } |
| |
| /** Returns a single int representing all the capabilities of the vibrator. */ |
| public long getCapabilities() { |
| return mCapabilities; |
| } |
| |
| private String[] getCapabilitiesNames() { |
| List<String> names = new ArrayList<>(); |
| if (hasCapability(IVibrator.CAP_ON_CALLBACK)) { |
| names.add("ON_CALLBACK"); |
| } |
| if (hasCapability(IVibrator.CAP_PERFORM_CALLBACK)) { |
| names.add("PERFORM_CALLBACK"); |
| } |
| if (hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) { |
| names.add("COMPOSE_EFFECTS"); |
| } |
| if (hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS)) { |
| names.add("COMPOSE_PWLE_EFFECTS"); |
| } |
| if (hasCapability(IVibrator.CAP_ALWAYS_ON_CONTROL)) { |
| names.add("ALWAYS_ON_CONTROL"); |
| } |
| if (hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL)) { |
| names.add("AMPLITUDE_CONTROL"); |
| } |
| if (hasCapability(IVibrator.CAP_FREQUENCY_CONTROL)) { |
| names.add("FREQUENCY_CONTROL"); |
| } |
| if (hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) { |
| names.add("EXTERNAL_CONTROL"); |
| } |
| if (hasCapability(IVibrator.CAP_EXTERNAL_AMPLITUDE_CONTROL)) { |
| names.add("EXTERNAL_AMPLITUDE_CONTROL"); |
| } |
| return names.toArray(new String[names.size()]); |
| } |
| |
| private String[] getSupportedEffectsNames() { |
| if (mSupportedEffects == null) { |
| return new String[0]; |
| } |
| String[] names = new String[mSupportedEffects.size()]; |
| for (int i = 0; i < mSupportedEffects.size(); i++) { |
| names[i] = VibrationEffect.effectIdToString(mSupportedEffects.keyAt(i)); |
| } |
| return names; |
| } |
| |
| private String[] getSupportedBrakingNames() { |
| if (mSupportedBraking == null) { |
| return new String[0]; |
| } |
| String[] names = new String[mSupportedBraking.size()]; |
| for (int i = 0; i < mSupportedBraking.size(); i++) { |
| switch (mSupportedBraking.keyAt(i)) { |
| case Braking.NONE: |
| names[i] = "NONE"; |
| break; |
| case Braking.CLAB: |
| names[i] = "CLAB"; |
| break; |
| default: |
| names[i] = Integer.toString(mSupportedBraking.keyAt(i)); |
| } |
| } |
| return names; |
| } |
| |
| private String[] getSupportedPrimitivesNames() { |
| int supportedPrimitivesCount = mSupportedPrimitives.size(); |
| String[] names = new String[supportedPrimitivesCount]; |
| for (int i = 0; i < supportedPrimitivesCount; i++) { |
| names[i] = VibrationEffect.Composition.primitiveToString(mSupportedPrimitives.keyAt(i)) |
| + "(" + mSupportedPrimitives.valueAt(i) + "ms)"; |
| } |
| return names; |
| } |
| |
| /** |
| * Describes the maximum relative output acceleration that can be achieved for each supported |
| * frequency in a specific vibrator. |
| * |
| * <p>This profile is defined by the following parameters: |
| * |
| * <ol> |
| * <li>{@code minFrequencyHz}, {@code resonantFrequencyHz} and {@code frequencyResolutionHz} |
| * provided by the vibrator in hertz. |
| * <li>{@code maxAmplitudes} a list of values in [0,1] provided by the vibrator, where |
| * {@code maxAmplitudes[i]} represents max supported amplitude at frequency |
| * {@code minFrequencyHz + frequencyResolutionHz * i}. |
| * <li>{@code maxFrequencyHz = minFrequencyHz |
| * + frequencyResolutionHz * (maxAmplitudes.length-1)} |
| * </ol> |
| * |
| * @hide |
| */ |
| public static final class FrequencyProfile implements Parcelable { |
| @Nullable |
| private final Range<Float> mFrequencyRangeHz; |
| private final float mMinFrequencyHz; |
| private final float mResonantFrequencyHz; |
| private final float mFrequencyResolutionHz; |
| private final float[] mMaxAmplitudes; |
| |
| FrequencyProfile(Parcel in) { |
| this(in.readFloat(), in.readFloat(), in.readFloat(), in.createFloatArray()); |
| } |
| |
| /** |
| * Default constructor. |
| * |
| * @param resonantFrequencyHz The vibrator resonant frequency, in hertz. |
| * @param minFrequencyHz Minimum supported frequency, in hertz. |
| * @param frequencyResolutionHz The frequency resolution, in hertz, used by the max |
| * amplitude measurements. |
| * @param maxAmplitudes The max amplitude supported by each supported frequency, |
| * starting at minimum frequency with jumps of frequency |
| * resolution. |
| * @hide |
| */ |
| public FrequencyProfile(float resonantFrequencyHz, float minFrequencyHz, |
| float frequencyResolutionHz, float[] maxAmplitudes) { |
| mMinFrequencyHz = minFrequencyHz; |
| mResonantFrequencyHz = resonantFrequencyHz; |
| mFrequencyResolutionHz = frequencyResolutionHz; |
| mMaxAmplitudes = new float[maxAmplitudes == null ? 0 : maxAmplitudes.length]; |
| if (maxAmplitudes != null) { |
| System.arraycopy(maxAmplitudes, 0, mMaxAmplitudes, 0, maxAmplitudes.length); |
| } |
| |
| // If any required field is undefined or has a bad value then this profile is invalid. |
| boolean isValid = !Float.isNaN(resonantFrequencyHz) |
| && (resonantFrequencyHz > 0) |
| && !Float.isNaN(minFrequencyHz) |
| && (minFrequencyHz > 0) |
| && !Float.isNaN(frequencyResolutionHz) |
| && (frequencyResolutionHz > 0) |
| && (mMaxAmplitudes.length > 0); |
| |
| // If any max amplitude is outside the allowed range then this profile is invalid. |
| for (int i = 0; i < mMaxAmplitudes.length; i++) { |
| isValid &= (mMaxAmplitudes[i] >= 0) && (mMaxAmplitudes[i] <= 1); |
| } |
| |
| float maxFrequencyHz = isValid |
| ? minFrequencyHz + frequencyResolutionHz * (mMaxAmplitudes.length - 1) |
| : Float.NaN; |
| |
| // If the constraint min < resonant < max is not met then it is invalid. |
| isValid &= !Float.isNaN(maxFrequencyHz) |
| && (resonantFrequencyHz >= minFrequencyHz) |
| && (resonantFrequencyHz <= maxFrequencyHz) |
| && (minFrequencyHz < maxFrequencyHz); |
| |
| mFrequencyRangeHz = isValid ? Range.create(minFrequencyHz, maxFrequencyHz) : null; |
| } |
| |
| /** Returns true if the supported frequency range is empty. */ |
| public boolean isEmpty() { |
| return mFrequencyRangeHz == null; |
| } |
| |
| /** Returns the supported frequency range, in hertz. */ |
| @Nullable |
| public Range<Float> getFrequencyRangeHz() { |
| return mFrequencyRangeHz; |
| } |
| |
| /** |
| * Returns the maximum relative amplitude the vibrator can reach while playing at the |
| * given frequency. |
| * |
| * @param frequencyHz frequency, in hertz, for query. |
| * @return A value in [0,1] representing the max relative amplitude supported at the given |
| * frequency. This will return 0 if the frequency is outside the supported range, or if the |
| * supported frequency range is empty. |
| */ |
| public float getMaxAmplitude(float frequencyHz) { |
| if (isEmpty() || Float.isNaN(frequencyHz) || !mFrequencyRangeHz.contains(frequencyHz)) { |
| // Unsupported frequency requested, vibrator cannot play at this frequency. |
| return 0; |
| } |
| |
| // Subtract minFrequencyHz to simplify offset calculations. |
| float mappingFreq = frequencyHz - mMinFrequencyHz; |
| |
| // Find the bucket to interpolate within. |
| // Any calculated index should be safe, except exactly equal to max amplitude can be |
| // one step too high, so constrain it to guarantee safety. |
| int startIdx = MathUtils.constrain( |
| /* amount= */ (int) Math.floor(mappingFreq / mFrequencyResolutionHz), |
| /* low= */ 0, /* high= */ mMaxAmplitudes.length - 1); |
| int nextIdx = MathUtils.constrain( |
| /* amount= */ startIdx + 1, |
| /* low= */ 0, /* high= */ mMaxAmplitudes.length - 1); |
| |
| // Linearly interpolate the amplitudes based on the frequency range of the bucket. |
| return MathUtils.constrainedMap( |
| mMaxAmplitudes[startIdx], mMaxAmplitudes[nextIdx], |
| startIdx * mFrequencyResolutionHz, nextIdx * mFrequencyResolutionHz, |
| mappingFreq); |
| } |
| |
| /** Returns the raw list of maximum relative output accelerations from the vibrator. */ |
| @NonNull |
| public float[] getMaxAmplitudes() { |
| return Arrays.copyOf(mMaxAmplitudes, mMaxAmplitudes.length); |
| } |
| |
| /** Returns the raw frequency resolution used for max amplitude measurements, in hertz. */ |
| public float getFrequencyResolutionHz() { |
| return mFrequencyResolutionHz; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| dest.writeFloat(mResonantFrequencyHz); |
| dest.writeFloat(mMinFrequencyHz); |
| dest.writeFloat(mFrequencyResolutionHz); |
| dest.writeFloatArray(mMaxAmplitudes); |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (!(o instanceof FrequencyProfile)) { |
| return false; |
| } |
| FrequencyProfile that = (FrequencyProfile) o; |
| return Float.compare(mMinFrequencyHz, that.mMinFrequencyHz) == 0 |
| && Float.compare(mResonantFrequencyHz, that.mResonantFrequencyHz) == 0 |
| && Float.compare(mFrequencyResolutionHz, that.mFrequencyResolutionHz) == 0 |
| && Arrays.equals(mMaxAmplitudes, that.mMaxAmplitudes); |
| } |
| |
| @Override |
| public int hashCode() { |
| int hashCode = Objects.hash(mMinFrequencyHz, mFrequencyResolutionHz, |
| mFrequencyResolutionHz); |
| hashCode = 31 * hashCode + Arrays.hashCode(mMaxAmplitudes); |
| return hashCode; |
| } |
| |
| @Override |
| public String toString() { |
| return "FrequencyProfile{" |
| + "mFrequencyRange=" + mFrequencyRangeHz |
| + ", mMinFrequency=" + mMinFrequencyHz |
| + ", mResonantFrequency=" + mResonantFrequencyHz |
| + ", mFrequencyResolution=" + mFrequencyResolutionHz |
| + ", mMaxAmplitudes count=" + mMaxAmplitudes.length |
| + '}'; |
| } |
| |
| @NonNull |
| public static final Creator<FrequencyProfile> CREATOR = |
| new Creator<FrequencyProfile>() { |
| @Override |
| public FrequencyProfile createFromParcel(Parcel in) { |
| return new FrequencyProfile(in); |
| } |
| |
| @Override |
| public FrequencyProfile[] newArray(int size) { |
| return new FrequencyProfile[size]; |
| } |
| }; |
| } |
| |
| /** @hide */ |
| public static final class Builder { |
| private final int mId; |
| private long mCapabilities; |
| private SparseBooleanArray mSupportedEffects; |
| private SparseBooleanArray mSupportedBraking; |
| private SparseIntArray mSupportedPrimitives = new SparseIntArray(); |
| private int mPrimitiveDelayMax; |
| private int mCompositionSizeMax; |
| private int mPwlePrimitiveDurationMax; |
| private int mPwleSizeMax; |
| private float mQFactor = Float.NaN; |
| private FrequencyProfile mFrequencyProfile = |
| new FrequencyProfile(Float.NaN, Float.NaN, Float.NaN, null); |
| |
| /** A builder class for a {@link VibratorInfo}. */ |
| public Builder(int id) { |
| mId = id; |
| } |
| |
| /** Configure the vibrator capabilities with a combination of IVibrator.CAP_* values. */ |
| @NonNull |
| public Builder setCapabilities(long capabilities) { |
| mCapabilities = capabilities; |
| return this; |
| } |
| |
| /** Configure the effects supported with {@link android.hardware.vibrator.Effect} values. */ |
| @NonNull |
| public Builder setSupportedEffects(int... supportedEffects) { |
| mSupportedEffects = toSparseBooleanArray(supportedEffects); |
| return this; |
| } |
| |
| /** Configure braking supported with {@link android.hardware.vibrator.Braking} values. */ |
| @NonNull |
| public Builder setSupportedBraking(int... supportedBraking) { |
| mSupportedBraking = toSparseBooleanArray(supportedBraking); |
| return this; |
| } |
| |
| /** Configure maximum duration, in milliseconds, of a PWLE primitive. */ |
| @NonNull |
| public Builder setPwlePrimitiveDurationMax(int pwlePrimitiveDurationMax) { |
| mPwlePrimitiveDurationMax = pwlePrimitiveDurationMax; |
| return this; |
| } |
| |
| /** Configure maximum number of primitives supported in a single PWLE composed effect. */ |
| @NonNull |
| public Builder setPwleSizeMax(int pwleSizeMax) { |
| mPwleSizeMax = pwleSizeMax; |
| return this; |
| } |
| |
| /** Configure the duration of a {@link android.hardware.vibrator.CompositePrimitive}. */ |
| @NonNull |
| public Builder setSupportedPrimitive(int primitiveId, int duration) { |
| mSupportedPrimitives.put(primitiveId, duration); |
| return this; |
| } |
| |
| /** Configure maximum delay, in milliseconds, supported in a composed effect primitive. */ |
| @NonNull |
| public Builder setPrimitiveDelayMax(int primitiveDelayMax) { |
| mPrimitiveDelayMax = primitiveDelayMax; |
| return this; |
| } |
| |
| /** Configure maximum number of primitives supported in a single composed effect. */ |
| @NonNull |
| public Builder setCompositionSizeMax(int compositionSizeMax) { |
| mCompositionSizeMax = compositionSizeMax; |
| return this; |
| } |
| |
| /** Configure the vibrator quality factor. */ |
| @NonNull |
| public Builder setQFactor(float qFactor) { |
| mQFactor = qFactor; |
| return this; |
| } |
| |
| /** Configure the vibrator frequency information like resonant frequency and bandwidth. */ |
| @NonNull |
| public Builder setFrequencyProfile(@NonNull FrequencyProfile frequencyProfile) { |
| mFrequencyProfile = frequencyProfile; |
| return this; |
| } |
| |
| /** Build the configured {@link VibratorInfo}. */ |
| @NonNull |
| public VibratorInfo build() { |
| return new VibratorInfo(mId, mCapabilities, mSupportedEffects, mSupportedBraking, |
| mSupportedPrimitives, mPrimitiveDelayMax, mCompositionSizeMax, |
| mPwlePrimitiveDurationMax, mPwleSizeMax, mQFactor, mFrequencyProfile); |
| } |
| |
| /** |
| * Create a {@link SparseBooleanArray} from given {@code supportedKeys} where each key is |
| * mapped |
| * to {@code true}. |
| */ |
| @Nullable |
| private static SparseBooleanArray toSparseBooleanArray(int[] supportedKeys) { |
| if (supportedKeys == null) { |
| return null; |
| } |
| SparseBooleanArray array = new SparseBooleanArray(); |
| for (int key : supportedKeys) { |
| array.put(key, true); |
| } |
| return array; |
| } |
| } |
| |
| @NonNull |
| public static final Creator<VibratorInfo> CREATOR = |
| new Creator<VibratorInfo>() { |
| @Override |
| public VibratorInfo createFromParcel(Parcel in) { |
| return new VibratorInfo(in); |
| } |
| |
| @Override |
| public VibratorInfo[] newArray(int size) { |
| return new VibratorInfo[size]; |
| } |
| }; |
| } |