| /* |
| * 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 com.android.server.vibrator; |
| |
| import android.annotation.Nullable; |
| import android.content.res.Resources; |
| import android.os.VibrationAttributes; |
| import android.os.VibrationEffect; |
| import android.os.Vibrator; |
| import android.os.VibratorInfo; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| import android.view.HapticFeedbackConstants; |
| import android.view.flags.FeatureFlags; |
| import android.view.flags.FeatureFlagsImpl; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| |
| /** |
| * Provides the {@link VibrationEffect} and {@link VibrationAttributes} for haptic feedback. |
| * |
| * @hide |
| */ |
| public final class HapticFeedbackVibrationProvider { |
| private static final String TAG = "HapticFeedbackVibrationProvider"; |
| |
| private static final VibrationAttributes TOUCH_VIBRATION_ATTRIBUTES = |
| VibrationAttributes.createForUsage(VibrationAttributes.USAGE_TOUCH); |
| private static final VibrationAttributes PHYSICAL_EMULATION_VIBRATION_ATTRIBUTES = |
| VibrationAttributes.createForUsage(VibrationAttributes.USAGE_PHYSICAL_EMULATION); |
| private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES = |
| VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK); |
| |
| private final VibratorInfo mVibratorInfo; |
| private final boolean mHapticTextHandleEnabled; |
| // Vibrator effect for haptic feedback during boot when safe mode is enabled. |
| private final VibrationEffect mSafeModeEnabledVibrationEffect; |
| // Haptic feedback vibration customizations specific to the device. |
| // If present and valid, a vibration here will be used for an effect. |
| // Otherwise, the system's default vibration will be used. |
| @Nullable private final SparseArray<VibrationEffect> mHapticCustomizations; |
| private final FeatureFlags mViewFeatureFlags; |
| |
| /** @hide */ |
| public HapticFeedbackVibrationProvider(Resources res, Vibrator vibrator) { |
| this(res, vibrator.getInfo()); |
| } |
| |
| /** @hide */ |
| public HapticFeedbackVibrationProvider(Resources res, VibratorInfo vibratorInfo) { |
| this(res, vibratorInfo, loadHapticCustomizations(res, vibratorInfo), |
| new FeatureFlagsImpl()); |
| } |
| |
| /** @hide */ |
| @VisibleForTesting HapticFeedbackVibrationProvider( |
| Resources res, |
| VibratorInfo vibratorInfo, |
| @Nullable SparseArray<VibrationEffect> hapticCustomizations, |
| FeatureFlags viewFeatureFlags) { |
| mVibratorInfo = vibratorInfo; |
| mHapticTextHandleEnabled = res.getBoolean( |
| com.android.internal.R.bool.config_enableHapticTextHandle); |
| |
| if (hapticCustomizations != null && hapticCustomizations.size() == 0) { |
| hapticCustomizations = null; |
| } |
| mHapticCustomizations = hapticCustomizations; |
| mViewFeatureFlags = viewFeatureFlags; |
| |
| mSafeModeEnabledVibrationEffect = |
| effectHasCustomization(HapticFeedbackConstants.SAFE_MODE_ENABLED) |
| ? mHapticCustomizations.get(HapticFeedbackConstants.SAFE_MODE_ENABLED) |
| : VibrationSettings.createEffectFromResource( |
| res, |
| com.android.internal.R.array.config_safeModeEnabledVibePattern); |
| } |
| |
| /** |
| * Provides the {@link VibrationEffect} for a given haptic feedback effect ID (provided in |
| * {@link HapticFeedbackConstants}). |
| * |
| * @param effectId the haptic feedback effect ID whose respective vibration we want to get. |
| * @return a {@link VibrationEffect} for the given haptic feedback effect ID, or {@code null} if |
| * the provided effect ID is not supported. |
| */ |
| @Nullable public VibrationEffect getVibrationForHapticFeedback(int effectId) { |
| switch (effectId) { |
| case HapticFeedbackConstants.CONTEXT_CLICK: |
| case HapticFeedbackConstants.GESTURE_END: |
| case HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE: |
| case HapticFeedbackConstants.SCROLL_TICK: |
| case HapticFeedbackConstants.SEGMENT_TICK: |
| return getVibration(effectId, VibrationEffect.EFFECT_TICK); |
| |
| case HapticFeedbackConstants.TEXT_HANDLE_MOVE: |
| if (!mHapticTextHandleEnabled) { |
| return null; |
| } |
| // fallthrough |
| case HapticFeedbackConstants.CLOCK_TICK: |
| case HapticFeedbackConstants.SEGMENT_FREQUENT_TICK: |
| return getVibration(effectId, VibrationEffect.EFFECT_TEXTURE_TICK); |
| |
| case HapticFeedbackConstants.KEYBOARD_RELEASE: |
| case HapticFeedbackConstants.VIRTUAL_KEY_RELEASE: |
| case HapticFeedbackConstants.ENTRY_BUMP: |
| case HapticFeedbackConstants.DRAG_CROSSING: |
| return getVibration( |
| effectId, |
| VibrationEffect.EFFECT_TICK, |
| /* fallbackForPredefinedEffect= */ false); |
| |
| case HapticFeedbackConstants.KEYBOARD_TAP: // == KEYBOARD_PRESS |
| case HapticFeedbackConstants.VIRTUAL_KEY: |
| case HapticFeedbackConstants.EDGE_RELEASE: |
| case HapticFeedbackConstants.CALENDAR_DATE: |
| case HapticFeedbackConstants.CONFIRM: |
| case HapticFeedbackConstants.GESTURE_START: |
| case HapticFeedbackConstants.SCROLL_ITEM_FOCUS: |
| case HapticFeedbackConstants.SCROLL_LIMIT: |
| return getVibration(effectId, VibrationEffect.EFFECT_CLICK); |
| |
| case HapticFeedbackConstants.LONG_PRESS: |
| case HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON: |
| case HapticFeedbackConstants.DRAG_START: |
| case HapticFeedbackConstants.EDGE_SQUEEZE: |
| return getVibration(effectId, VibrationEffect.EFFECT_HEAVY_CLICK); |
| |
| case HapticFeedbackConstants.REJECT: |
| return getVibration(effectId, VibrationEffect.EFFECT_DOUBLE_CLICK); |
| |
| case HapticFeedbackConstants.SAFE_MODE_ENABLED: |
| return mSafeModeEnabledVibrationEffect; |
| |
| case HapticFeedbackConstants.ASSISTANT_BUTTON: |
| return getAssistantButtonVibration(); |
| |
| case HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE: |
| return getVibration( |
| effectId, |
| VibrationEffect.Composition.PRIMITIVE_TICK, |
| /* primitiveScale= */ 0.4f, |
| VibrationEffect.EFFECT_TEXTURE_TICK); |
| |
| case HapticFeedbackConstants.TOGGLE_ON: |
| return getVibration( |
| effectId, |
| VibrationEffect.Composition.PRIMITIVE_TICK, |
| /* primitiveScale= */ 0.5f, |
| VibrationEffect.EFFECT_TICK); |
| |
| case HapticFeedbackConstants.TOGGLE_OFF: |
| return getVibration( |
| effectId, |
| VibrationEffect.Composition.PRIMITIVE_LOW_TICK, |
| /* primitiveScale= */ 0.2f, |
| VibrationEffect.EFFECT_TEXTURE_TICK); |
| |
| case HapticFeedbackConstants.NO_HAPTICS: |
| default: |
| return null; |
| } |
| } |
| |
| /** |
| * Provides the {@link VibrationAttributes} that should be used for a haptic feedback. |
| * |
| * @param effectId the haptic feedback effect ID whose respective vibration attributes we want |
| * to get. |
| * @param bypassVibrationIntensitySetting {@code true} if the returned attribute should bypass |
| * vibration intensity settings. {@code false} otherwise. |
| * @return the {@link VibrationAttributes} that should be used for the provided haptic feedback. |
| */ |
| public VibrationAttributes getVibrationAttributesForHapticFeedback( |
| int effectId, boolean bypassVibrationIntensitySetting) { |
| VibrationAttributes attrs; |
| switch (effectId) { |
| case HapticFeedbackConstants.EDGE_SQUEEZE: |
| case HapticFeedbackConstants.EDGE_RELEASE: |
| attrs = PHYSICAL_EMULATION_VIBRATION_ATTRIBUTES; |
| break; |
| case HapticFeedbackConstants.ASSISTANT_BUTTON: |
| case HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON: |
| case HapticFeedbackConstants.SCROLL_TICK: |
| case HapticFeedbackConstants.SCROLL_ITEM_FOCUS: |
| case HapticFeedbackConstants.SCROLL_LIMIT: |
| attrs = HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES; |
| break; |
| default: |
| attrs = TOUCH_VIBRATION_ATTRIBUTES; |
| } |
| |
| int flags = 0; |
| if (bypassVibrationIntensitySetting) { |
| flags |= VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF; |
| } |
| if (shouldBypassInterruptionPolicy(effectId, mViewFeatureFlags)) { |
| flags |= VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY; |
| } |
| |
| return flags == 0 ? attrs : new VibrationAttributes.Builder(attrs).setFlags(flags).build(); |
| } |
| |
| /** Dumps relevant state. */ |
| public void dump(String prefix, PrintWriter pw) { |
| pw.print("mHapticTextHandleEnabled="); pw.println(mHapticTextHandleEnabled); |
| } |
| |
| private VibrationEffect getVibration(int effectId, int predefinedVibrationEffectId) { |
| return getVibration( |
| effectId, predefinedVibrationEffectId, /* fallbackForPredefinedEffect= */ true); |
| } |
| |
| /** |
| * Returns the customized vibration for {@code hapticFeedbackId}, or |
| * {@code predefinedVibrationEffectId} if a customization does not exist for the haptic |
| * feedback. |
| * |
| * <p>If a customization does not exist and the default predefined effect is to be returned, |
| * {@code fallbackForPredefinedEffect} will be used to decide whether or not to fallback |
| * to a generic pattern if the predefined effect is not hardware supported. |
| * |
| * @see VibrationEffect#get(int, boolean) |
| */ |
| private VibrationEffect getVibration( |
| int hapticFeedbackId, |
| int predefinedVibrationEffectId, |
| boolean fallbackForPredefinedEffect) { |
| if (effectHasCustomization(hapticFeedbackId)) { |
| return mHapticCustomizations.get(hapticFeedbackId); |
| } |
| return VibrationEffect.get(predefinedVibrationEffectId, fallbackForPredefinedEffect); |
| } |
| |
| /** |
| * Returns the customized vibration for {@code hapticFeedbackId}, or some fallback vibration if |
| * a customization does not exist for the ID. |
| * |
| * <p>The fallback will be a primitive composition formed of {@code primitiveId} and |
| * {@code primitiveScale}, if the primitive is supported. Otherwise, it will be a predefined |
| * vibration of {@code elsePredefinedVibrationEffectId}. |
| */ |
| private VibrationEffect getVibration( |
| int hapticFeedbackId, |
| int primitiveId, |
| float primitiveScale, |
| int elsePredefinedVibrationEffectId) { |
| if (effectHasCustomization(hapticFeedbackId)) { |
| return mHapticCustomizations.get(hapticFeedbackId); |
| } |
| if (mVibratorInfo.isPrimitiveSupported(primitiveId)) { |
| return VibrationEffect.startComposition() |
| .addPrimitive(primitiveId, primitiveScale) |
| .compose(); |
| } else { |
| return VibrationEffect.get(elsePredefinedVibrationEffectId); |
| } |
| } |
| |
| private VibrationEffect getAssistantButtonVibration() { |
| if (effectHasCustomization(HapticFeedbackConstants.ASSISTANT_BUTTON)) { |
| return mHapticCustomizations.get(HapticFeedbackConstants.ASSISTANT_BUTTON); |
| } |
| if (mVibratorInfo.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE) |
| && mVibratorInfo.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_TICK)) { |
| // quiet ramp, short pause, then sharp tick |
| return VibrationEffect.startComposition() |
| .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.25f) |
| .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1f, 50) |
| .compose(); |
| } |
| // fallback for devices without composition support |
| return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK); |
| } |
| |
| private boolean effectHasCustomization(int effectId) { |
| return mHapticCustomizations != null && mHapticCustomizations.contains(effectId); |
| } |
| |
| @Nullable |
| private static SparseArray<VibrationEffect> loadHapticCustomizations( |
| Resources res, VibratorInfo vibratorInfo) { |
| try { |
| return HapticFeedbackCustomization.loadVibrations(res, vibratorInfo); |
| } catch (IOException | HapticFeedbackCustomization.CustomizationParserException e) { |
| Slog.e(TAG, "Unable to load haptic customizations.", e); |
| return null; |
| } |
| } |
| |
| private static boolean shouldBypassInterruptionPolicy( |
| int effectId, FeatureFlags viewFeatureFlags) { |
| switch (effectId) { |
| case HapticFeedbackConstants.SCROLL_TICK: |
| case HapticFeedbackConstants.SCROLL_ITEM_FOCUS: |
| case HapticFeedbackConstants.SCROLL_LIMIT: |
| // The SCROLL_* constants should bypass interruption filter, so that scroll haptics |
| // can play regardless of focus modes like DND. Guard this behavior by the feature |
| // flag controlling the general scroll feedback APIs. |
| return viewFeatureFlags.scrollFeedbackApi(); |
| default: |
| return false; |
| } |
| } |
| } |