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.quickstep.util 18 19 import android.content.Context 20 import android.os.VibrationEffect 21 import android.os.VibrationEffect.Composition 22 import android.os.Vibrator 23 import com.android.launcher3.dagger.ApplicationContext 24 import com.android.launcher3.dagger.LauncherAppSingleton 25 import com.android.launcher3.util.DaggerSingletonObject 26 import com.android.launcher3.util.VibratorWrapper 27 import com.android.quickstep.DeviceConfigWrapper.Companion.get 28 import com.android.quickstep.dagger.QuickstepBaseAppComponent 29 import javax.inject.Inject 30 import kotlin.math.pow 31 32 /** Manages haptics relating to Contextual Search invocations. */ 33 @LauncherAppSingleton 34 class ContextualSearchHapticManager 35 @Inject 36 internal constructor( 37 @ApplicationContext private val context: Context, 38 private val contextualSearchStateManager: ContextualSearchStateManager, 39 private val vibratorWrapper: VibratorWrapper, 40 ) { 41 42 private var searchEffect = createSearchEffect() 43 createSearchEffectnull44 private fun createSearchEffect() = 45 if ( 46 context 47 .getSystemService(Vibrator::class.java)!! 48 .areAllPrimitivesSupported(Composition.PRIMITIVE_TICK) 49 ) { 50 VibrationEffect.startComposition() 51 .addPrimitive(Composition.PRIMITIVE_TICK, 1f) 52 .compose() 53 } else { 54 // fallback for devices without composition support 55 VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK) 56 } 57 58 /** Indicates that search has been invoked. */ vibrateForSearchnull59 fun vibrateForSearch() { 60 searchEffect.let { vibratorWrapper.vibrate(it) } 61 } 62 63 /** Indicates that search will be invoked if the current gesture is maintained. */ vibrateForSearchHintnull64 fun vibrateForSearchHint() { 65 val navbarConfig = get() 66 // Whether we should play the hint (ramp up) haptic 67 val shouldVibrate: Boolean = 68 if ( 69 context 70 .getSystemService(Vibrator::class.java)!! 71 .areAllPrimitivesSupported(Composition.PRIMITIVE_LOW_TICK) 72 ) { 73 if (contextualSearchStateManager.shouldPlayHapticOverride.isPresent) { 74 contextualSearchStateManager.shouldPlayHapticOverride.get() 75 } else { 76 navbarConfig.enableSearchHapticHint 77 } 78 } else { 79 false 80 } 81 82 if (shouldVibrate) { 83 val startScale = navbarConfig.lpnhHapticHintStartScalePercent / 100f 84 val endScale = navbarConfig.lpnhHapticHintEndScalePercent / 100f 85 val scaleExponent = navbarConfig.lpnhHapticHintScaleExponent 86 val iterations = navbarConfig.lpnhHapticHintIterations 87 val delayMs = navbarConfig.lpnhHapticHintDelay 88 val composition = VibrationEffect.startComposition() 89 for (i in 0 until iterations) { 90 val t = i / (iterations - 1f) 91 val scale = 92 ((1 - t) * startScale + t * endScale) 93 .toDouble() 94 .pow(scaleExponent.toDouble()) 95 .toFloat() 96 if (i == 0) { 97 // Adds a delay before the ramp starts 98 composition.addPrimitive(Composition.PRIMITIVE_LOW_TICK, scale, delayMs) 99 } else { 100 composition.addPrimitive(Composition.PRIMITIVE_LOW_TICK, scale) 101 } 102 } 103 vibratorWrapper.vibrate(composition.compose()) 104 } 105 } 106 107 companion object { 108 @JvmField 109 val INSTANCE = 110 DaggerSingletonObject(QuickstepBaseAppComponent::getContextualSearchHapticManager) 111 } 112 } 113