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