/* * Copyright (C) 2021 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.launcher3.util; import static android.os.VibrationEffect.Composition.PRIMITIVE_LOW_TICK; import static android.os.VibrationEffect.createPredefined; import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import android.annotation.SuppressLint; import android.content.Context; import android.media.AudioAttributes; import android.net.Uri; import android.os.VibrationEffect; import android.os.Vibrator; import android.provider.Settings; import androidx.annotation.VisibleForTesting; import com.android.launcher3.dagger.ApplicationContext; import com.android.launcher3.dagger.LauncherAppSingleton; import com.android.launcher3.dagger.LauncherBaseAppComponent; import javax.inject.Inject; /** * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary. */ @LauncherAppSingleton public class VibratorWrapper { public static final DaggerSingletonObject INSTANCE = new DaggerSingletonObject<>(LauncherBaseAppComponent::getVibratorWrapper); public static final AudioAttributes VIBRATION_ATTRS = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build(); public static final VibrationEffect EFFECT_CLICK = createPredefined(VibrationEffect.EFFECT_CLICK); @VisibleForTesting static final Uri HAPTIC_FEEDBACK_URI = Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED); @VisibleForTesting static final float LOW_TICK_SCALE = 0.9f; /** * Haptic when entering overview. */ public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK; private final Vibrator mVibrator; private final boolean mHasVibrator; @VisibleForTesting final SettingsCache.OnChangeListener mHapticChangeListener = isEnabled -> mIsHapticFeedbackEnabled = isEnabled; private boolean mIsHapticFeedbackEnabled; @Inject public VibratorWrapper(@ApplicationContext Context context, SettingsCache settingsCache, DaggerSingletonTracker tracker) { mVibrator = context.getSystemService(Vibrator.class); mHasVibrator = mVibrator.hasVibrator(); if (mHasVibrator) { MAIN_EXECUTOR.execute( () -> settingsCache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener)); mIsHapticFeedbackEnabled = settingsCache.getValue(HAPTIC_FEEDBACK_URI, 0); tracker.addCloseable( () -> settingsCache.unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener)); } else { mIsHapticFeedbackEnabled = false; } } /** * This should be used to cancel a haptic in case where the haptic shouldn't be vibrating. For * example, when no animation is happening but a vibrator happens to be vibrating still. */ public void cancelVibrate() { UI_HELPER_EXECUTOR.execute(mVibrator::cancel); } /** Vibrates with the given effect if haptic feedback is available and enabled. */ public void vibrate(VibrationEffect vibrationEffect) { if (mHasVibrator && mIsHapticFeedbackEnabled) { UI_HELPER_EXECUTOR.execute(() -> mVibrator.vibrate(vibrationEffect, VIBRATION_ATTRS)); } } /** * Vibrates with a single primitive, if supported, or use a fallback effect instead. This only * vibrates if haptic feedback is available and enabled. */ @SuppressLint("NewApi") public void vibrate(int primitiveId, float primitiveScale, VibrationEffect fallbackEffect) { if (mHasVibrator && mIsHapticFeedbackEnabled) { UI_HELPER_EXECUTOR.execute(() -> { if (primitiveId >= 0 && mVibrator.areAllPrimitivesSupported(primitiveId)) { mVibrator.vibrate(VibrationEffect.startComposition() .addPrimitive(primitiveId, primitiveScale) .compose(), VIBRATION_ATTRS); } else { mVibrator.vibrate(fallbackEffect, VIBRATION_ATTRS); } }); } } /** Indicates that Taskbar has been invoked. */ public void vibrateForTaskbarUnstash() { if (mVibrator.areAllPrimitivesSupported(PRIMITIVE_LOW_TICK)) { VibrationEffect primitiveLowTickEffect = VibrationEffect .startComposition() .addPrimitive(PRIMITIVE_LOW_TICK, LOW_TICK_SCALE) .compose(); vibrate(primitiveLowTickEffect); } } }