• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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