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