blob: 69cc90bf381ec7bbe0d263d50506baaf2a6c7e5e [file] [log] [blame]
/*
* Copyright (C) 2020 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 com.android.server.vibrator;
import android.annotation.Nullable;
import android.hardware.vibrator.IVibrator;
import android.os.Binder;
import android.os.IVibratorStateListener;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.VibratorInfo;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.RampSegment;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import libcore.util.NativeAllocationRegistry;
/** Controls a single vibrator. */
final class VibratorController {
private static final String TAG = "VibratorController";
// TODO(b/167947076): load suggested range from config
private static final int SUGGESTED_FREQUENCY_SAFE_RANGE = 200;
private final Object mLock = new Object();
private final NativeWrapper mNativeWrapper;
private final VibratorInfo.Builder mVibratorInfoBuilder;
@GuardedBy("mLock")
private VibratorInfo mVibratorInfo;
@GuardedBy("mLock")
private boolean mVibratorInfoLoaded;
@GuardedBy("mLock")
private final RemoteCallbackList<IVibratorStateListener> mVibratorStateListeners =
new RemoteCallbackList<>();
@GuardedBy("mLock")
private boolean mIsVibrating;
@GuardedBy("mLock")
private boolean mIsUnderExternalControl;
@GuardedBy("mLock")
private float mCurrentAmplitude;
/** Listener for vibration completion callbacks from native. */
public interface OnVibrationCompleteListener {
/** Callback triggered when vibration is complete. */
void onComplete(int vibratorId, long vibrationId);
}
VibratorController(int vibratorId, OnVibrationCompleteListener listener) {
this(vibratorId, listener, new NativeWrapper());
}
@VisibleForTesting
VibratorController(int vibratorId, OnVibrationCompleteListener listener,
NativeWrapper nativeWrapper) {
mNativeWrapper = nativeWrapper;
mNativeWrapper.init(vibratorId, listener);
mVibratorInfoBuilder = new VibratorInfo.Builder(vibratorId);
mVibratorInfoLoaded = mNativeWrapper.getInfo(SUGGESTED_FREQUENCY_SAFE_RANGE,
mVibratorInfoBuilder);
mVibratorInfo = mVibratorInfoBuilder.build();
}
/** Register state listener for this vibrator. */
public boolean registerVibratorStateListener(IVibratorStateListener listener) {
synchronized (mLock) {
final long token = Binder.clearCallingIdentity();
try {
if (!mVibratorStateListeners.register(listener)) {
return false;
}
// Notify its callback after new client registered.
notifyStateListenerLocked(listener);
return true;
} finally {
Binder.restoreCallingIdentity(token);
}
}
}
/** Remove registered state listener for this vibrator. */
public boolean unregisterVibratorStateListener(IVibratorStateListener listener) {
synchronized (mLock) {
final long token = Binder.clearCallingIdentity();
try {
return mVibratorStateListeners.unregister(listener);
} finally {
Binder.restoreCallingIdentity(token);
}
}
}
/** Return the {@link VibratorInfo} representing the vibrator controlled by this instance. */
public VibratorInfo getVibratorInfo() {
synchronized (mLock) {
if (!mVibratorInfoLoaded) {
// Try to load the vibrator metadata that has failed in the last attempt.
mVibratorInfoLoaded = mNativeWrapper.getInfo(SUGGESTED_FREQUENCY_SAFE_RANGE,
mVibratorInfoBuilder);
mVibratorInfo = mVibratorInfoBuilder.build();
}
return mVibratorInfo;
}
}
/**
* Return {@code true} is this vibrator is currently vibrating, false otherwise.
*
* <p>This state is controlled by calls to {@link #on} and {@link #off} methods, and is
* automatically notified to any registered {@link IVibratorStateListener} on change.
*/
public boolean isVibrating() {
synchronized (mLock) {
return mIsVibrating;
}
}
/**
* Returns the current amplitude the device is vibrating.
*
* <p>This value is set to 1 by the method {@link #on(long, long)}, and can be updated via
* {@link #setAmplitude(float)} if called while the device is vibrating.
*
* <p>If the device is vibrating via any other {@link #on} method then the current amplitude is
* unknown and this will return -1.
*
* <p>If {@link #isVibrating()} is false then this will be zero.
*/
public float getCurrentAmplitude() {
synchronized (mLock) {
return mCurrentAmplitude;
}
}
/** Return {@code true} if this vibrator is under external control, false otherwise. */
public boolean isUnderExternalControl() {
synchronized (mLock) {
return mIsUnderExternalControl;
}
}
/**
* Check against this vibrator capabilities.
*
* @param capability one of IVibrator.CAP_*
* @return true if this vibrator has this capability, false otherwise
*/
public boolean hasCapability(long capability) {
return mVibratorInfo.hasCapability(capability);
}
/** Return {@code true} if the underlying vibrator is currently available, false otherwise. */
public boolean isAvailable() {
return mNativeWrapper.isAvailable();
}
/**
* Set the vibrator control to be external or not, based on given flag.
*
* <p>This will affect the state of {@link #isUnderExternalControl()}.
*/
public void setExternalControl(boolean externalControl) {
if (!mVibratorInfo.hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) {
return;
}
synchronized (mLock) {
mIsUnderExternalControl = externalControl;
mNativeWrapper.setExternalControl(externalControl);
}
}
/**
* Update the predefined vibration effect saved with given id. This will remove the saved effect
* if given {@code effect} is {@code null}.
*/
public void updateAlwaysOn(int id, @Nullable PrebakedSegment prebaked) {
if (!mVibratorInfo.hasCapability(IVibrator.CAP_ALWAYS_ON_CONTROL)) {
return;
}
synchronized (mLock) {
if (prebaked == null) {
mNativeWrapper.alwaysOnDisable(id);
} else {
mNativeWrapper.alwaysOnEnable(id, prebaked.getEffectId(),
prebaked.getEffectStrength());
}
}
}
/** Set the vibration amplitude. This will NOT affect the state of {@link #isVibrating()}. */
public void setAmplitude(float amplitude) {
synchronized (mLock) {
if (mVibratorInfo.hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL)) {
mNativeWrapper.setAmplitude(amplitude);
}
if (mIsVibrating) {
mCurrentAmplitude = amplitude;
}
}
}
/**
* Turn on the vibrator for {@code milliseconds} time, using {@code vibrationId} or completion
* callback to {@link OnVibrationCompleteListener}.
*
* <p>This will affect the state of {@link #isVibrating()}.
*
* @return The positive duration of the vibration started, if successful, zero if the vibrator
* do not support the input or a negative number if the operation failed.
*/
public long on(long milliseconds, long vibrationId) {
synchronized (mLock) {
long duration = mNativeWrapper.on(milliseconds, vibrationId);
if (duration > 0) {
mCurrentAmplitude = -1;
notifyVibratorOnLocked();
}
return duration;
}
}
/**
* Plays predefined vibration effect, using {@code vibrationId} or completion callback to
* {@link OnVibrationCompleteListener}.
*
* <p>This will affect the state of {@link #isVibrating()}.
*
* @return The positive duration of the vibration started, if successful, zero if the vibrator
* do not support the input or a negative number if the operation failed.
*/
public long on(PrebakedSegment prebaked, long vibrationId) {
synchronized (mLock) {
long duration = mNativeWrapper.perform(prebaked.getEffectId(),
prebaked.getEffectStrength(), vibrationId);
if (duration > 0) {
mCurrentAmplitude = -1;
notifyVibratorOnLocked();
}
return duration;
}
}
/**
* Plays a composition of vibration primitives, using {@code vibrationId} or completion callback
* to {@link OnVibrationCompleteListener}.
*
* <p>This will affect the state of {@link #isVibrating()}.
*
* @return The positive duration of the vibration started, if successful, zero if the vibrator
* do not support the input or a negative number if the operation failed.
*/
public long on(PrimitiveSegment[] primitives, long vibrationId) {
if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) {
return 0;
}
synchronized (mLock) {
long duration = mNativeWrapper.compose(primitives, vibrationId);
if (duration > 0) {
mCurrentAmplitude = -1;
notifyVibratorOnLocked();
}
return duration;
}
}
/**
* Plays a composition of pwle primitives, using {@code vibrationId} or completion callback
* to {@link OnVibrationCompleteListener}.
*
* <p>This will affect the state of {@link #isVibrating()}.
*
* @return The duration of the effect playing, or 0 if unsupported.
*/
public long on(RampSegment[] primitives, long vibrationId) {
if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS)) {
return 0;
}
synchronized (mLock) {
int braking = mVibratorInfo.getDefaultBraking();
long duration = mNativeWrapper.composePwle(primitives, braking, vibrationId);
if (duration > 0) {
mCurrentAmplitude = -1;
notifyVibratorOnLocked();
}
return duration;
}
}
/** Turns off the vibrator.This will affect the state of {@link #isVibrating()}. */
public void off() {
synchronized (mLock) {
mNativeWrapper.off();
mCurrentAmplitude = 0;
notifyVibratorOffLocked();
}
}
@Override
public String toString() {
synchronized (mLock) {
return "VibratorController{"
+ "mVibratorInfo=" + mVibratorInfo
+ ", mIsVibrating=" + mIsVibrating
+ ", mCurrentAmplitude=" + mCurrentAmplitude
+ ", mIsUnderExternalControl=" + mIsUnderExternalControl
+ ", mVibratorStateListeners count="
+ mVibratorStateListeners.getRegisteredCallbackCount()
+ '}';
}
}
@GuardedBy("mLock")
private void notifyVibratorOnLocked() {
if (!mIsVibrating) {
mIsVibrating = true;
notifyStateListenersLocked();
}
}
@GuardedBy("mLock")
private void notifyVibratorOffLocked() {
if (mIsVibrating) {
mIsVibrating = false;
notifyStateListenersLocked();
}
}
@GuardedBy("mLock")
private void notifyStateListenersLocked() {
final int length = mVibratorStateListeners.beginBroadcast();
try {
for (int i = 0; i < length; i++) {
notifyStateListenerLocked(mVibratorStateListeners.getBroadcastItem(i));
}
} finally {
mVibratorStateListeners.finishBroadcast();
}
}
@GuardedBy("mLock")
private void notifyStateListenerLocked(IVibratorStateListener listener) {
try {
listener.onVibrating(mIsVibrating);
} catch (RemoteException | RuntimeException e) {
Slog.e(TAG, "Vibrator state listener failed to call", e);
}
}
/** Wrapper around the static-native methods of {@link VibratorController} for tests. */
@VisibleForTesting
public static class NativeWrapper {
/**
* Initializes the native part of this controller, creating a global reference to given
* {@link OnVibrationCompleteListener} and returns a newly allocated native pointer. This
* wrapper is responsible for deleting this pointer by calling the method pointed
* by {@link #getNativeFinalizer()}.
*
* <p><b>Note:</b> Make sure the given implementation of {@link OnVibrationCompleteListener}
* do not hold any strong reference to the instance responsible for deleting the returned
* pointer, to avoid creating a cyclic GC root reference.
*/
private static native long nativeInit(int vibratorId, OnVibrationCompleteListener listener);
/**
* Returns pointer to native function responsible for cleaning up the native pointer
* allocated and returned by {@link #nativeInit(int, OnVibrationCompleteListener)}.
*/
private static native long getNativeFinalizer();
private static native boolean isAvailable(long nativePtr);
private static native long on(long nativePtr, long milliseconds, long vibrationId);
private static native void off(long nativePtr);
private static native void setAmplitude(long nativePtr, float amplitude);
private static native long performEffect(long nativePtr, long effect, long strength,
long vibrationId);
private static native long performComposedEffect(long nativePtr, PrimitiveSegment[] effect,
long vibrationId);
private static native long performPwleEffect(long nativePtr, RampSegment[] effect,
int braking, long vibrationId);
private static native void setExternalControl(long nativePtr, boolean enabled);
private static native void alwaysOnEnable(long nativePtr, long id, long effect,
long strength);
private static native void alwaysOnDisable(long nativePtr, long id);
private static native boolean getInfo(long nativePtr, float suggestedFrequencyRange,
VibratorInfo.Builder infoBuilder);
private long mNativePtr = 0;
/** Initializes native controller and allocation registry to destroy native instances. */
public void init(int vibratorId, OnVibrationCompleteListener listener) {
mNativePtr = nativeInit(vibratorId, listener);
long finalizerPtr = getNativeFinalizer();
if (finalizerPtr != 0) {
NativeAllocationRegistry registry =
NativeAllocationRegistry.createMalloced(
VibratorController.class.getClassLoader(), finalizerPtr);
registry.registerNativeAllocation(this, mNativePtr);
}
}
/** Check if the vibrator is currently available. */
public boolean isAvailable() {
return isAvailable(mNativePtr);
}
/** Turns vibrator on for given time. */
public long on(long milliseconds, long vibrationId) {
return on(mNativePtr, milliseconds, vibrationId);
}
/** Turns vibrator off. */
public void off() {
off(mNativePtr);
}
/** Sets the amplitude for the vibrator to run. */
public void setAmplitude(float amplitude) {
setAmplitude(mNativePtr, amplitude);
}
/** Turns vibrator on to perform one of the supported effects. */
public long perform(long effect, long strength, long vibrationId) {
return performEffect(mNativePtr, effect, strength, vibrationId);
}
/** Turns vibrator on to perform effect composed of give primitives effect. */
public long compose(PrimitiveSegment[] primitives, long vibrationId) {
return performComposedEffect(mNativePtr, primitives, vibrationId);
}
/** Turns vibrator on to perform PWLE effect composed of given primitives. */
public long composePwle(RampSegment[] primitives, int braking, long vibrationId) {
return performPwleEffect(mNativePtr, primitives, braking, vibrationId);
}
/** Enabled the device vibrator to be controlled by another service. */
public void setExternalControl(boolean enabled) {
setExternalControl(mNativePtr, enabled);
}
/** Enable always-on vibration with given id and effect. */
public void alwaysOnEnable(long id, long effect, long strength) {
alwaysOnEnable(mNativePtr, id, effect, strength);
}
/** Disable always-on vibration for given id. */
public void alwaysOnDisable(long id) {
alwaysOnDisable(mNativePtr, id);
}
/**
* Loads device vibrator metadata and returns true if all metadata was loaded successfully.
*/
public boolean getInfo(float suggestedFrequencyRange, VibratorInfo.Builder infoBuilder) {
return getInfo(mNativePtr, suggestedFrequencyRange, infoBuilder);
}
}
}