diff options
20 files changed, 1188 insertions, 482 deletions
diff --git a/Android.mk b/Android.mk index 03b2533e8bc0..b46aeb891008 100644 --- a/Android.mk +++ b/Android.mk @@ -568,6 +568,7 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ android.hardware.thermal@1.0-java-constants \ android.hardware.health@1.0-java-constants \ android.hardware.usb@1.0-java-constants \ + android.hardware.vibrator@1.0-java-constants \ LOCAL_PROTOC_OPTIMIZE_TYPE := stream LOCAL_PROTOC_FLAGS := \ diff --git a/api/current.txt b/api/current.txt index aab29be21d0f..b18457fe5d1d 100644 --- a/api/current.txt +++ b/api/current.txt @@ -31654,13 +31654,25 @@ package android.os { field public static final int USER_CREATION_FAILED_NO_MORE_USERS = 2; // 0x2 } + public abstract class VibrationEffect implements android.os.Parcelable { + method public static android.os.VibrationEffect createOneShot(long, int); + method public static android.os.VibrationEffect createWaveform(long[], int); + method public static android.os.VibrationEffect createWaveform(long[], int[], int); + method public int describeContents(); + field public static final android.os.Parcelable.Creator<android.os.VibrationEffect> CREATOR; + field public static final int DEFAULT_AMPLITUDE = -1; // 0xffffffff + } + public abstract class Vibrator { method public abstract void cancel(); + method public abstract boolean hasAmplitudeControl(); method public abstract boolean hasVibrator(); - method public void vibrate(long); - method public void vibrate(long, android.media.AudioAttributes); - method public void vibrate(long[], int); - method public void vibrate(long[], int, android.media.AudioAttributes); + method public deprecated void vibrate(long); + method public deprecated void vibrate(long, android.media.AudioAttributes); + method public deprecated void vibrate(long[], int); + method public deprecated void vibrate(long[], int, android.media.AudioAttributes); + method public void vibrate(android.os.VibrationEffect); + method public void vibrate(android.os.VibrationEffect, android.media.AudioAttributes); } public class WorkSource implements android.os.Parcelable { diff --git a/api/system-current.txt b/api/system-current.txt index 5e6717cbece4..87b2e4dc5a86 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -34494,13 +34494,25 @@ package android.os { public static abstract class UserManager.UserRestrictionSource implements java.lang.annotation.Annotation { } + public abstract class VibrationEffect implements android.os.Parcelable { + method public static android.os.VibrationEffect createOneShot(long, int); + method public static android.os.VibrationEffect createWaveform(long[], int); + method public static android.os.VibrationEffect createWaveform(long[], int[], int); + method public int describeContents(); + field public static final android.os.Parcelable.Creator<android.os.VibrationEffect> CREATOR; + field public static final int DEFAULT_AMPLITUDE = -1; // 0xffffffff + } + public abstract class Vibrator { method public abstract void cancel(); + method public abstract boolean hasAmplitudeControl(); method public abstract boolean hasVibrator(); - method public void vibrate(long); - method public void vibrate(long, android.media.AudioAttributes); - method public void vibrate(long[], int); - method public void vibrate(long[], int, android.media.AudioAttributes); + method public deprecated void vibrate(long); + method public deprecated void vibrate(long, android.media.AudioAttributes); + method public deprecated void vibrate(long[], int); + method public deprecated void vibrate(long[], int, android.media.AudioAttributes); + method public void vibrate(android.os.VibrationEffect); + method public void vibrate(android.os.VibrationEffect, android.media.AudioAttributes); } public class WorkSource implements android.os.Parcelable { diff --git a/api/test-current.txt b/api/test-current.txt index f91bbb9a045b..9bac49f9232a 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -31779,13 +31779,25 @@ package android.os { field public static final int USER_CREATION_FAILED_NO_MORE_USERS = 2; // 0x2 } + public abstract class VibrationEffect implements android.os.Parcelable { + method public static android.os.VibrationEffect createOneShot(long, int); + method public static android.os.VibrationEffect createWaveform(long[], int); + method public static android.os.VibrationEffect createWaveform(long[], int[], int); + method public int describeContents(); + field public static final android.os.Parcelable.Creator<android.os.VibrationEffect> CREATOR; + field public static final int DEFAULT_AMPLITUDE = -1; // 0xffffffff + } + public abstract class Vibrator { method public abstract void cancel(); + method public abstract boolean hasAmplitudeControl(); method public abstract boolean hasVibrator(); - method public void vibrate(long); - method public void vibrate(long, android.media.AudioAttributes); - method public void vibrate(long[], int); - method public void vibrate(long[], int, android.media.AudioAttributes); + method public deprecated void vibrate(long); + method public deprecated void vibrate(long, android.media.AudioAttributes); + method public deprecated void vibrate(long[], int); + method public deprecated void vibrate(long[], int, android.media.AudioAttributes); + method public void vibrate(android.os.VibrationEffect); + method public void vibrate(android.os.VibrationEffect, android.media.AudioAttributes); } public class WorkSource implements android.os.Parcelable { diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 6e202b00274f..631b77d4132c 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -33,6 +33,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.Vibrator; +import android.os.VibrationEffect; import android.os.ServiceManager.ServiceNotFoundException; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; @@ -1154,23 +1155,33 @@ public final class InputManager { return true; } - /** - * @hide - */ @Override - public void vibrate(int uid, String opPkg, long milliseconds, AudioAttributes attributes) { - vibrate(new long[] { 0, milliseconds}, -1); + public boolean hasAmplitudeControl() { + return false; } /** * @hide */ @Override - public void vibrate(int uid, String opPkg, long[] pattern, int repeat, - AudioAttributes attributes) { - if (repeat >= pattern.length) { - throw new ArrayIndexOutOfBoundsException(); + public void vibrate(int uid, String opPkg, + VibrationEffect effect, AudioAttributes attributes) { + long[] pattern; + int repeat; + if (effect instanceof VibrationEffect.OneShot) { + VibrationEffect.OneShot oneShot = (VibrationEffect.OneShot) effect; + pattern = new long[] { 0, oneShot.getTiming() }; + repeat = -1; + } else if (effect instanceof VibrationEffect.Waveform) { + VibrationEffect.Waveform waveform = (VibrationEffect.Waveform) effect; + pattern = waveform.getTimings(); + repeat = waveform.getRepeatIndex(); + } else { + // TODO: Add support for prebaked effects + Log.w(TAG, "Pre-baked effects aren't supported on input devices"); + return; } + try { mIm.vibrate(mDeviceId, pattern, repeat, mToken); } catch (RemoteException ex) { diff --git a/core/java/android/os/IVibratorService.aidl b/core/java/android/os/IVibratorService.aidl index 6f2857d46625..e59c3ae16ef7 100644 --- a/core/java/android/os/IVibratorService.aidl +++ b/core/java/android/os/IVibratorService.aidl @@ -16,12 +16,14 @@ package android.os; +import android.os.VibrationEffect; + /** {@hide} */ interface IVibratorService { boolean hasVibrator(); - void vibrate(int uid, String opPkg, long milliseconds, int usageHint, IBinder token); - void vibratePattern(int uid, String opPkg, in long[] pattern, int repeat, int usageHint, IBinder token); + boolean hasAmplitudeControl(); + void vibrate(int uid, String opPkg, in VibrationEffect effect, int usageHint, IBinder token); void cancelVibrate(IBinder token); } diff --git a/core/java/android/os/NullVibrator.java b/core/java/android/os/NullVibrator.java index 19b452f323ce..b8bdc89a2f72 100644 --- a/core/java/android/os/NullVibrator.java +++ b/core/java/android/os/NullVibrator.java @@ -38,22 +38,14 @@ public class NullVibrator extends Vibrator { return false; } - /** - * @hide - */ @Override - public void vibrate(int uid, String opPkg, long milliseconds, AudioAttributes attributes) { + public boolean hasAmplitudeControl() { + return false; } - /** - * @hide - */ @Override - public void vibrate(int uid, String opPkg, long[] pattern, int repeat, - AudioAttributes attributes) { - if (repeat >= pattern.length) { - throw new ArrayIndexOutOfBoundsException(); - } + public void vibrate(int uid, String opPkg, + VibrationEffect effect, AudioAttributes attributes) { } @Override diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java index c4888111d0ef..f776c17e56be 100644 --- a/core/java/android/os/SystemVibrator.java +++ b/core/java/android/os/SystemVibrator.java @@ -32,14 +32,12 @@ public class SystemVibrator extends Vibrator { private final Binder mToken = new Binder(); public SystemVibrator() { - mService = IVibratorService.Stub.asInterface( - ServiceManager.getService("vibrator")); + mService = IVibratorService.Stub.asInterface(ServiceManager.getService("vibrator")); } public SystemVibrator(Context context) { super(context); - mService = IVibratorService.Stub.asInterface( - ServiceManager.getService("vibrator")); + mService = IVibratorService.Stub.asInterface(ServiceManager.getService("vibrator")); } @Override @@ -55,44 +53,30 @@ public class SystemVibrator extends Vibrator { return false; } - /** - * @hide - */ @Override - public void vibrate(int uid, String opPkg, long milliseconds, AudioAttributes attributes) { + public boolean hasAmplitudeControl() { if (mService == null) { - Log.w(TAG, "Failed to vibrate; no vibrator service."); - return; + Log.w(TAG, "Failed to check amplitude control; no vibrator service."); + return false; } try { - mService.vibrate(uid, opPkg, milliseconds, usageForAttributes(attributes), mToken); + return mService.hasAmplitudeControl(); } catch (RemoteException e) { - Log.w(TAG, "Failed to vibrate.", e); } + return false; } - /** - * @hide - */ @Override - public void vibrate(int uid, String opPkg, long[] pattern, int repeat, - AudioAttributes attributes) { + public void vibrate(int uid, String opPkg, + VibrationEffect effect, AudioAttributes attributes) { if (mService == null) { Log.w(TAG, "Failed to vibrate; no vibrator service."); return; } - // catch this here because the server will do nothing. pattern may - // not be null, let that be checked, because the server will drop it - // anyway - if (repeat < pattern.length) { - try { - mService.vibratePattern(uid, opPkg, pattern, repeat, usageForAttributes(attributes), - mToken); - } catch (RemoteException e) { - Log.w(TAG, "Failed to vibrate.", e); - } - } else { - throw new ArrayIndexOutOfBoundsException(); + try { + mService.vibrate(uid, opPkg, effect, usageForAttributes(attributes), mToken); + } catch (RemoteException e) { + Log.w(TAG, "Failed to vibrate.", e); } } diff --git a/core/java/android/os/VibrationEffect.aidl b/core/java/android/os/VibrationEffect.aidl new file mode 100644 index 000000000000..dcc79d798c3d --- /dev/null +++ b/core/java/android/os/VibrationEffect.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +parcelable VibrationEffect; +parcelable VibrationEffect.OneShotVibration; +parcelable VibrationEffect.WaveformVibration; +parcelable VibrationEffect.EffectVibration; diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java new file mode 100644 index 000000000000..eceaa31b9cf8 --- /dev/null +++ b/core/java/android/os/VibrationEffect.java @@ -0,0 +1,444 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.hardware.vibrator.V1_0.Constants.Effect; + +import java.util.Arrays; + +/** + * A VibrationEffect describes a haptic effect to be performed by a {@link Vibrator}. + * + * These effects may be any number of things, from single shot vibrations to complex waveforms. + */ +public abstract class VibrationEffect implements Parcelable { + private static final int PARCEL_TOKEN_ONE_SHOT = 1; + private static final int PARCEL_TOKEN_WAVEFORM = 2; + private static final int PARCEL_TOKEN_EFFECT = 3; + + /** + * The default vibration strength of the device. + */ + public static final int DEFAULT_AMPLITUDE = -1; + + /** + * A click effect. + * + * @see #get(int) + * @hide + */ + public static final int EFFECT_CLICK = Effect.CLICK; + + /** + * A double click effect. + * + * @see #get(int) + * @hide + */ + public static final int EFFECT_DOUBLE_CLICK = Effect.DOUBLE_CLICK; + + /** @hide to prevent subclassing from outside of the framework */ + public VibrationEffect() { } + + /** + * Create a one shot vibration. + * + * One shot vibrations will vibrate constantly for the specified period of time at the + * specified amplitude, and then stop. + * + * @param milliseconds The number of milliseconds to vibrate. This must be a positive number. + * @param amplitude The strength of the vibration. This must be a value between 1 and 255, or + * {@link #DEFAULT_AMPLITUDE}. + * + * @return The desired effect. + */ + public static VibrationEffect createOneShot(long milliseconds, int amplitude) { + VibrationEffect effect = new OneShot(milliseconds, amplitude); + effect.validate(); + return effect; + } + + /** + * Create a waveform vibration. + * + * Waveform vibrations are a potentially repeating series of timing and amplitude pairs. For + * each pair, the value in the amplitude array determines the strength of the vibration and the + * value in the timing array determines how long it vibrates for. An amplitude of 0 implies no + * vibration (i.e. off), and any pairs with a timing value of 0 will be ignored. + * <p> + * The amplitude array of the generated waveform will be the same size as the given + * timing array with alternating values of 0 (i.e. off) and {@link #DEFAULT_AMPLITUDE}, + * starting with 0. Therefore the first timing value will be the period to wait before turning + * the vibrator on, the second value will be how long to vibrate at {@link #DEFAULT_AMPLITUDE} + * strength, etc. + * </p><p> + * To cause the pattern to repeat, pass the index into the timings array at which to start the + * repetition, or -1 to disable repeating. + * </p> + * + * @param timings The pattern of alternating on-off timings, starting with off. Timing values + * of 0 will cause the timing / amplitude pair to be ignored. + * @param repeat The index into the timings array at which to repeat, or -1 if you you don't + * want to repeat. + * + * @return The desired effect. + */ + public static VibrationEffect createWaveform(long[] timings, int repeat) { + int[] amplitudes = new int[timings.length]; + for (int i = 0; i < (timings.length / 2); i++) { + amplitudes[i*2 + 1] = VibrationEffect.DEFAULT_AMPLITUDE; + } + return createWaveform(timings, amplitudes, repeat); + } + + /** + * Create a waveform vibration. + * + * Waveform vibrations are a potentially repeating series of timing and amplitude pairs. For + * each pair, the value in the amplitude array determines the strength of the vibration and the + * value in the timing array determines how long it vibrates for. An amplitude of 0 implies no + * vibration (i.e. off), and any pairs with a timing value of 0 will be ignored. + * </p><p> + * To cause the pattern to repeat, pass the index into the timings array at which to start the + * repetition, or -1 to disable repeating. + * </p> + * + * @param timings The timing values of the timing / amplitude pairs. Timing values of 0 + * will cause the pair to be ignored. + * @param amplitudes The amplitude values of the timing / amplitude pairs. Amplitude values + * must be between 0 and 255, or equal to {@link #DEFAULT_AMPLITUDE}. An + * amplitude value of 0 implies the motor is off. + * @param repeat The index into the timings array at which to repeat, or -1 if you you don't + * want to repeat. + * + * @return The desired effect. + */ + public static VibrationEffect createWaveform(long[] timings, int[] amplitudes, int repeat) { + VibrationEffect effect = new Waveform(timings, amplitudes, repeat); + effect.validate(); + return effect; + } + + /** + * Get a predefined vibration effect. + * + * Predefined effects are a set of common vibration effects that should be identical, regardless + * of the app they come from, in order to provide a cohesive experience for users across + * the entire device. They also may be custom tailored to the device hardware in order to + * provide a better experience than you could otherwise build using the generic building + * blocks. + * + * @param effectId The ID of the effect to perform: + * {@link #EFFECT_CLICK}, {@link #EFFECT_DOUBLE_CLICK}. + * + * @return The desired effect. + * @hide + */ + public static VibrationEffect get(int effectId) { + VibrationEffect effect = new Prebaked(effectId); + effect.validate(); + return effect; + } + + @Override + public int describeContents() { + return 0; + } + + /** @hide */ + public abstract void validate(); + + /** @hide */ + public static class OneShot extends VibrationEffect implements Parcelable { + private long mTiming; + private int mAmplitude; + + public OneShot(Parcel in) { + this(in.readLong(), in.readInt()); + } + + public OneShot(long milliseconds, int amplitude) { + mTiming = milliseconds; + mAmplitude = amplitude; + } + + public long getTiming() { + return mTiming; + } + + public int getAmplitude() { + return mAmplitude; + } + + @Override + public void validate() { + if (mAmplitude < -1 || mAmplitude == 0 || mAmplitude > 255) { + throw new IllegalArgumentException( + "amplitude must either be DEFAULT_AMPLITUDE, " + + "or between 1 and 255 inclusive"); + } + if (mTiming <= 0) { + throw new IllegalArgumentException("timing must be positive"); + } + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof VibrationEffect.OneShot)) { + return false; + } + VibrationEffect.OneShot other = (VibrationEffect.OneShot) o; + return other.mTiming == mTiming && other.mAmplitude == mAmplitude; + } + + @Override + public int hashCode() { + int result = 17; + result = 37 * (int) mTiming; + result = 37 * mAmplitude; + return result; + } + + @Override + public String toString() { + return "OneShot{mTiming=" + mTiming +", mAmplitude=" + mAmplitude + "}"; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(PARCEL_TOKEN_ONE_SHOT); + out.writeLong(mTiming); + out.writeInt(mAmplitude); + } + + public static final Parcelable.Creator<OneShot> CREATOR = + new Parcelable.Creator<OneShot>() { + @Override + public OneShot createFromParcel(Parcel in) { + // Skip the type token + in.readInt(); + return new OneShot(in); + } + @Override + public OneShot[] newArray(int size) { + return new OneShot[size]; + } + }; + } + + /** @hide */ + public static class Waveform extends VibrationEffect implements Parcelable { + private long[] mTimings; + private int[] mAmplitudes; + private int mRepeat; + + public Waveform(Parcel in) { + this(in.createLongArray(), in.createIntArray(), in.readInt()); + } + + public Waveform(long[] timings, int[] amplitudes, int repeat) { + mTimings = new long[timings.length]; + System.arraycopy(timings, 0, mTimings, 0, timings.length); + mAmplitudes = new int[amplitudes.length]; + System.arraycopy(amplitudes, 0, mAmplitudes, 0, amplitudes.length); + mRepeat = repeat; + } + + public long[] getTimings() { + return mTimings; + } + + public int[] getAmplitudes() { + return mAmplitudes; + } + + public int getRepeatIndex() { + return mRepeat; + } + + @Override + public void validate() { + if (mTimings.length != mAmplitudes.length) { + throw new IllegalArgumentException( + "timing and amplitude arrays must be of equal length"); + } + if (!hasNonZeroEntry(mTimings)) { + throw new IllegalArgumentException("at least one timing must be non-zero"); + } + for (long timing : mTimings) { + if (timing < 0) { + throw new IllegalArgumentException("timings must all be >= 0"); + } + } + for (int amplitude : mAmplitudes) { + if (amplitude < -1 || amplitude > 255) { + throw new IllegalArgumentException( + "amplitudes must all be DEFAULT_AMPLITUDE or between 0 and 255"); + } + } + if (mRepeat < -1 || mRepeat >= mTimings.length) { + throw new IllegalArgumentException("repeat index must be >= -1"); + } + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof VibrationEffect.Waveform)) { + return false; + } + VibrationEffect.Waveform other = (VibrationEffect.Waveform) o; + return Arrays.equals(mTimings, other.mTimings) && + Arrays.equals(mAmplitudes, other.mAmplitudes) && + mRepeat == other.mRepeat; + } + + @Override + public int hashCode() { + int result = 17; + result = 37 * Arrays.hashCode(mTimings); + result = 37 * Arrays.hashCode(mAmplitudes); + result = 37 * mRepeat; + return result; + } + + @Override + public String toString() { + return "Waveform{mTimings=" + Arrays.toString(mTimings) + + ", mAmplitudes=" + Arrays.toString(mAmplitudes) + + ", mRepeat=" + mRepeat + + "}"; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(PARCEL_TOKEN_WAVEFORM); + out.writeLongArray(mTimings); + out.writeIntArray(mAmplitudes); + out.writeInt(mRepeat); + } + + private static boolean hasNonZeroEntry(long[] vals) { + for (long val : vals) { + if (val != 0) { + return true; + } + } + return false; + } + + + public static final Parcelable.Creator<Waveform> CREATOR = + new Parcelable.Creator<Waveform>() { + @Override + public Waveform createFromParcel(Parcel in) { + // Skip the type token + in.readInt(); + return new Waveform(in); + } + @Override + public Waveform[] newArray(int size) { + return new Waveform[size]; + } + }; + } + + /** @hide */ + public static class Prebaked extends VibrationEffect implements Parcelable { + private int mEffectId; + + public Prebaked(Parcel in) { + this(in.readInt()); + } + + public Prebaked(int effectId) { + mEffectId = effectId; + } + + public int getId() { + return mEffectId; + } + + @Override + public void validate() { + if (mEffectId != EFFECT_CLICK) { + throw new IllegalArgumentException("Unknown prebaked effect type"); + } + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof VibrationEffect.Prebaked)) { + return false; + } + VibrationEffect.Prebaked other = (VibrationEffect.Prebaked) o; + return mEffectId == other.mEffectId; + } + + @Override + public int hashCode() { + return mEffectId; + } + + @Override + public String toString() { + return "Prebaked{mEffectId=" + mEffectId + "}"; + } + + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(PARCEL_TOKEN_EFFECT); + out.writeInt(mEffectId); + } + + public static final Parcelable.Creator<Prebaked> CREATOR = + new Parcelable.Creator<Prebaked>() { + @Override + public Prebaked createFromParcel(Parcel in) { + // Skip the type token + in.readInt(); + return new Prebaked(in); + } + @Override + public Prebaked[] newArray(int size) { + return new Prebaked[size]; + } + }; + } + + public static final Parcelable.Creator<VibrationEffect> CREATOR = + new Parcelable.Creator<VibrationEffect>() { + @Override + public VibrationEffect createFromParcel(Parcel in) { + int token = in.readInt(); + if (token == PARCEL_TOKEN_ONE_SHOT) { + return new OneShot(in); + } else if (token == PARCEL_TOKEN_WAVEFORM) { + return new Waveform(in); + } else if (token == PARCEL_TOKEN_EFFECT) { + return new Prebaked(in); + } else { + throw new IllegalStateException( + "Unexpected vibration event type token in parcel."); + } + } + @Override + public VibrationEffect[] newArray(int size) { + return new VibrationEffect[size]; + } + }; +} diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java index f9b7666b79f7..b1f64218b7b4 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -55,12 +55,22 @@ public abstract class Vibrator { public abstract boolean hasVibrator(); /** + * Check whether the vibrator has amplitude control. + * + * @return True if the hardware can control the amplitude of the vibrations, otherwise false. + */ + public abstract boolean hasAmplitudeControl(); + + /** * Vibrate constantly for the specified period of time. * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#VIBRATE}. * * @param milliseconds The number of milliseconds to vibrate. + * + * @deprecated Use {@link #vibrate(VibrationEffect)} instead. */ + @Deprecated public void vibrate(long milliseconds) { vibrate(milliseconds, null); } @@ -75,9 +85,14 @@ public abstract class Vibrator { * specify {@link AudioAttributes#USAGE_ALARM} for alarm vibrations or * {@link AudioAttributes#USAGE_NOTIFICATION_RINGTONE} for * vibrations associated with incoming calls. + * + * @deprecated Use {@link #vibrate(VibrationEffect, AudioAttributes)} instead. */ + @Deprecated public void vibrate(long milliseconds, AudioAttributes attributes) { - vibrate(Process.myUid(), mPackageName, milliseconds, attributes); + VibrationEffect effect = + VibrationEffect.createOneShot(milliseconds, VibrationEffect.DEFAULT_AMPLITUDE); + vibrate(effect, attributes); } /** @@ -99,7 +114,10 @@ public abstract class Vibrator { * @param pattern an array of longs of times for which to turn the vibrator on or off. * @param repeat the index into pattern at which to repeat, or -1 if * you don't want to repeat. + * + * @deprecated Use {@link #vibrate(VibrationEffect)} instead. */ + @Deprecated public void vibrate(long[] pattern, int repeat) { vibrate(pattern, repeat, null); } @@ -127,26 +145,34 @@ public abstract class Vibrator { * specify {@link AudioAttributes#USAGE_ALARM} for alarm vibrations or * {@link AudioAttributes#USAGE_NOTIFICATION_RINGTONE} for * vibrations associated with incoming calls. + * + * @deprecated Use {@link #vibrate(VibrationEffect, AudioAttributes)} instead. */ + @Deprecated public void vibrate(long[] pattern, int repeat, AudioAttributes attributes) { - vibrate(Process.myUid(), mPackageName, pattern, repeat, attributes); + // This call needs to continue throwing ArrayIndexOutOfBoundsException for compatibility + // purposes, whereas VibrationEffect throws an IllegalArgumentException. + if (repeat < -1 || repeat >= pattern.length) { + throw new ArrayIndexOutOfBoundsException(); + } + vibrate(VibrationEffect.createWaveform(pattern, repeat), attributes); } - /** - * @hide - * Like {@link #vibrate(long, AudioAttributes)}, but allowing the caller to specify that - * the vibration is owned by someone else. - */ - public abstract void vibrate(int uid, String opPkg, long milliseconds, - AudioAttributes attributes); + public void vibrate(VibrationEffect vibe) { + vibrate(vibe, null); + } + + public void vibrate(VibrationEffect vibe, AudioAttributes attributes) { + vibrate(Process.myUid(), mPackageName, vibe, attributes); + } /** + * Like {@link #vibrate(VibrationEffect, AudioAttributes)}, but allowing the caller to specify + * that the vibration is owned by someone else. * @hide - * Like {@link #vibrate(long[], int, AudioAttributes)}, but allowing the caller to specify that - * the vibration is owned by someone else. */ - public abstract void vibrate(int uid, String opPkg, long[] pattern, int repeat, - AudioAttributes attributes); + public abstract void vibrate(int uid, String opPkg, + VibrationEffect vibe, AudioAttributes attributes); /** * Turn the vibrator off. diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 385f256f5d1c..a35705b0315c 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2498,6 +2498,9 @@ <!-- How long history of previous vibrations should be kept for the dumpsys. --> <integer name="config_previousVibrationsDumpLimit">20</integer> + <!-- The default vibration strength, must be between 1 and 255 inclusive. --> + <integer name="config_defaultVibrationAmplitude">255</integer> + <!-- Number of retries Cell Data should attempt for a given error code before restarting the modem. Error codes not listed will not lead to modem restarts. diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 0d4a407db242..359a9474de85 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1816,6 +1816,7 @@ <java-symbol type="integer" name="config_notificationsBatteryMediumARGB" /> <java-symbol type="integer" name="config_notificationServiceArchiveSize" /> <java-symbol type="integer" name="config_previousVibrationsDumpLimit" /> + <java-symbol type="integer" name="config_defaultVibrationAmplitude" /> <java-symbol type="integer" name="config_radioScanningTimeout" /> <java-symbol type="integer" name="config_screenBrightnessSettingMinimum" /> <java-symbol type="integer" name="config_screenBrightnessSettingMaximum" /> diff --git a/services/core/Android.mk b/services/core/Android.mk index 794ece65b03d..d312902382f9 100644 --- a/services/core/Android.mk +++ b/services/core/Android.mk @@ -28,6 +28,7 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ tzdata_update2 \ android.hidl.base@1.0-java-static \ android.hardware.biometrics.fingerprint@2.1-java-static \ + android.hardware.vibrator@1.0-java-constants \ ifneq ($(INCREMENTAL_BUILDS),) LOCAL_PROGUARD_ENABLED := disabled diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java index 5fe6952fbc66..c4676d12c8bd 100644 --- a/services/core/java/com/android/server/VibratorService.java +++ b/services/core/java/com/android/server/VibratorService.java @@ -22,8 +22,10 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.database.ContentObserver; import android.hardware.input.InputManager; +import android.hardware.vibrator.V1_0.Constants.EffectStrength; import android.media.AudioManager; import android.os.PowerSaveState; import android.os.BatteryStats; @@ -42,6 +44,7 @@ import android.os.ShellCommand; import android.os.SystemClock; import android.os.UserHandle; import android.os.Vibrator; +import android.os.VibrationEffect; import android.os.WorkSource; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; @@ -67,12 +70,14 @@ public class VibratorService extends IVibratorService.Stub private static final boolean DEBUG = false; private static final String SYSTEM_UI_PACKAGE = "com.android.systemui"; - private final LinkedList<Vibration> mVibrations; private final LinkedList<VibrationInfo> mPreviousVibrations; private final int mPreviousVibrationsLimit; - private Vibration mCurrentVibration; + private final boolean mSupportsAmplitudeControl; + private final int mDefaultVibrationAmplitude; + private final VibrationEffect[] mFallbackEffects; private final WorkSource mTmpWorkSource = new WorkSource(); private final Handler mH = new Handler(); + private final Object mLock = new Object(); private final Context mContext; private final PowerManager.WakeLock mWakeLock; @@ -81,14 +86,15 @@ public class VibratorService extends IVibratorService.Stub private PowerManagerInternal mPowerManagerInternal; private InputManager mIm; - volatile VibrateThread mThread; + private volatile VibrateThread mThread; - // mInputDeviceVibrators lock should be acquired after mVibrations lock, if both are + // mInputDeviceVibrators lock should be acquired after mLock, if both are // to be acquired private final ArrayList<Vibrator> mInputDeviceVibrators = new ArrayList<Vibrator>(); private boolean mVibrateInputDevicesSetting; // guarded by mInputDeviceVibrators private boolean mInputDeviceListenerRegistered; // guarded by mInputDeviceVibrators + private Vibration mCurrentVibration; private int mCurVibUid = -1; private boolean mLowPowerMode; private SettingsObserver mSettingObserver; @@ -97,106 +103,87 @@ public class VibratorService extends IVibratorService.Stub native static void vibratorInit(); native static void vibratorOn(long milliseconds); native static void vibratorOff(); + native static boolean vibratorSupportsAmplitudeControl(); + native static void vibratorSetAmplitude(int amplitude); + native static long vibratorPerformEffect(long effect, long strength); private class Vibration implements IBinder.DeathRecipient { private final IBinder mToken; - private final long mTimeout; - private final long mStartTime; - private final long[] mPattern; - private final int mRepeat; - private final int mUsageHint; - private final int mUid; - private final String mOpPkg; - - Vibration(IBinder token, long millis, int usageHint, int uid, String opPkg) { - this(token, millis, null, 0, usageHint, uid, opPkg); - } + private final VibrationEffect mEffect; + private final long mStartTime; + private final int mUsageHint; + private final int mUid; + private final String mOpPkg; - Vibration(IBinder token, long[] pattern, int repeat, int usageHint, int uid, - String opPkg) { - this(token, 0, pattern, repeat, usageHint, uid, opPkg); - } - - private Vibration(IBinder token, long millis, long[] pattern, - int repeat, int usageHint, int uid, String opPkg) { + private Vibration(IBinder token, VibrationEffect effect, + int usageHint, int uid, String opPkg) { mToken = token; - mTimeout = millis; + mEffect = effect; mStartTime = SystemClock.uptimeMillis(); - mPattern = pattern; - mRepeat = repeat; mUsageHint = usageHint; mUid = uid; mOpPkg = opPkg; } public void binderDied() { - synchronized (mVibrations) { - mVibrations.remove(this); + synchronized (mLock) { if (this == mCurrentVibration) { doCancelVibrateLocked(); - startNextVibrationLocked(); } } } public boolean hasLongerTimeout(long millis) { - if (mTimeout == 0) { - // This is a pattern, return false to play the simple - // vibration. - return false; + // If the current effect is a one shot vibration that will end after the given timeout + // for the new one shot vibration, then just let the current vibration finish. All + // other effect types will get pre-empted. + if (mEffect instanceof VibrationEffect.OneShot) { + VibrationEffect.OneShot oneShot = (VibrationEffect.OneShot) mEffect; + return mStartTime + oneShot.getTiming() > SystemClock.uptimeMillis() + millis; } - if ((mStartTime + mTimeout) - < (SystemClock.uptimeMillis() + millis)) { - // If this vibration will end before the time passed in, let - // the new vibration play. - return false; - } - return true; + return false; } public boolean isSystemHapticFeedback() { + boolean repeating = false; + if (mEffect instanceof VibrationEffect.Waveform) { + VibrationEffect.Waveform waveform = (VibrationEffect.Waveform) mEffect; + repeating = (waveform.getRepeatIndex() < 0); + } return (mUid == Process.SYSTEM_UID || mUid == 0 || SYSTEM_UI_PACKAGE.equals(mOpPkg)) - && mRepeat < 0; + && !repeating; } } private static class VibrationInfo { - long timeout; - long startTime; - long[] pattern; - int repeat; - int usageHint; - int uid; - String opPkg; - - public VibrationInfo(long timeout, long startTime, long[] pattern, int repeat, + private final long mStartTime; + private final VibrationEffect mEffect; + private final int mUsageHint; + private final int mUid; + private final String mOpPkg; + + public VibrationInfo(long startTime, VibrationEffect effect, int usageHint, int uid, String opPkg) { - this.timeout = timeout; - this.startTime = startTime; - this.pattern = pattern; - this.repeat = repeat; - this.usageHint = usageHint; - this.uid = uid; - this.opPkg = opPkg; + mStartTime = startTime; + mEffect = effect; + mUsageHint = usageHint; + mUid = uid; + mOpPkg = opPkg; } @Override public String toString() { return new StringBuilder() - .append("timeout: ") - .append(timeout) .append(", startTime: ") - .append(startTime) - .append(", pattern: ") - .append(Arrays.toString(pattern)) - .append(", repeat: ") - .append(repeat) + .append(mStartTime) + .append(", effect: ") + .append(mEffect) .append(", usageHint: ") - .append(usageHint) + .append(mUsageHint) .append(", uid: ") - .append(uid) + .append(mUid) .append(", opPkg: ") - .append(opPkg) + .append(mOpPkg) .toString(); } } @@ -207,25 +194,38 @@ public class VibratorService extends IVibratorService.Stub // restart instead of a fresh boot. vibratorOff(); + mSupportsAmplitudeControl = vibratorSupportsAmplitudeControl(); + mContext = context; - PowerManager pm = (PowerManager)context.getSystemService( - Context.POWER_SERVICE); + PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*vibrator*"); mWakeLock.setReferenceCounted(true); - mAppOpsService = IAppOpsService.Stub.asInterface(ServiceManager.getService(Context.APP_OPS_SERVICE)); + mAppOpsService = + IAppOpsService.Stub.asInterface(ServiceManager.getService(Context.APP_OPS_SERVICE)); mBatteryStatsService = IBatteryStats.Stub.asInterface(ServiceManager.getService( BatteryStats.SERVICE_NAME)); mPreviousVibrationsLimit = mContext.getResources().getInteger( com.android.internal.R.integer.config_previousVibrationsDumpLimit); - mVibrations = new LinkedList<>(); + mDefaultVibrationAmplitude = mContext.getResources().getInteger( + com.android.internal.R.integer.config_defaultVibrationAmplitude); + mPreviousVibrations = new LinkedList<>(); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_SCREEN_OFF); context.registerReceiver(mIntentReceiver, filter); + + long[] clickEffectTimings = getLongIntArray(context.getResources(), + com.android.internal.R.array.config_virtualKeyVibePattern); + VibrationEffect clickEffect = VibrationEffect.createWaveform(clickEffectTimings, -1); + VibrationEffect doubleClickEffect = VibrationEffect.createWaveform( + new long[] {0, 30, 100, 30} /*timings*/, -1); + + mFallbackEffects = new VibrationEffect[] { clickEffect, doubleClickEffect }; + } public void systemReady() { @@ -242,7 +242,7 @@ public class VibratorService extends IVibratorService.Stub @Override public void onLowPowerModeChanged(PowerSaveState result) { - updateInputDeviceVibrators(); + updateVibrators(); } }); @@ -253,11 +253,11 @@ public class VibratorService extends IVibratorService.Stub mContext.registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - updateInputDeviceVibrators(); + updateVibrators(); } }, new IntentFilter(Intent.ACTION_USER_SWITCHED), null, mH); - updateInputDeviceVibrators(); + updateVibrators(); } private final class SettingsObserver extends ContentObserver { @@ -267,7 +267,7 @@ public class VibratorService extends IVibratorService.Stub @Override public void onChange(boolean SelfChange) { - updateInputDeviceVibrators(); + updateVibrators(); } } @@ -276,6 +276,15 @@ public class VibratorService extends IVibratorService.Stub return doVibratorExists(); } + @Override // Binder call + public boolean hasAmplitudeControl() { + synchronized (mInputDeviceVibrators) { + // Input device vibrators don't support amplitude controls yet, but are still used over + // the system vibrator when connected. + return mSupportsAmplitudeControl && mInputDeviceVibrators.isEmpty(); + } + } + private void verifyIncomingUid(int uid) { if (uid == Binder.getCallingUid()) { return; @@ -287,103 +296,96 @@ public class VibratorService extends IVibratorService.Stub Binder.getCallingPid(), Binder.getCallingUid(), null); } - @Override // Binder call - public void vibrate(int uid, String opPkg, long milliseconds, int usageHint, - IBinder token) { - if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.VIBRATE) - != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("Requires VIBRATE permission"); + /** + * Validate the incoming VibrationEffect. + * + * We can't throw exceptions here since we might be called from some system_server component, + * which would bring the whole system down. + * + * @return whether the VibrationEffect is valid + */ + private static boolean verifyVibrationEffect(VibrationEffect effect) { + if (effect == null) { + // Effect must not be null. + Slog.wtf(TAG, "effect must not be null"); + return false; } - verifyIncomingUid(uid); - // We're running in the system server so we cannot crash. Check for a - // timeout of 0 or negative. This will ensure that a vibration has - // either a timeout of > 0 or a non-null pattern. - if (milliseconds <= 0 || (mCurrentVibration != null - && mCurrentVibration.hasLongerTimeout(milliseconds))) { - // Ignore this vibration since the current vibration will play for - // longer than milliseconds. - return; - } - - if (DEBUG) { - Slog.d(TAG, "Vibrating for " + milliseconds + " ms."); - } - - Vibration vib = new Vibration(token, milliseconds, usageHint, uid, opPkg); - - final long ident = Binder.clearCallingIdentity(); try { - synchronized (mVibrations) { - removeVibrationLocked(token); - doCancelVibrateLocked(); - addToPreviousVibrationsLocked(vib); - startVibrationLocked(vib); - } - } finally { - Binder.restoreCallingIdentity(ident); + effect.validate(); + } catch (Exception e) { + Slog.wtf(TAG, "Encountered issue when verifying VibrationEffect.", e); + return false; } + return true; } - private boolean isAll0(long[] pattern) { - int N = pattern.length; - for (int i = 0; i < N; i++) { - if (pattern[i] != 0) { - return false; - } + private static long[] getLongIntArray(Resources r, int resid) { + int[] ar = r.getIntArray(resid); + if (ar == null) { + return null; } - return true; + long[] out = new long[ar.length]; + for (int i = 0; i < ar.length; i++) { + out[i] = ar[i]; + } + return out; } @Override // Binder call - public void vibratePattern(int uid, String packageName, long[] pattern, int repeat, - int usageHint, IBinder token) { + public void vibrate(int uid, String opPkg, VibrationEffect effect, int usageHint, + IBinder token) { if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.VIBRATE) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires VIBRATE permission"); } + if (token == null) { + Slog.e(TAG, "token must not be null"); + return; + } verifyIncomingUid(uid); - // so wakelock calls will succeed - long identity = Binder.clearCallingIdentity(); - try { - if (DEBUG) { - String s = ""; - int N = pattern.length; - for (int i=0; i<N; i++) { - s += " " + pattern[i]; - } - Slog.d(TAG, "Vibrating with pattern:" + s); - } + if (!verifyVibrationEffect(effect)) { + return; + } - // we're running in the server so we can't fail - if (pattern == null || pattern.length == 0 - || isAll0(pattern) - || repeat >= pattern.length || token == null) { + // If our current vibration is longer than the new vibration and is the same amplitude, + // then just let the current one finish. + if (effect instanceof VibrationEffect.OneShot + && mCurrentVibration != null + && mCurrentVibration.mEffect instanceof VibrationEffect.OneShot) { + VibrationEffect.OneShot newOneShot = (VibrationEffect.OneShot) effect; + VibrationEffect.OneShot currentOneShot = + (VibrationEffect.OneShot) mCurrentVibration.mEffect; + if (mCurrentVibration.hasLongerTimeout(newOneShot.getTiming()) + && newOneShot.getAmplitude() == currentOneShot.getAmplitude()) { + if (DEBUG) { + Slog.e(TAG, "Ignoring incoming vibration in favor of current vibration"); + } return; } + } + + Vibration vib = new Vibration(token, effect, usageHint, uid, opPkg); - Vibration vib = new Vibration(token, pattern, repeat, usageHint, uid, packageName); + // Only link against waveforms since they potentially don't have a finish if + // they're repeating. Let other effects just play out until they're done. + if (effect instanceof VibrationEffect.Waveform) { try { token.linkToDeath(vib, 0); } catch (RemoteException e) { return; } + } - synchronized (mVibrations) { - removeVibrationLocked(token); + + long ident = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { doCancelVibrateLocked(); - if (repeat >= 0) { - mVibrations.addFirst(vib); - startNextVibrationLocked(); - } else { - // A negative repeat means that this pattern is not meant - // to repeat. Treat it like a simple vibration. - startVibrationLocked(vib); - } + startVibrationLocked(vib); addToPreviousVibrationsLocked(vib); } - } - finally { - Binder.restoreCallingIdentity(identity); + } finally { + Binder.restoreCallingIdentity(ident); } } @@ -391,8 +393,8 @@ public class VibratorService extends IVibratorService.Stub if (mPreviousVibrations.size() > mPreviousVibrationsLimit) { mPreviousVibrations.removeFirst(); } - mPreviousVibrations.addLast(new VibratorService.VibrationInfo(vib.mTimeout, vib.mStartTime, - vib.mPattern, vib.mRepeat, vib.mUsageHint, vib.mUid, vib.mOpPkg)); + mPreviousVibrations.addLast(new VibrationInfo( + vib.mStartTime, vib.mEffect, vib.mUsageHint, vib.mUid, vib.mOpPkg)); } @Override // Binder call @@ -401,97 +403,97 @@ public class VibratorService extends IVibratorService.Stub android.Manifest.permission.VIBRATE, "cancelVibrate"); - // so wakelock calls will succeed - long identity = Binder.clearCallingIdentity(); - try { - synchronized (mVibrations) { - final Vibration vib = removeVibrationLocked(token); - if (vib == mCurrentVibration) { - if (DEBUG) { - Slog.d(TAG, "Canceling vibration."); - } + synchronized (mLock) { + if (mCurrentVibration != null && mCurrentVibration.mToken == token) { + if (DEBUG) { + Slog.d(TAG, "Canceling vibration."); + } + long ident = Binder.clearCallingIdentity(); + try { doCancelVibrateLocked(); - startNextVibrationLocked(); + } finally { + Binder.restoreCallingIdentity(ident); } } } - finally { - Binder.restoreCallingIdentity(identity); - } } - private final Runnable mVibrationRunnable = new Runnable() { + private final Runnable mVibrationEndRunnable = new Runnable() { @Override public void run() { - synchronized (mVibrations) { - doCancelVibrateLocked(); - startNextVibrationLocked(); - } + onVibrationFinished(); } }; - // Lock held on mVibrations private void doCancelVibrateLocked() { + mH.removeCallbacks(mVibrationEndRunnable); if (mThread != null) { - synchronized (mThread) { - mThread.mDone = true; - mThread.notify(); - } + mThread.cancel(); mThread = null; } doVibratorOff(); - mH.removeCallbacks(mVibrationRunnable); reportFinishVibrationLocked(); } - // Lock held on mVibrations - private void startNextVibrationLocked() { - if (mVibrations.size() <= 0) { - reportFinishVibrationLocked(); - mCurrentVibration = null; - return; + // Callback for whenever the current vibration has finished played out + public void onVibrationFinished() { + if (DEBUG) { + Slog.e(TAG, "Vibration finished, cleaning up"); + } + synchronized (mLock) { + // Make sure the vibration is really done. This also reports that the vibration is + // finished. + doCancelVibrateLocked(); } - startVibrationLocked(mVibrations.getFirst()); } - // Lock held on mVibrations private void startVibrationLocked(final Vibration vib) { - try { - if (mLowPowerMode - && vib.mUsageHint != AudioAttributes.USAGE_NOTIFICATION_RINGTONE) { - return; + if (mLowPowerMode && vib.mUsageHint != AudioAttributes.USAGE_NOTIFICATION_RINGTONE) { + if (DEBUG) { + Slog.e(TAG, "Vibrate ignored, low power mode"); } + return; + } - if (vib.mUsageHint == AudioAttributes.USAGE_NOTIFICATION_RINGTONE && - !shouldVibrateForRingtone()) { - return; + if (vib.mUsageHint == AudioAttributes.USAGE_NOTIFICATION_RINGTONE && + !shouldVibrateForRingtone()) { + if (DEBUG) { + Slog.e(TAG, "Vibrate ignored, not vibrating for ringtones"); } + return; + } - int mode = mAppOpsService.checkAudioOperation(AppOpsManager.OP_VIBRATE, - vib.mUsageHint, vib.mUid, vib.mOpPkg); - if (mode == AppOpsManager.MODE_ALLOWED) { - mode = mAppOpsService.startOperation(AppOpsManager.getToken(mAppOpsService), - AppOpsManager.OP_VIBRATE, vib.mUid, vib.mOpPkg); + final int mode = getAppOpMode(vib); + if (mode != AppOpsManager.MODE_ALLOWED) { + if (mode == AppOpsManager.MODE_ERRORED) { + // We might be getting calls from within system_server, so we don't actually want + // to throw a SecurityException here. + Slog.w(TAG, "Would be an error: vibrate from uid " + vib.mUid); } - if (mode == AppOpsManager.MODE_ALLOWED) { - mCurrentVibration = vib; - } else { - if (mode == AppOpsManager.MODE_ERRORED) { - Slog.w(TAG, "Would be an error: vibrate from uid " + vib.mUid); - } - mH.post(mVibrationRunnable); - return; - } - } catch (RemoteException e) { + return; } - if (vib.mTimeout != 0) { - doVibratorOn(vib.mTimeout, vib.mUid, vib.mUsageHint); - mH.postDelayed(mVibrationRunnable, vib.mTimeout); - } else { + startVibrationInnerLocked(vib); + } + + private void startVibrationInnerLocked(Vibration vib) { + mCurrentVibration = vib; + if (vib.mEffect instanceof VibrationEffect.OneShot) { + VibrationEffect.OneShot oneShot = (VibrationEffect.OneShot) vib.mEffect; + doVibratorOn(oneShot.getTiming(), oneShot.getAmplitude(), vib.mUid, vib.mUsageHint); + mH.postDelayed(mVibrationEndRunnable, oneShot.getTiming()); + } else if (vib.mEffect instanceof VibrationEffect.Waveform) { // mThread better be null here. doCancelVibrate should always be // called before startNextVibrationLocked or startVibrationLocked. - mThread = new VibrateThread(vib); + VibrationEffect.Waveform waveform = (VibrationEffect.Waveform) vib.mEffect; + mThread = new VibrateThread(waveform, vib.mUid, vib.mUsageHint); mThread.start(); + } else if (vib.mEffect instanceof VibrationEffect.Prebaked) { + long timeout = doVibratorPrebakedEffectLocked(vib); + if (timeout > 0) { + mH.postDelayed(mVibrationEndRunnable, timeout); + } + } else { + Slog.e(TAG, "Unknown vibration type, ignoring"); } } @@ -507,104 +509,115 @@ public class VibratorService extends IVibratorService.Stub } } + private int getAppOpMode(Vibration vib) { + int mode; + try { + mode = mAppOpsService.checkAudioOperation(AppOpsManager.OP_VIBRATE, + vib.mUsageHint, vib.mUid, vib.mOpPkg); + if (mode == AppOpsManager.MODE_ALLOWED) { + mode = mAppOpsService.startOperation(AppOpsManager.getToken(mAppOpsService), + AppOpsManager.OP_VIBRATE, vib.mUid, vib.mOpPkg); + } + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get appop mode for vibration!", e); + mode = AppOpsManager.MODE_IGNORED; + } + return mode; + } + private void reportFinishVibrationLocked() { if (mCurrentVibration != null) { try { mAppOpsService.finishOperation(AppOpsManager.getToken(mAppOpsService), AppOpsManager.OP_VIBRATE, mCurrentVibration.mUid, mCurrentVibration.mOpPkg); - } catch (RemoteException e) { - } + } catch (RemoteException e) { } mCurrentVibration = null; } } - // Lock held on mVibrations - private Vibration removeVibrationLocked(IBinder token) { - ListIterator<Vibration> iter = mVibrations.listIterator(0); - while (iter.hasNext()) { - Vibration vib = iter.next(); - if (vib.mToken == token) { - iter.remove(); - unlinkVibration(vib); - return vib; - } - } - // We might be looking for a simple vibration which is only stored in - // mCurrentVibration. - if (mCurrentVibration != null && mCurrentVibration.mToken == token) { - unlinkVibration(mCurrentVibration); - return mCurrentVibration; - } - return null; - } - private void unlinkVibration(Vibration vib) { - if (vib.mPattern != null) { - // If Vibration object has a pattern, - // the Vibration object has also been linkedToDeath. + if (vib.mEffect instanceof VibrationEffect.Waveform) { vib.mToken.unlinkToDeath(vib, 0); } } - private void updateInputDeviceVibrators() { - synchronized (mVibrations) { - doCancelVibrateLocked(); + private void updateVibrators() { + synchronized (mLock) { + boolean devicesUpdated = updateInputDeviceVibratorsLocked(); + boolean lowPowerModeUpdated = updateLowPowerModeLocked(); - synchronized (mInputDeviceVibrators) { - mVibrateInputDevicesSetting = false; - try { - mVibrateInputDevicesSetting = Settings.System.getIntForUser( - mContext.getContentResolver(), - Settings.System.VIBRATE_INPUT_DEVICES, UserHandle.USER_CURRENT) > 0; - } catch (SettingNotFoundException snfe) { - } + if (devicesUpdated || lowPowerModeUpdated) { + // If the state changes out from under us then just reset. + doCancelVibrateLocked(); + } + } + } - mLowPowerMode = mPowerManagerInternal - .getLowPowerState(ServiceType.VIBRATION).batterySaverEnabled; + private boolean updateInputDeviceVibratorsLocked() { + boolean changed = false; + boolean vibrateInputDevices = false; + try { + vibrateInputDevices = Settings.System.getIntForUser( + mContext.getContentResolver(), + Settings.System.VIBRATE_INPUT_DEVICES, UserHandle.USER_CURRENT) > 0; + } catch (SettingNotFoundException snfe) { + } + if (vibrateInputDevices != mVibrateInputDevicesSetting) { + changed = true; + mVibrateInputDevicesSetting = vibrateInputDevices; + } - if (mVibrateInputDevicesSetting) { - if (!mInputDeviceListenerRegistered) { - mInputDeviceListenerRegistered = true; - mIm.registerInputDeviceListener(this, mH); - } - } else { - if (mInputDeviceListenerRegistered) { - mInputDeviceListenerRegistered = false; - mIm.unregisterInputDeviceListener(this); - } - } + if (mVibrateInputDevicesSetting) { + if (!mInputDeviceListenerRegistered) { + mInputDeviceListenerRegistered = true; + mIm.registerInputDeviceListener(this, mH); + } + } else { + if (mInputDeviceListenerRegistered) { + mInputDeviceListenerRegistered = false; + mIm.unregisterInputDeviceListener(this); + } + } - mInputDeviceVibrators.clear(); - if (mVibrateInputDevicesSetting) { - int[] ids = mIm.getInputDeviceIds(); - for (int i = 0; i < ids.length; i++) { - InputDevice device = mIm.getInputDevice(ids[i]); - Vibrator vibrator = device.getVibrator(); - if (vibrator.hasVibrator()) { - mInputDeviceVibrators.add(vibrator); - } - } + mInputDeviceVibrators.clear(); + if (mVibrateInputDevicesSetting) { + int[] ids = mIm.getInputDeviceIds(); + for (int i = 0; i < ids.length; i++) { + InputDevice device = mIm.getInputDevice(ids[i]); + Vibrator vibrator = device.getVibrator(); + if (vibrator.hasVibrator()) { + mInputDeviceVibrators.add(vibrator); } } + return true; + } + return changed; + } - startNextVibrationLocked(); + private boolean updateLowPowerModeLocked() { + boolean lowPowerMode = mPowerManagerInternal + .getLowPowerState(ServiceType.VIBRATION).batterySaverEnabled; + if (lowPowerMode != mLowPowerMode) { + mLowPowerMode = lowPowerMode; + return true; } + return false; } @Override public void onInputDeviceAdded(int deviceId) { - updateInputDeviceVibrators(); + updateVibrators(); } @Override public void onInputDeviceChanged(int deviceId) { - updateInputDeviceVibrators(); + updateVibrators(); } @Override public void onInputDeviceRemoved(int deviceId) { - updateInputDeviceVibrators(); + updateVibrators(); } private boolean doVibratorExists() { @@ -619,41 +632,44 @@ public class VibratorService extends IVibratorService.Stub return vibratorExists(); } - private void doVibratorOn(long millis, int uid, int usageHint) { + private void doVibratorOn(long millis, int amplitude, int uid, int usageHint) { synchronized (mInputDeviceVibrators) { - if (DEBUG) { - Slog.d(TAG, "Turning vibrator on for " + millis + " ms."); + if (amplitude == VibrationEffect.DEFAULT_AMPLITUDE) { + amplitude = mDefaultVibrationAmplitude; } - try { - mBatteryStatsService.noteVibratorOn(uid, millis); - mCurVibUid = uid; - } catch (RemoteException e) { + if (DEBUG) { + Slog.d(TAG, "Turning vibrator on for " + millis + " ms" + + " with amplitude " + amplitude + "."); } + noteVibratorOnLocked(uid, millis); final int vibratorCount = mInputDeviceVibrators.size(); if (vibratorCount != 0) { - final AudioAttributes attributes = new AudioAttributes.Builder().setUsage(usageHint) - .build(); + final AudioAttributes attributes = + new AudioAttributes.Builder().setUsage(usageHint).build(); for (int i = 0; i < vibratorCount; i++) { mInputDeviceVibrators.get(i).vibrate(millis, attributes); } } else { + // Note: ordering is important here! Many haptic drivers will reset their amplitude + // when enabled, so we always have to enable frst, then set the amplitude. vibratorOn(millis); + doVibratorSetAmplitude(amplitude); } } } + private void doVibratorSetAmplitude(int amplitude) { + if (mSupportsAmplitudeControl) { + vibratorSetAmplitude(amplitude); + } + } + private void doVibratorOff() { synchronized (mInputDeviceVibrators) { if (DEBUG) { Slog.d(TAG, "Turning vibrator off."); } - if (mCurVibUid >= 0) { - try { - mBatteryStatsService.noteVibratorOff(mCurVibUid); - } catch (RemoteException e) { - } - mCurVibUid = -1; - } + noteVibratorOffLocked(); final int vibratorCount = mInputDeviceVibrators.size(); if (vibratorCount != 0) { for (int i = 0; i < vibratorCount; i++) { @@ -665,86 +681,175 @@ public class VibratorService extends IVibratorService.Stub } } + private long doVibratorPrebakedEffectLocked(Vibration vib) { + synchronized (mInputDeviceVibrators) { + VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) vib.mEffect; + // Input devices don't support prebaked effect, so skip trying it with them. + final int vibratorCount = mInputDeviceVibrators.size(); + if (vibratorCount == 0) { + long timeout = vibratorPerformEffect(prebaked.getId(), EffectStrength.MEDIUM); + if (timeout > 0) { + noteVibratorOnLocked(vib.mUid, timeout); + return timeout; + } + } + final int id = prebaked.getId(); + if (id < 0 || id >= mFallbackEffects.length) { + Slog.w(TAG, "Failed to play prebaked effect, no fallback"); + return 0; + } + VibrationEffect effect = mFallbackEffects[id]; + Vibration fallbackVib = + new Vibration(vib.mToken, effect, vib.mUsageHint, vib.mUid, vib.mOpPkg); + startVibrationInnerLocked(fallbackVib); + } + return 0; + } + + private void noteVibratorOnLocked(int uid, long millis) { + try { + mBatteryStatsService.noteVibratorOn(uid, millis); + mCurVibUid = uid; + } catch (RemoteException e) { + } + } + + private void noteVibratorOffLocked() { + if (mCurVibUid >= 0) { + try { + mBatteryStatsService.noteVibratorOff(mCurVibUid); + } catch (RemoteException e) { } + mCurVibUid = -1; + } + } + private class VibrateThread extends Thread { - final Vibration mVibration; - boolean mDone; + private final VibrationEffect.Waveform mWaveform; + private final int mUid; + private final int mUsageHint; - VibrateThread(Vibration vib) { - mVibration = vib; - mTmpWorkSource.set(vib.mUid); + private boolean mForceStop; + + VibrateThread(VibrationEffect.Waveform waveform, int uid, int usageHint) { + mWaveform = waveform; + mUid = uid; + mUsageHint = usageHint; + mTmpWorkSource.set(uid); mWakeLock.setWorkSource(mTmpWorkSource); - mWakeLock.acquire(); } - private void delay(long duration) { + private long delayLocked(long duration) { + long durationRemaining = duration; if (duration > 0) { - long bedtime = duration + SystemClock.uptimeMillis(); + final long bedtime = duration + SystemClock.uptimeMillis(); do { try { - this.wait(duration); + this.wait(durationRemaining); } - catch (InterruptedException e) { - } - if (mDone) { + catch (InterruptedException e) { } + if (mForceStop) { break; } - duration = bedtime - SystemClock.uptimeMillis(); - } while (duration > 0); + durationRemaining = bedtime - SystemClock.uptimeMillis(); + } while (durationRemaining > 0); + return duration - durationRemaining; } + return 0; } public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY); + mWakeLock.acquire(); + try { + boolean finished = playWaveform(); + if (finished) { + onVibrationFinished(); + } + } finally { + mWakeLock.release(); + } + } + + /** + * Play the waveform. + * + * @return true if it finished naturally, false otherwise (e.g. it was canceled). + */ + public boolean playWaveform() { synchronized (this) { - final long[] pattern = mVibration.mPattern; - final int len = pattern.length; - final int repeat = mVibration.mRepeat; - final int uid = mVibration.mUid; - final int usageHint = mVibration.mUsageHint; - int index = 0; - long duration = 0; + final long[] timings = mWaveform.getTimings(); + final int[] amplitudes = mWaveform.getAmplitudes(); + final int len = timings.length; + final int repeat = mWaveform.getRepeatIndex(); - while (!mDone) { - // add off-time duration to any accumulated on-time duration + int index = 0; + long onDuration = 0; + while (!mForceStop) { if (index < len) { - duration += pattern[index++]; - } - - // sleep until it is time to start the vibrator - delay(duration); - if (mDone) { - break; - } + final int amplitude = amplitudes[index]; + final long duration = timings[index++]; + if (duration <= 0) { + continue; + } + if (amplitude != 0) { + if (onDuration <= 0) { + // Telling the vibrator to start multiple times usually causes + // effects to feel "choppy" because the motor resets at every on + // command. Instead we figure out how long our next "on" period is + // going to be, tell the motor to stay on for the full duration, + // and then wake up to change the amplitude at the appropriate + // intervals. + onDuration = + getTotalOnDuration(timings, amplitudes, index - 1, repeat); + doVibratorOn(onDuration, amplitude, mUid, mUsageHint); + } else { + doVibratorSetAmplitude(amplitude); + } + } - if (index < len) { - // read on-time duration and start the vibrator - // duration is saved for delay() at top of loop - duration = pattern[index++]; - if (duration > 0) { - VibratorService.this.doVibratorOn(duration, uid, usageHint); + long waitTime = delayLocked(duration); + if (amplitude != 0) { + onDuration -= waitTime; } + } else if (repeat < 0) { + break; } else { - if (repeat < 0) { - break; - } else { - index = repeat; - duration = 0; - } + index = repeat; } } - mWakeLock.release(); + return !mForceStop; + } + } + + public void cancel() { + synchronized (this) { + mThread.mForceStop = true; + mThread.notify(); } - synchronized (mVibrations) { - if (mThread == this) { - mThread = null; + } + + /** + * Get the duration the vibrator will be on starting at startIndex until the next time it's + * off. + */ + private long getTotalOnDuration( + long[] timings, int[] amplitudes, int startIndex, int repeatIndex) { + int i = startIndex; + long timing = 0; + while(amplitudes[i] != 0) { + timing += timings[i++]; + if (i >= timings.length) { + if (repeatIndex >= 0) { + i = repeatIndex; + } else { + break; + } } - if (!mDone) { - // If this vibration finished naturally, start the next - // vibration. - unlinkVibration(mVibration); - startNextVibrationLocked(); + if (i == startIndex) { + return 1000; } } + return timing; } } @@ -752,7 +857,7 @@ public class VibratorService extends IVibratorService.Stub @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { - synchronized (mVibrations) { + synchronized (mLock) { // When the system is entering a non-interactive state, we want // to cancel vibrations in case a misbehaving app has abandoned // them. However it may happen that the system is currently playing @@ -762,16 +867,6 @@ public class VibratorService extends IVibratorService.Stub && !mCurrentVibration.isSystemHapticFeedback()) { doCancelVibrateLocked(); } - - // Clear all remaining vibrations. - Iterator<Vibration> it = mVibrations.iterator(); - while (it.hasNext()) { - Vibration vibration = it.next(); - if (vibration != mCurrentVibration) { - unlinkVibration(vibration); - it.remove(); - } - } } } } @@ -788,7 +883,7 @@ public class VibratorService extends IVibratorService.Stub return; } pw.println("Previous vibrations:"); - synchronized (mVibrations) { + synchronized (mLock) { for (VibrationInfo info : mPreviousVibrations) { pw.print(" "); pw.println(info.toString()); @@ -830,7 +925,10 @@ public class VibratorService extends IVibratorService.Stub if (description == null) { description = "Shell command"; } - vibrate(Binder.getCallingUid(), description, duration, AudioAttributes.USAGE_UNKNOWN, + + VibrationEffect effect = + VibrationEffect.createOneShot(duration, VibrationEffect.DEFAULT_AMPLITUDE); + vibrate(Binder.getCallingUid(), description, effect, AudioAttributes.USAGE_UNKNOWN, mToken); return 0; } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 3dcc5d997e08..ae5c2c1c240c 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -105,6 +105,7 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.os.Vibrator; +import android.os.VibrationEffect; import android.provider.Settings; import android.service.notification.Adjustment; import android.service.notification.Condition; @@ -3613,9 +3614,12 @@ public class NotificationManagerService extends SystemService { // notifying app does not have the VIBRATE permission. long identity = Binder.clearCallingIdentity(); try { - mVibrator.vibrate(record.sbn.getUid(), record.sbn.getOpPkg(), vibration, - ((record.getNotification().flags & Notification.FLAG_INSISTENT) != 0) - ? 0: -1, record.getAudioAttributes()); + final boolean insistent = + (record.getNotification().flags & Notification.FLAG_INSISTENT) != 0; + final VibrationEffect effect = VibrationEffect.createWaveform( + vibration, insistent ? 0 : -1 /*repeatIndex*/); + mVibrator.vibrate(record.sbn.getUid(), record.sbn.getOpPkg(), + effect, record.getAudioAttributes()); return true; } finally{ Binder.restoreCallingIdentity(identity); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 548fa1ec6d75..9386532450cb 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -177,6 +177,7 @@ import android.os.SystemProperties; import android.os.UEventObserver; import android.os.UserHandle; import android.os.Vibrator; +import android.os.VibrationEffect; import android.provider.MediaStore; import android.provider.Settings; import android.service.dreams.DreamManagerInternal; @@ -7524,17 +7525,35 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (hapticsDisabled && !always) { return false; } - long[] pattern = null; + + VibrationEffect effect = getVibrationEffect(effectId); + if (effect == null) { + return false; + } + + int owningUid; + String owningPackage; + if (win != null) { + owningUid = win.getOwningUid(); + owningPackage = win.getOwningPackage(); + } else { + owningUid = android.os.Process.myUid(); + owningPackage = mContext.getOpPackageName(); + } + mVibrator.vibrate(owningUid, owningPackage, effect, VIBRATION_ATTRIBUTES); + return true; + } + + private VibrationEffect getVibrationEffect(int effectId) { + long[] pattern; switch (effectId) { + case HapticFeedbackConstants.VIRTUAL_KEY: + return VibrationEffect.get(VibrationEffect.EFFECT_CLICK); case HapticFeedbackConstants.LONG_PRESS: pattern = mLongPressVibePattern; break; - case HapticFeedbackConstants.VIRTUAL_KEY: - pattern = mVirtualKeyVibePattern; - break; case HapticFeedbackConstants.KEYBOARD_TAP: - pattern = mKeyboardTapVibePattern; - break; + return VibrationEffect.get(VibrationEffect.EFFECT_CLICK); case HapticFeedbackConstants.CLOCK_TICK: pattern = mClockTickVibePattern; break; @@ -7551,25 +7570,15 @@ public class PhoneWindowManager implements WindowManagerPolicy { pattern = mContextClickVibePattern; break; default: - return false; - } - int owningUid; - String owningPackage; - if (win != null) { - owningUid = win.getOwningUid(); - owningPackage = win.getOwningPackage(); - } else { - owningUid = android.os.Process.myUid(); - owningPackage = mContext.getOpPackageName(); + return null; } if (pattern.length == 1) { // One-shot vibration - mVibrator.vibrate(owningUid, owningPackage, pattern[0], VIBRATION_ATTRIBUTES); + return VibrationEffect.createOneShot(pattern[0], VibrationEffect.DEFAULT_AMPLITUDE); } else { // Pattern vibration - mVibrator.vibrate(owningUid, owningPackage, pattern, -1, VIBRATION_ATTRIBUTES); + return VibrationEffect.createWaveform(pattern, -1); } - return true; } @Override diff --git a/services/core/jni/com_android_server_VibratorService.cpp b/services/core/jni/com_android_server_VibratorService.cpp index 50bae7943574..76ce890717d7 100644 --- a/services/core/jni/com_android_server_VibratorService.cpp +++ b/services/core/jni/com_android_server_VibratorService.cpp @@ -27,8 +27,12 @@ #include <utils/Log.h> #include <hardware/vibrator.h> +#include <inttypes.h> #include <stdio.h> +using android::hardware::Return; +using android::hardware::vibrator::V1_0::Effect; +using android::hardware::vibrator::V1_0::EffectStrength; using android::hardware::vibrator::V1_0::IVibrator; using android::hardware::vibrator::V1_0::Status; @@ -59,8 +63,8 @@ static void vibratorOn(JNIEnv* /* env */, jobject /* clazz */, jlong timeout_ms) { if (mHal != nullptr) { Status retStatus = mHal->on(timeout_ms); - if (retStatus == Status::ERR) { - ALOGE("vibratorOn command failed."); + if (retStatus != Status::OK) { + ALOGE("vibratorOn command failed (%" PRIu32 ").", static_cast<uint32_t>(retStatus)); } } else { ALOGW("Tried to vibrate but there is no vibrator device."); @@ -71,19 +75,68 @@ static void vibratorOff(JNIEnv* /* env */, jobject /* clazz */) { if (mHal != nullptr) { Status retStatus = mHal->off(); - if (retStatus == Status::ERR) { - ALOGE("vibratorOff command failed."); + if (retStatus != Status::OK) { + ALOGE("vibratorOff command failed (%" PRIu32 ").", static_cast<uint32_t>(retStatus)); } } else { ALOGW("Tried to stop vibrating but there is no vibrator device."); } } +static jlong vibratorSupportsAmplitudeControl(JNIEnv*, jobject) { + if (mHal != nullptr) { + return mHal->supportsAmplitudeControl(); + } else { + ALOGW("Unable to get max vibration amplitude, there is no vibrator device."); + } + return false; +} + +static void vibratorSetAmplitude(JNIEnv*, jobject, jint amplitude) { + if (mHal != nullptr) { + Status status = mHal->setAmplitude(static_cast<uint32_t>(amplitude)); + if (status != Status::OK) { + ALOGE("Failed to set vibrator amplitude (%" PRIu32 ").", + static_cast<uint32_t>(status)); + } + } else { + ALOGW("Unable to set vibration amplitude, there is no vibrator device."); + } +} + +static jlong vibratorPerformEffect(JNIEnv*, jobject, jlong effect, jint strength) { + if (mHal != nullptr) { + Status status; + uint32_t lengthMs; + mHal->perform(static_cast<Effect>(effect), static_cast<EffectStrength>(strength), + [&status, &lengthMs](Status retStatus, uint32_t retLengthMs) { + status = retStatus; + lengthMs = retLengthMs; + }); + if (status == Status::OK) { + return lengthMs; + } else if (status != Status::UNSUPPORTED_OPERATION) { + // Don't warn on UNSUPPORTED_OPERATION, that's a normal even and just means the motor + // doesn't have a pre-defined waveform to perform for it, so we should just fall back + // to the framework waveforms. + ALOGE("Failed to perform haptic effect: effect=%" PRId64 ", strength=%" PRId32 + ", error=%" PRIu32 ").", static_cast<int64_t>(effect), + static_cast<int32_t>(strength), static_cast<uint32_t>(status)); + } + } else { + ALOGW("Unable to perform haptic effect, there is no vibrator device."); + } + return -1; +} + static const JNINativeMethod method_table[] = { { "vibratorExists", "()Z", (void*)vibratorExists }, { "vibratorInit", "()V", (void*)vibratorInit }, { "vibratorOn", "(J)V", (void*)vibratorOn }, - { "vibratorOff", "()V", (void*)vibratorOff } + { "vibratorOff", "()V", (void*)vibratorOff }, + { "vibratorSupportsAmplitudeControl", "()Z", (void*)vibratorSupportsAmplitudeControl}, + { "vibratorSetAmplitude", "(I)V", (void*)vibratorSetAmplitude}, + { "vibratorPerformEffect", "(JJ)J", (void*)vibratorPerformEffect} }; int register_android_server_VibratorService(JNIEnv *env) diff --git a/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java index 15f7557c92ef..e28566931d23 100644 --- a/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java +++ b/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java @@ -39,12 +39,14 @@ import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; import android.os.Vibrator; +import android.os.VibrationEffect; import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; +import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -53,6 +55,7 @@ import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -78,6 +81,9 @@ public class BuzzBeepBlinkTest { private int mPid = 2000; private android.os.UserHandle mUser = UserHandle.of(ActivityManager.getCurrentUser()); + private VibrateRepeatMatcher mVibrateOnceMatcher = new VibrateRepeatMatcher(-1); + private VibrateRepeatMatcher mVibrateLoopMatcher = new VibrateRepeatMatcher(0); + private static final long[] CUSTOM_VIBRATION = new long[] { 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, @@ -90,7 +96,9 @@ public class BuzzBeepBlinkTest { private static final int CUSTOM_LIGHT_COLOR = Color.BLACK; private static final int CUSTOM_LIGHT_ON = 10000; private static final int CUSTOM_LIGHT_OFF = 10000; - private static final long[] FALLBACK_VIBRATION = new long[] {100, 100, 100}; + private static final long[] FALLBACK_VIBRATION_PATTERN = new long[] {100, 100, 100}; + private static final VibrationEffect FALLBACK_VIBRATION = + VibrationEffect.createWaveform(FALLBACK_VIBRATION_PATTERN, -1); @Before public void setUp() { @@ -108,7 +116,7 @@ public class BuzzBeepBlinkTest { mService.setHandler(mHandler); mService.setLights(mLight); mService.setScreenOn(false); - mService.setFallbackVibrationPattern(FALLBACK_VIBRATION); + mService.setFallbackVibrationPattern(FALLBACK_VIBRATION_PATTERN); } // @@ -272,18 +280,18 @@ public class BuzzBeepBlinkTest { } private void verifyNeverVibrate() { - verify(mVibrator, never()).vibrate(anyInt(), anyString(), (long[]) anyObject(), - anyInt(), (AudioAttributes) anyObject()); + verify(mVibrator, never()).vibrate(anyInt(), anyString(), (VibrationEffect) anyObject(), + (AudioAttributes) anyObject()); } private void verifyVibrate() { - verify(mVibrator, times(1)).vibrate(anyInt(), anyString(), (long[]) anyObject(), - eq(-1), (AudioAttributes) anyObject()); + verify(mVibrator, times(1)).vibrate(anyInt(), anyString(), argThat(mVibrateOnceMatcher), + (AudioAttributes) anyObject()); } private void verifyVibrateLooped() { - verify(mVibrator, times(1)).vibrate(anyInt(), anyString(), (long[]) anyObject(), - eq(0), (AudioAttributes) anyObject()); + verify(mVibrator, times(1)).vibrate(anyInt(), anyString(), argThat(mVibrateLoopMatcher), + (AudioAttributes) anyObject()); } private void verifyStopVibrate() { @@ -485,8 +493,10 @@ public class BuzzBeepBlinkTest { mService.buzzBeepBlinkLocked(r); - verify(mVibrator, times(1)).vibrate(anyInt(), anyString(), eq(r.getVibration()), - eq(-1), (AudioAttributes) anyObject()); + VibrationEffect effect = VibrationEffect.createWaveform(r.getVibration(), -1); + + verify(mVibrator, times(1)).vibrate(anyInt(), anyString(), eq(effect), + (AudioAttributes) anyObject()); } @Test @@ -501,7 +511,7 @@ public class BuzzBeepBlinkTest { mService.buzzBeepBlinkLocked(r); verify(mVibrator, times(1)).vibrate(anyInt(), anyString(), eq(FALLBACK_VIBRATION), - eq(-1), (AudioAttributes) anyObject()); + (AudioAttributes) anyObject()); verify(mRingtonePlayer, never()).playAsync (anyObject(), anyObject(), anyBoolean(), anyObject()); } @@ -667,4 +677,27 @@ public class BuzzBeepBlinkTest { mService.buzzBeepBlinkLocked(s); verifyStopVibrate(); } + + static class VibrateRepeatMatcher implements ArgumentMatcher<VibrationEffect> { + private final int mRepeatIndex; + + VibrateRepeatMatcher(int repeatIndex) { + mRepeatIndex = repeatIndex; + } + + @Override + public boolean matches(VibrationEffect actual) { + if (actual instanceof VibrationEffect.Waveform && + ((VibrationEffect.Waveform) actual).getRepeatIndex() == mRepeatIndex) { + return true; + } + // All non-waveform effects are essentially one shots. + return mRepeatIndex == -1; + } + + @Override + public String toString() { + return "repeatIndex=" + mRepeatIndex; + } + } } diff --git a/tests/permission/src/com/android/framework/permission/tests/VibratorServicePermissionTest.java b/tests/permission/src/com/android/framework/permission/tests/VibratorServicePermissionTest.java index b12ed94467b7..2757296f588f 100644 --- a/tests/permission/src/com/android/framework/permission/tests/VibratorServicePermissionTest.java +++ b/tests/permission/src/com/android/framework/permission/tests/VibratorServicePermissionTest.java @@ -24,6 +24,7 @@ import android.os.IVibratorService; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.VibrationEffect; import android.test.suitebuilder.annotation.SmallTest; /** @@ -48,7 +49,9 @@ public class VibratorServicePermissionTest extends TestCase { */ public void testVibrate() throws RemoteException { try { - mVibratorService.vibrate(Process.myUid(), null, 2000, AudioManager.STREAM_ALARM, + final VibrationEffect effect = + VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE); + mVibratorService.vibrate(Process.myUid(), null, effect, AudioManager.STREAM_ALARM, new Binder()); fail("vibrate did not throw SecurityException as expected"); } catch (SecurityException e) { @@ -57,23 +60,6 @@ public class VibratorServicePermissionTest extends TestCase { } /** - * Test that calling {@link android.os.IVibratorService#vibratePattern(long[], - * int, android.os.IBinder)} requires permissions. - * <p>Tests permission: - * {@link android.Manifest.permission#VIBRATE} - * @throws RemoteException - */ - public void testVibratePattern() throws RemoteException { - try { - mVibratorService.vibratePattern(Process.myUid(), null, new long[] {0}, 0, - AudioManager.STREAM_ALARM, new Binder()); - fail("vibratePattern did not throw SecurityException as expected"); - } catch (SecurityException e) { - // expected - } - } - - /** * Test that calling {@link android.os.IVibratorService#cancelVibrate()} requires permissions. * <p>Tests permission: * {@link android.Manifest.permission#VIBRATE} |