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 
17 package com.android.server.notification;
18 
19 import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
20 import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
21 
22 import android.annotation.Nullable;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.content.res.TypedArray;
26 import android.media.AudioAttributes;
27 import android.os.Process;
28 import android.os.VibrationAttributes;
29 import android.os.VibrationEffect;
30 import android.os.Vibrator;
31 import android.util.Slog;
32 
33 import com.android.internal.R;
34 import com.android.server.pm.PackageManagerService;
35 
36 import java.time.Duration;
37 import java.util.Arrays;
38 
39 /**
40  * NotificationManagerService helper for functionality related to the vibrator.
41  */
42 public final class VibratorHelper {
43     private static final String TAG = "NotificationVibratorHelper";
44 
45     private static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250};
46     private static final int VIBRATE_PATTERN_MAXLEN = 8 * 2 + 1; // up to eight bumps
47 
48     private final Vibrator mVibrator;
49     private final long[] mDefaultPattern;
50     private final long[] mFallbackPattern;
51     @Nullable private final float[] mDefaultPwlePattern;
52     @Nullable private final float[] mFallbackPwlePattern;
53 
VibratorHelper(Context context)54     public VibratorHelper(Context context) {
55         mVibrator = context.getSystemService(Vibrator.class);
56         mDefaultPattern = getLongArray(context.getResources(),
57                 com.android.internal.R.array.config_defaultNotificationVibePattern,
58                 VIBRATE_PATTERN_MAXLEN,
59                 DEFAULT_VIBRATE_PATTERN);
60         mFallbackPattern = getLongArray(context.getResources(),
61                 R.array.config_notificationFallbackVibePattern,
62                 VIBRATE_PATTERN_MAXLEN,
63                 DEFAULT_VIBRATE_PATTERN);
64         mDefaultPwlePattern = getFloatArray(context.getResources(),
65                 com.android.internal.R.array.config_defaultNotificationVibeWaveform);
66         mFallbackPwlePattern = getFloatArray(context.getResources(),
67                 com.android.internal.R.array.config_notificationFallbackVibeWaveform);
68     }
69 
70     /**
71      * Safely create a {@link VibrationEffect} from given vibration {@code pattern}.
72      *
73      * <p>This method returns {@code null} if the pattern is also {@code null} or invalid.
74      *
75      * @param pattern The off/on vibration pattern, where each item is a duration in milliseconds.
76      * @param insistent {@code true} if the vibration should loop until it is cancelled.
77      */
78     @Nullable
createWaveformVibration(@ullable long[] pattern, boolean insistent)79     public static VibrationEffect createWaveformVibration(@Nullable long[] pattern,
80             boolean insistent) {
81         try {
82             if (pattern != null) {
83                 return VibrationEffect.createWaveform(pattern, /* repeat= */ insistent ? 0 : -1);
84             }
85         } catch (IllegalArgumentException e) {
86             Slog.e(TAG, "Error creating vibration waveform with pattern: "
87                     + Arrays.toString(pattern));
88         }
89         return null;
90     }
91 
92     /**
93      * Safely create a {@link VibrationEffect} from given waveform description.
94      *
95      * <p>The waveform is described by a sequence of values for target amplitude, frequency and
96      * duration, that are forwarded to {@link VibrationEffect.WaveformBuilder#addTransition}.
97      *
98      * <p>This method returns {@code null} if the pattern is also {@code null} or invalid.
99      *
100      * @param values The list of values describing the waveform as a sequence of target amplitude,
101      *               frequency and duration.
102      * @param insistent {@code true} if the vibration should loop until it is cancelled.
103      */
104     @Nullable
createPwleWaveformVibration(@ullable float[] values, boolean insistent)105     public static VibrationEffect createPwleWaveformVibration(@Nullable float[] values,
106             boolean insistent) {
107         try {
108             if (values == null) {
109                 return null;
110             }
111 
112             int length = values.length;
113             // The waveform is described by triples (amplitude, frequency, duration)
114             if ((length == 0) || (length % 3 != 0)) {
115                 return null;
116             }
117 
118             VibrationEffect.WaveformBuilder waveformBuilder = VibrationEffect.startWaveform();
119             for (int i = 0; i < length; i += 3) {
120                 waveformBuilder.addTransition(Duration.ofMillis((int) values[i + 2]),
121                         targetAmplitude(values[i]), targetFrequency(values[i + 1]));
122             }
123 
124             VibrationEffect effect = waveformBuilder.build();
125             if (insistent) {
126                 return VibrationEffect.startComposition()
127                         .repeatEffectIndefinitely(effect)
128                         .compose();
129             }
130             return effect;
131         } catch (IllegalArgumentException e) {
132             Slog.e(TAG, "Error creating vibration PWLE waveform with pattern: "
133                     + Arrays.toString(values));
134         }
135         return null;
136     }
137 
138     /**
139      * Vibrate the device with given {@code effect}.
140      *
141      * <p>We need to vibrate as "android" so we can breakthrough DND.
142      */
vibrate(VibrationEffect effect, AudioAttributes attrs, String reason)143     public void vibrate(VibrationEffect effect, AudioAttributes attrs, String reason) {
144         mVibrator.vibrate(Process.SYSTEM_UID, PackageManagerService.PLATFORM_PACKAGE_NAME,
145                 effect, reason, new VibrationAttributes.Builder(attrs).build());
146     }
147 
148     /** Stop all notification vibrations (ringtone, alarm, notification usages). */
cancelVibration()149     public void cancelVibration() {
150         int usageFilter =
151                 VibrationAttributes.USAGE_CLASS_ALARM | ~VibrationAttributes.USAGE_CLASS_MASK;
152         mVibrator.cancel(usageFilter);
153     }
154 
155     /**
156      * Creates a vibration to be used as fallback when the device is in vibrate mode.
157      *
158      * @param insistent {@code true} if the vibration should loop until it is cancelled.
159      */
createFallbackVibration(boolean insistent)160     public VibrationEffect createFallbackVibration(boolean insistent) {
161         if (mVibrator.hasFrequencyControl()) {
162             VibrationEffect effect = createPwleWaveformVibration(mFallbackPwlePattern, insistent);
163             if (effect != null) {
164                 return effect;
165             }
166         }
167         return createWaveformVibration(mFallbackPattern, insistent);
168     }
169 
170     /**
171      * Creates a vibration to be used by notifications without a custom pattern.
172      *
173      * @param insistent {@code true} if the vibration should loop until it is cancelled.
174      */
createDefaultVibration(boolean insistent)175     public VibrationEffect createDefaultVibration(boolean insistent) {
176         if (mVibrator.hasFrequencyControl()) {
177             VibrationEffect effect = createPwleWaveformVibration(mDefaultPwlePattern, insistent);
178             if (effect != null) {
179                 return effect;
180             }
181         }
182         return createWaveformVibration(mDefaultPattern, insistent);
183     }
184 
185     @Nullable
getFloatArray(Resources resources, int resId)186     private static float[] getFloatArray(Resources resources, int resId) {
187         TypedArray array = resources.obtainTypedArray(resId);
188         try {
189             float[] values = new float[array.length()];
190             for (int i = 0; i < values.length; i++) {
191                 values[i] = array.getFloat(i, Float.NaN);
192                 if (Float.isNaN(values[i])) {
193                     return null;
194                 }
195             }
196             return values;
197         } finally {
198             array.recycle();
199         }
200     }
201 
getLongArray(Resources resources, int resId, int maxLength, long[] def)202     private static long[] getLongArray(Resources resources, int resId, int maxLength, long[] def) {
203         int[] ar = resources.getIntArray(resId);
204         if (ar == null) {
205             return def;
206         }
207         final int len = ar.length > maxLength ? maxLength : ar.length;
208         long[] out = new long[len];
209         for (int i = 0; i < len; i++) {
210             out[i] = ar[i];
211         }
212         return out;
213     }
214 }
215