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