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