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.UI_HELPER_EXECUTOR; 23 24 import android.annotation.SuppressLint; 25 import android.content.Context; 26 import android.media.AudioAttributes; 27 import android.net.Uri; 28 import android.os.SystemClock; 29 import android.os.VibrationEffect; 30 import android.os.Vibrator; 31 import android.provider.Settings; 32 33 import androidx.annotation.Nullable; 34 35 import com.android.launcher3.Utilities; 36 37 /** 38 * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary. 39 */ 40 public class VibratorWrapper implements SafeCloseable { 41 42 public static final MainThreadInitializedObject<VibratorWrapper> INSTANCE = 43 new MainThreadInitializedObject<>(VibratorWrapper::new); 44 45 public static final AudioAttributes VIBRATION_ATTRS = new AudioAttributes.Builder() 46 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) 47 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 48 .build(); 49 50 public static final VibrationEffect EFFECT_CLICK = 51 createPredefined(VibrationEffect.EFFECT_CLICK); 52 private static final Uri HAPTIC_FEEDBACK_URI = 53 Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED); 54 55 private static final float LOW_TICK_SCALE = 0.9f; 56 private static final float DRAG_TEXTURE_SCALE = 0.03f; 57 private static final float DRAG_COMMIT_SCALE = 0.5f; 58 private static final float DRAG_BUMP_SCALE = 0.4f; 59 private static final int DRAG_TEXTURE_EFFECT_SIZE = 200; 60 61 @Nullable 62 private final VibrationEffect mDragEffect; 63 @Nullable 64 private final VibrationEffect mCommitEffect; 65 @Nullable 66 private final VibrationEffect mBumpEffect; 67 68 private long mLastDragTime; 69 private final int mThresholdUntilNextDragCallMillis; 70 71 /** 72 * Haptic when entering overview. 73 */ 74 public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK; 75 76 private final Context mContext; 77 private final Vibrator mVibrator; 78 private final boolean mHasVibrator; 79 private final SettingsCache.OnChangeListener mHapticChangeListener = 80 isEnabled -> mIsHapticFeedbackEnabled = isEnabled; 81 82 private boolean mIsHapticFeedbackEnabled; 83 VibratorWrapper(Context context)84 private VibratorWrapper(Context context) { 85 mContext = context; 86 mVibrator = context.getSystemService(Vibrator.class); 87 mHasVibrator = mVibrator.hasVibrator(); 88 if (mHasVibrator) { 89 SettingsCache cache = SettingsCache.INSTANCE.get(mContext); 90 cache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener); 91 mIsHapticFeedbackEnabled = cache.getValue(HAPTIC_FEEDBACK_URI, 0); 92 } else { 93 mIsHapticFeedbackEnabled = false; 94 } 95 96 if (Utilities.ATLEAST_S && mVibrator.areAllPrimitivesSupported( 97 PRIMITIVE_LOW_TICK)) { 98 99 // Drag texture, Commit, and Bump should only be used for premium phones. 100 // Before using these haptics make sure check if the device can use it 101 VibrationEffect.Composition dragEffect = VibrationEffect.startComposition(); 102 for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) { 103 dragEffect.addPrimitive( 104 PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE); 105 } 106 mDragEffect = dragEffect.compose(); 107 mCommitEffect = VibrationEffect.startComposition().addPrimitive( 108 VibrationEffect.Composition.PRIMITIVE_TICK, DRAG_COMMIT_SCALE).compose(); 109 mBumpEffect = VibrationEffect.startComposition().addPrimitive( 110 PRIMITIVE_LOW_TICK, DRAG_BUMP_SCALE).compose(); 111 int primitiveDuration = mVibrator.getPrimitiveDurations( 112 PRIMITIVE_LOW_TICK)[0]; 113 114 mThresholdUntilNextDragCallMillis = 115 DRAG_TEXTURE_EFFECT_SIZE * primitiveDuration + 100; 116 } else { 117 mDragEffect = null; 118 mCommitEffect = null; 119 mBumpEffect = null; 120 mThresholdUntilNextDragCallMillis = 0; 121 } 122 } 123 124 @Override close()125 public void close() { 126 if (mHasVibrator) { 127 SettingsCache.INSTANCE.get(mContext) 128 .unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener); 129 } 130 } 131 132 /** 133 * This is called when the user swipes to/from all apps. This is meant to be used in between 134 * long animation progresses so that it gives a dragging texture effect. For a better 135 * experience, this should be used in combination with vibrateForDragCommit(). 136 */ vibrateForDragTexture()137 public void vibrateForDragTexture() { 138 if (mDragEffect == null) { 139 return; 140 } 141 long currentTime = SystemClock.elapsedRealtime(); 142 long elapsedTimeSinceDrag = currentTime - mLastDragTime; 143 if (elapsedTimeSinceDrag >= mThresholdUntilNextDragCallMillis) { 144 vibrate(mDragEffect); 145 mLastDragTime = currentTime; 146 } 147 } 148 149 /** 150 * This is used when user reaches the commit threshold when swiping to/from from all apps. 151 */ vibrateForDragCommit()152 public void vibrateForDragCommit() { 153 if (mCommitEffect != null) { 154 vibrate(mCommitEffect); 155 } 156 // resetting dragTexture timestamp to be able to play dragTexture again 157 mLastDragTime = 0; 158 } 159 160 /** 161 * The bump haptic is used to be called at the end of a swipe and only if it the gesture is a 162 * FLING going to/from all apps. Client can just call this method elsewhere just for the 163 * effect. 164 */ vibrateForDragBump()165 public void vibrateForDragBump() { 166 if (mBumpEffect != null) { 167 vibrate(mBumpEffect); 168 } 169 } 170 171 /** 172 * This should be used to cancel a haptic in case where the haptic shouldn't be vibrating. For 173 * example, when no animation is happening but a vibrator happens to be vibrating still. 174 */ cancelVibrate()175 public void cancelVibrate() { 176 UI_HELPER_EXECUTOR.execute(mVibrator::cancel); 177 // reset dragTexture timestamp to be able to play dragTexture again whenever cancelled 178 mLastDragTime = 0; 179 } 180 181 /** Vibrates with the given effect if haptic feedback is available and enabled. */ vibrate(VibrationEffect vibrationEffect)182 public void vibrate(VibrationEffect vibrationEffect) { 183 if (mHasVibrator && mIsHapticFeedbackEnabled) { 184 UI_HELPER_EXECUTOR.execute(() -> mVibrator.vibrate(vibrationEffect, VIBRATION_ATTRS)); 185 } 186 } 187 188 /** 189 * Vibrates with a single primitive, if supported, or use a fallback effect instead. This only 190 * vibrates if haptic feedback is available and enabled. 191 */ 192 @SuppressLint("NewApi") vibrate(int primitiveId, float primitiveScale, VibrationEffect fallbackEffect)193 public void vibrate(int primitiveId, float primitiveScale, VibrationEffect fallbackEffect) { 194 if (mHasVibrator && mIsHapticFeedbackEnabled) { 195 UI_HELPER_EXECUTOR.execute(() -> { 196 if (primitiveId >= 0 && mVibrator.areAllPrimitivesSupported(primitiveId)) { 197 mVibrator.vibrate(VibrationEffect.startComposition() 198 .addPrimitive(primitiveId, primitiveScale) 199 .compose(), VIBRATION_ATTRS); 200 } else { 201 mVibrator.vibrate(fallbackEffect, VIBRATION_ATTRS); 202 } 203 }); 204 } 205 } 206 207 /** Indicates that Taskbar has been invoked. */ vibrateForTaskbarUnstash()208 public void vibrateForTaskbarUnstash() { 209 if (Utilities.ATLEAST_S && mVibrator.areAllPrimitivesSupported(PRIMITIVE_LOW_TICK)) { 210 VibrationEffect primitiveLowTickEffect = VibrationEffect 211 .startComposition() 212 .addPrimitive(PRIMITIVE_LOW_TICK, LOW_TICK_SCALE) 213 .compose(); 214 215 vibrate(primitiveLowTickEffect); 216 } 217 } 218 } 219