1 /* 2 * Copyright (C) 2021 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 package com.android.launcher3.util; 17 18 import static android.os.VibrationEffect.Composition.PRIMITIVE_LOW_TICK; 19 import static android.os.VibrationEffect.createPredefined; 20 import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED; 21 22 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 23 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; 24 25 import android.annotation.SuppressLint; 26 import android.content.Context; 27 import android.media.AudioAttributes; 28 import android.net.Uri; 29 import android.os.VibrationEffect; 30 import android.os.Vibrator; 31 import android.provider.Settings; 32 33 import androidx.annotation.VisibleForTesting; 34 35 import com.android.launcher3.dagger.ApplicationContext; 36 import com.android.launcher3.dagger.LauncherAppSingleton; 37 import com.android.launcher3.dagger.LauncherBaseAppComponent; 38 39 import javax.inject.Inject; 40 41 /** 42 * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary. 43 */ 44 @LauncherAppSingleton 45 public class VibratorWrapper { 46 47 public static final DaggerSingletonObject<VibratorWrapper> INSTANCE = 48 new DaggerSingletonObject<>(LauncherBaseAppComponent::getVibratorWrapper); 49 50 public static final AudioAttributes VIBRATION_ATTRS = new AudioAttributes.Builder() 51 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) 52 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 53 .build(); 54 55 public static final VibrationEffect EFFECT_CLICK = 56 createPredefined(VibrationEffect.EFFECT_CLICK); 57 @VisibleForTesting 58 static final Uri HAPTIC_FEEDBACK_URI = Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED); 59 60 @VisibleForTesting static final float LOW_TICK_SCALE = 0.9f; 61 62 /** 63 * Haptic when entering overview. 64 */ 65 public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK; 66 67 private final Vibrator mVibrator; 68 private final boolean mHasVibrator; 69 70 @VisibleForTesting 71 final SettingsCache.OnChangeListener mHapticChangeListener = 72 isEnabled -> mIsHapticFeedbackEnabled = isEnabled; 73 74 private boolean mIsHapticFeedbackEnabled; 75 76 @Inject VibratorWrapper(@pplicationContext Context context, SettingsCache settingsCache, DaggerSingletonTracker tracker)77 public VibratorWrapper(@ApplicationContext Context context, SettingsCache settingsCache, 78 DaggerSingletonTracker tracker) { 79 80 mVibrator = context.getSystemService(Vibrator.class); 81 mHasVibrator = mVibrator.hasVibrator(); 82 if (mHasVibrator) { 83 MAIN_EXECUTOR.execute( 84 () -> settingsCache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener)); 85 mIsHapticFeedbackEnabled = settingsCache.getValue(HAPTIC_FEEDBACK_URI, 0); 86 tracker.addCloseable( 87 () -> settingsCache.unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener)); 88 } else { 89 mIsHapticFeedbackEnabled = false; 90 } 91 } 92 93 /** 94 * This should be used to cancel a haptic in case where the haptic shouldn't be vibrating. For 95 * example, when no animation is happening but a vibrator happens to be vibrating still. 96 */ cancelVibrate()97 public void cancelVibrate() { 98 UI_HELPER_EXECUTOR.execute(mVibrator::cancel); 99 } 100 101 /** Vibrates with the given effect if haptic feedback is available and enabled. */ vibrate(VibrationEffect vibrationEffect)102 public void vibrate(VibrationEffect vibrationEffect) { 103 if (mHasVibrator && mIsHapticFeedbackEnabled) { 104 UI_HELPER_EXECUTOR.execute(() -> mVibrator.vibrate(vibrationEffect, VIBRATION_ATTRS)); 105 } 106 } 107 108 /** 109 * Vibrates with a single primitive, if supported, or use a fallback effect instead. This only 110 * vibrates if haptic feedback is available and enabled. 111 */ 112 @SuppressLint("NewApi") vibrate(int primitiveId, float primitiveScale, VibrationEffect fallbackEffect)113 public void vibrate(int primitiveId, float primitiveScale, VibrationEffect fallbackEffect) { 114 if (mHasVibrator && mIsHapticFeedbackEnabled) { 115 UI_HELPER_EXECUTOR.execute(() -> { 116 if (primitiveId >= 0 && mVibrator.areAllPrimitivesSupported(primitiveId)) { 117 mVibrator.vibrate(VibrationEffect.startComposition() 118 .addPrimitive(primitiveId, primitiveScale) 119 .compose(), VIBRATION_ATTRS); 120 } else { 121 mVibrator.vibrate(fallbackEffect, VIBRATION_ATTRS); 122 } 123 }); 124 } 125 } 126 127 /** Indicates that Taskbar has been invoked. */ vibrateForTaskbarUnstash()128 public void vibrateForTaskbarUnstash() { 129 if (mVibrator.areAllPrimitivesSupported(PRIMITIVE_LOW_TICK)) { 130 VibrationEffect primitiveLowTickEffect = VibrationEffect 131 .startComposition() 132 .addPrimitive(PRIMITIVE_LOW_TICK, LOW_TICK_SCALE) 133 .compose(); 134 135 vibrate(primitiveLowTickEffect); 136 } 137 } 138 } 139