diff options
11 files changed, 589 insertions, 58 deletions
diff --git a/services/core/Android.bp b/services/core/Android.bp index 37d2cdc16926..96cfe0235914 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -103,7 +103,7 @@ java_library_static { "android.hardware.gnss-V1-java", "android.hardware.power-V1-java", "android.hardware.power-V1.0-java", - "android.hardware.vibrator-V1-java", + "android.hardware.vibrator-V2-java", "android.net.ipsec.ike.stubs.module_lib", "app-compat-annotations", "framework-tethering.stubs.module_lib", diff --git a/services/core/java/com/android/server/VibratorManagerService.java b/services/core/java/com/android/server/VibratorManagerService.java index 6a816af68959..e7e5d67ff9f4 100644 --- a/services/core/java/com/android/server/VibratorManagerService.java +++ b/services/core/java/com/android/server/VibratorManagerService.java @@ -111,6 +111,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private final AppOpsManager mAppOps; private final NativeWrapper mNativeWrapper; private final VibratorManagerRecords mVibratorManagerRecords; + private final long mCapabilities; private final int[] mVibratorIds; private final SparseArray<VibratorController> mVibrators; private final VibrationCallbacks mVibrationCallbacks = new VibrationCallbacks(); @@ -127,18 +128,28 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private VibrationScaler mVibrationScaler; private InputDeviceDelegate mInputDeviceDelegate; - static native long nativeInit(); + static native long nativeInit(OnSyncedVibrationCompleteListener listener); static native long nativeGetFinalizer(); + static native long nativeGetCapabilities(long nativeServicePtr); + static native int[] nativeGetVibratorIds(long nativeServicePtr); + static native boolean nativePrepareSynced(long nativeServicePtr, int[] vibratorIds); + + static native boolean nativeTriggerSynced(long nativeServicePtr, long vibrationId); + + static native void nativeCancelSynced(long nativeServicePtr); + @VisibleForTesting VibratorManagerService(Context context, Injector injector) { mContext = context; mHandler = injector.createHandler(Looper.myLooper()); + + VibrationCompleteListener listener = new VibrationCompleteListener(this); mNativeWrapper = injector.getNativeWrapper(); - mNativeWrapper.init(); + mNativeWrapper.init(listener); int dumpLimit = mContext.getResources().getInteger( com.android.internal.R.integer.config_previousVibrationsDumpLimit); @@ -153,6 +164,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*vibrator*"); mWakeLock.setReferenceCounted(true); + mCapabilities = mNativeWrapper.getCapabilities(); int[] vibratorIds = mNativeWrapper.getVibratorIds(); if (vibratorIds == null) { mVibratorIds = new int[0]; @@ -161,11 +173,17 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // Keep original vibrator id order, which might be meaningful. mVibratorIds = vibratorIds; mVibrators = new SparseArray<>(mVibratorIds.length); - VibrationCompleteListener listener = new VibrationCompleteListener(this); for (int vibratorId : vibratorIds) { mVibrators.put(vibratorId, injector.createVibratorController(vibratorId, listener)); } } + + // Reset the hardware to a default state, in case this is a runtime restart instead of a + // fresh boot. + mNativeWrapper.cancelSynced(); + for (int i = 0; i < mVibrators.size(); i++) { + mVibrators.valueAt(i).off(); + } } /** Finish initialization at boot phase {@link SystemService#PHASE_SYSTEM_SERVICES_READY}. */ @@ -502,6 +520,17 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + private void onSyncedVibrationComplete(long vibrationId) { + synchronized (mLock) { + if (mCurrentVibration != null && mCurrentVibration.getVibration().id == vibrationId) { + if (DEBUG) { + Slog.d(TAG, "Synced vibration " + vibrationId + " complete, notifying thread"); + } + mCurrentVibration.syncedVibrationComplete(); + } + } + } + private void onVibrationComplete(int vibratorId, long vibrationId) { synchronized (mLock) { if (mCurrentVibration != null && mCurrentVibration.getVibration().id == vibrationId) { @@ -839,13 +868,22 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private final class VibrationCallbacks implements VibrationThread.VibrationCallbacks { @Override - public void prepareSyncedVibration(int requiredCapabilities, int[] vibratorIds) { - // TODO(b/167946816): call IVibratorManager to prepare + public boolean prepareSyncedVibration(long requiredCapabilities, int[] vibratorIds) { + if ((mCapabilities & requiredCapabilities) != requiredCapabilities) { + // This sync step requires capabilities this device doesn't have, skipping sync... + return false; + } + return mNativeWrapper.prepareSynced(vibratorIds); } @Override - public void triggerSyncedVibration(long vibrationId) { - // TODO(b/167946816): call IVibratorManager to trigger + public boolean triggerSyncedVibration(long vibrationId) { + return mNativeWrapper.triggerSynced(vibrationId); + } + + @Override + public void cancelSyncedVibration() { + mNativeWrapper.cancelSynced(); } @Override @@ -868,12 +906,19 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + /** Listener for synced vibration completion callbacks from native. */ + @VisibleForTesting + public interface OnSyncedVibrationCompleteListener { + + /** Callback triggered when synced vibration is complete. */ + void onComplete(long vibrationId); + } + /** - * Implementation of {@link VibratorController.OnVibrationCompleteListener} with a weak - * reference to this service. + * Implementation of listeners to native vibrators with a weak reference to this service. */ private static final class VibrationCompleteListener implements - VibratorController.OnVibrationCompleteListener { + VibratorController.OnVibrationCompleteListener, OnSyncedVibrationCompleteListener { private WeakReference<VibratorManagerService> mServiceRef; VibrationCompleteListener(VibratorManagerService service) { @@ -881,6 +926,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @Override + public void onComplete(long vibrationId) { + VibratorManagerService service = mServiceRef.get(); + if (service != null) { + service.onSyncedVibrationComplete(vibrationId); + } + } + + @Override public void onComplete(int vibratorId, long vibrationId) { VibratorManagerService service = mServiceRef.get(); if (service != null) { @@ -952,9 +1005,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private long mNativeServicePtr = 0; /** Returns native pointer to newly created controller and connects with HAL service. */ - public void init() { - mNativeServicePtr = VibratorManagerService.nativeInit(); - long finalizerPtr = VibratorManagerService.nativeGetFinalizer(); + public void init(OnSyncedVibrationCompleteListener listener) { + mNativeServicePtr = nativeInit(listener); + long finalizerPtr = nativeGetFinalizer(); if (finalizerPtr != 0) { NativeAllocationRegistry registry = @@ -964,9 +1017,29 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + /** Returns manager capabilities. */ + public long getCapabilities() { + return nativeGetCapabilities(mNativeServicePtr); + } + /** Returns vibrator ids. */ public int[] getVibratorIds() { - return VibratorManagerService.nativeGetVibratorIds(mNativeServicePtr); + return nativeGetVibratorIds(mNativeServicePtr); + } + + /** Prepare vibrators for triggering vibrations in sync. */ + public boolean prepareSynced(@NonNull int[] vibratorIds) { + return nativePrepareSynced(mNativeServicePtr, vibratorIds); + } + + /** Trigger prepared synced vibration. */ + public boolean triggerSynced(long vibrationId) { + return nativeTriggerSynced(mNativeServicePtr, vibrationId); + } + + /** Cancel prepared synced vibration. */ + public void cancelSynced() { + nativeCancelSynced(mNativeServicePtr); } } diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java index 026eb630d59a..2ac365d6d11b 100644 --- a/services/core/java/com/android/server/VibratorService.java +++ b/services/core/java/com/android/server/VibratorService.java @@ -119,11 +119,17 @@ public class VibratorService extends IVibratorService.Stub { private final class VibrationCallbacks implements VibrationThread.VibrationCallbacks { @Override - public void prepareSyncedVibration(int requiredCapabilities, int[] vibratorIds) { + public boolean prepareSyncedVibration(long requiredCapabilities, int[] vibratorIds) { + return false; + } + + @Override + public boolean triggerSyncedVibration(long vibrationId) { + return false; } @Override - public void triggerSyncedVibration(long vibrationId) { + public void cancelSyncedVibration() { } @Override diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java index 4f2fc86df06c..bee66637fb2f 100644 --- a/services/core/java/com/android/server/vibrator/VibrationThread.java +++ b/services/core/java/com/android/server/vibrator/VibrationThread.java @@ -17,6 +17,7 @@ package com.android.server.vibrator; import android.annotation.Nullable; +import android.hardware.vibrator.IVibratorManager; import android.os.CombinedVibrationEffect; import android.os.IBinder; import android.os.PowerManager; @@ -65,10 +66,13 @@ public final class VibrationThread extends Thread implements IBinder.DeathRecipi * IVibratorManager.CAP_MIXED_TRIGGER_*. * @param vibratorIds The id of the vibrators to be prepared. */ - void prepareSyncedVibration(int requiredCapabilities, int[] vibratorIds); + boolean prepareSyncedVibration(long requiredCapabilities, int[] vibratorIds); /** Callback triggered after synchronized vibrations were prepared. */ - void triggerSyncedVibration(long vibrationId); + boolean triggerSyncedVibration(long vibrationId); + + /** Callback triggered to cancel a prepared synced vibration. */ + void cancelSyncedVibration(); /** Callback triggered when vibration thread is complete. */ void onVibrationEnded(long vibrationId, Vibration.Status status); @@ -146,6 +150,20 @@ public final class VibrationThread extends Thread implements IBinder.DeathRecipi } } + /** Notify current vibration that a synced step has completed. */ + public void syncedVibrationComplete() { + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "Synced vibration complete reported by vibrator manager"); + } + if (mCurrentVibrateStep != null) { + for (int i = 0; i < mVibrators.size(); i++) { + mCurrentVibrateStep.vibratorComplete(mVibrators.keyAt(i)); + } + } + } + } + /** Notify current vibration that a step has completed on given vibrator. */ public void vibratorComplete(int vibratorId) { synchronized (mLock) { @@ -530,7 +548,7 @@ public final class VibrationThread extends Thread implements IBinder.DeathRecipi /** Represent a synchronized vibration step on multiple vibrators. */ private final class SyncedVibrateStep implements VibrateStep { private final SparseArray<VibrationEffect> mEffects; - private final int mRequiredCapabilities; + private final long mRequiredCapabilities; private final int[] mVibratorIds; @GuardedBy("mLock") @@ -539,8 +557,7 @@ public final class VibrationThread extends Thread implements IBinder.DeathRecipi SyncedVibrateStep(SparseArray<VibrationEffect> effects) { mEffects = effects; mActiveVibratorCounter = mEffects.size(); - // TODO(b/159207608): Calculate required capabilities for syncing this step. - mRequiredCapabilities = 0; + mRequiredCapabilities = calculateRequiredSyncCapabilities(effects); mVibratorIds = new int[effects.size()]; for (int i = 0; i < effects.size(); i++) { mVibratorIds[i] = effects.keyAt(i); @@ -574,18 +591,21 @@ public final class VibrationThread extends Thread implements IBinder.DeathRecipi @Override public Vibration.Status play() { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "SyncedVibrateStep"); - long timeout = -1; + long duration = -1; try { if (DEBUG) { Slog.d(TAG, "SyncedVibrateStep starting..."); } final PriorityQueue<AmplitudeStep> nextSteps = new PriorityQueue<>(mEffects.size()); long startTime = SystemClock.uptimeMillis(); - mCallbacks.prepareSyncedVibration(mRequiredCapabilities, mVibratorIds); - timeout = startVibrating(startTime, nextSteps); - mCallbacks.triggerSyncedVibration(mVibration.id); - noteVibratorOn(timeout); + duration = startVibratingSynced(startTime, nextSteps); + if (duration <= 0) { + // Vibrate step failed, vibrator could not be turned on for this step. + return Vibration.Status.IGNORED; + } + + noteVibratorOn(duration); while (!nextSteps.isEmpty()) { AmplitudeStep step = nextSteps.poll(); if (!waitUntil(step.startTime)) { @@ -607,7 +627,7 @@ public final class VibrationThread extends Thread implements IBinder.DeathRecipi synchronized (mLock) { // All OneShot and Waveform effects have finished. Just wait for the other // effects to end via native callbacks before finishing this synced step. - final long wakeUpTime = startTime + timeout + CALLBACKS_EXTRA_TIMEOUT; + final long wakeUpTime = startTime + duration + CALLBACKS_EXTRA_TIMEOUT; if (mActiveVibratorCounter <= 0 || waitForVibrationComplete(this, wakeUpTime)) { return Vibration.Status.FINISHED; } @@ -617,7 +637,7 @@ public final class VibrationThread extends Thread implements IBinder.DeathRecipi return mForceStop ? Vibration.Status.CANCELLED : Vibration.Status.FINISHED; } } finally { - if (timeout > 0) { + if (duration > 0) { noteVibratorOff(); } if (DEBUG) { @@ -628,14 +648,48 @@ public final class VibrationThread extends Thread implements IBinder.DeathRecipi } /** + * Starts playing effects on designated vibrators, in sync. + * + * @return A positive duration, in millis, to wait for the completion of this effect. + * Non-positive values indicate the vibrator has ignored this effect. Repeating waveform + * returns the duration of a single run to be used as timeout for callbacks. + */ + private long startVibratingSynced(long startTime, PriorityQueue<AmplitudeStep> nextSteps) { + // This synchronization of vibrators should be executed one at a time, even if we are + // vibrating different sets of vibrators in parallel. The manager can only prepareSynced + // one set of vibrators at a time. + synchronized (mLock) { + boolean hasPrepared = false; + boolean hasTriggered = false; + try { + hasPrepared = mCallbacks.prepareSyncedVibration(mRequiredCapabilities, + mVibratorIds); + long timeout = startVibrating(startTime, nextSteps); + + // Check if preparation was successful, otherwise devices area already vibrating + if (hasPrepared) { + hasTriggered = mCallbacks.triggerSyncedVibration(mVibration.id); + } + return timeout; + } finally { + if (hasPrepared && !hasTriggered) { + mCallbacks.cancelSyncedVibration(); + return 0; + } + } + } + } + + /** * Starts playing effects on designated vibrators. * * <p>This includes the {@link VibrationEffect.OneShot} and {@link VibrationEffect.Waveform} * effects, that should start in sync with all other effects in this step. The waveforms are * controlled by {@link AmplitudeStep} added to the {@code nextSteps} queue. * - * @return A duration, in millis, to wait for the completion of all vibrations. This ignores - * any repeating waveform duration and returns the duration of a single run. + * @return A positive duration, in millis, to wait for the completion of this effect. + * Non-positive values indicate the vibrator has ignored this effect. Repeating waveform + * returns the duration of a single run to be used as timeout for callbacks. */ private long startVibrating(long startTime, PriorityQueue<AmplitudeStep> nextSteps) { long maxDuration = 0; @@ -651,9 +705,9 @@ public final class VibrationThread extends Thread implements IBinder.DeathRecipi /** * Play a single effect on a single vibrator. * - * @return A duration, in millis, to wait for the completion of this effect. This ignores - * any repeating waveform duration and returns the duration of a single run to be used as - * timeout for callbacks. + * @return A positive duration, in millis, to wait for the completion of this effect. + * Non-positive values indicate the vibrator has ignored this effect. Repeating waveform + * returns the duration of a single run to be used as timeout for callbacks. */ private long startVibrating(VibratorController controller, VibrationEffect effect, long startTime, PriorityQueue<AmplitudeStep> nextSteps) { @@ -698,6 +752,49 @@ public final class VibrationThread extends Thread implements IBinder.DeathRecipi } } } + + /** + * Return all capabilities required from the {@link IVibratorManager} to prepare and + * trigger all given effects in sync. + * + * @return {@link IVibratorManager#CAP_SYNC} together with all required + * IVibratorManager.CAP_PREPARE_* and IVibratorManager.CAP_MIXED_TRIGGER_* capabilities. + */ + private long calculateRequiredSyncCapabilities(SparseArray<VibrationEffect> effects) { + long prepareCap = 0; + for (int i = 0; i < effects.size(); i++) { + VibrationEffect effect = effects.valueAt(i); + if (effect instanceof VibrationEffect.OneShot + || effect instanceof VibrationEffect.Waveform) { + prepareCap |= IVibratorManager.CAP_PREPARE_ON; + } else if (effect instanceof VibrationEffect.Prebaked) { + prepareCap |= IVibratorManager.CAP_PREPARE_PERFORM; + } else if (effect instanceof VibrationEffect.Composed) { + prepareCap |= IVibratorManager.CAP_PREPARE_COMPOSE; + } + } + int triggerCap = 0; + if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_ON)) { + triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_ON; + } + if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_PERFORM)) { + triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_PERFORM; + } + if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_COMPOSE)) { + triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE; + } + return IVibratorManager.CAP_SYNC | prepareCap | triggerCap; + } + + /** + * Return true if {@code prepareCapabilities} contains this {@code capability} mixed with + * different ones, requiring a mixed trigger capability from the vibrator manager for + * syncing all effects. + */ + private boolean requireMixedTriggerCapability(long prepareCapabilities, long capability) { + return (prepareCapabilities & capability) != 0 + && (prepareCapabilities & ~capability) != 0; + } } /** Represent a step to set amplitude on a single vibrator. */ @@ -794,12 +891,12 @@ public final class VibrationThread extends Thread implements IBinder.DeathRecipi // Waveform has ended, no more steps to run. return null; } - long nextWakeUpTime = startTime + waveform.getTimings()[currentIndex]; + long nextStartTime = startTime + waveform.getTimings()[currentIndex]; int nextIndex = currentIndex + 1; if (nextIndex >= waveform.getTimings().length) { nextIndex = waveform.getRepeatIndex(); } - return new AmplitudeStep(vibratorId, waveform, nextIndex, nextWakeUpTime, + return new AmplitudeStep(vibratorId, waveform, nextIndex, nextStartTime, nextVibratorStopTime()); } diff --git a/services/core/jni/com_android_server_VibratorManagerService.cpp b/services/core/jni/com_android_server_VibratorManagerService.cpp index 71de9bda3c4f..5dbb71a4976c 100644 --- a/services/core/jni/com_android_server_VibratorManagerService.cpp +++ b/services/core/jni/com_android_server_VibratorManagerService.cpp @@ -24,27 +24,47 @@ #include <utils/Log.h> #include <utils/misc.h> -#include <vibratorservice/VibratorManagerHalWrapper.h> +#include <vibratorservice/VibratorManagerHalController.h> #include "com_android_server_VibratorManagerService.h" namespace android { +static JavaVM* sJvm = nullptr; +static jmethodID sMethodIdOnComplete; static std::mutex gManagerMutex; -static vibrator::ManagerHalWrapper* gManager GUARDED_BY(gManagerMutex) = nullptr; +static vibrator::ManagerHalController* gManager GUARDED_BY(gManagerMutex) = nullptr; class NativeVibratorManagerService { public: - NativeVibratorManagerService() : mHal(std::make_unique<vibrator::LegacyManagerHalWrapper>()) {} - ~NativeVibratorManagerService() = default; + NativeVibratorManagerService(JNIEnv* env, jobject callbackListener) + : mHal(std::make_unique<vibrator::ManagerHalController>()), + mCallbackListener(env->NewGlobalRef(callbackListener)) { + LOG_ALWAYS_FATAL_IF(mHal == nullptr, "Unable to find reference to vibrator manager hal"); + LOG_ALWAYS_FATAL_IF(mCallbackListener == nullptr, + "Unable to create global reference to vibration callback handler"); + } + + ~NativeVibratorManagerService() { + auto jniEnv = GetOrAttachJNIEnvironment(sJvm); + jniEnv->DeleteGlobalRef(mCallbackListener); + } + + vibrator::ManagerHalController* hal() const { return mHal.get(); } - vibrator::ManagerHalWrapper* hal() const { return mHal.get(); } + std::function<void()> createCallback(jlong vibrationId) { + return [vibrationId, this]() { + auto jniEnv = GetOrAttachJNIEnvironment(sJvm); + jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnComplete, vibrationId); + }; + } private: - const std::unique_ptr<vibrator::ManagerHalWrapper> mHal; + const std::unique_ptr<vibrator::ManagerHalController> mHal; + const jobject mCallbackListener; }; -vibrator::ManagerHalWrapper* android_server_VibratorManagerService_getManager() { +vibrator::ManagerHalController* android_server_VibratorManagerService_getManager() { std::lock_guard<std::mutex> lock(gManagerMutex); return gManager; } @@ -58,9 +78,9 @@ static void destroyNativeService(void* ptr) { } } -static jlong nativeInit(JNIEnv* /* env */, jclass /* clazz */) { +static jlong nativeInit(JNIEnv* env, jclass /* clazz */, jobject callbackListener) { std::unique_ptr<NativeVibratorManagerService> service = - std::make_unique<NativeVibratorManagerService>(); + std::make_unique<NativeVibratorManagerService>(env, callbackListener); { std::lock_guard<std::mutex> lock(gManagerMutex); gManager = service->hal(); @@ -72,6 +92,17 @@ static jlong nativeGetFinalizer(JNIEnv* /* env */, jclass /* clazz */) { return static_cast<jlong>(reinterpret_cast<uintptr_t>(&destroyNativeService)); } +static jlong nativeGetCapabilities(JNIEnv* env, jclass /* clazz */, jlong servicePtr) { + NativeVibratorManagerService* service = + reinterpret_cast<NativeVibratorManagerService*>(servicePtr); + if (service == nullptr) { + ALOGE("nativeGetCapabilities failed because native service was not initialized"); + return 0; + } + auto result = service->hal()->getCapabilities(); + return result.isOk() ? static_cast<jlong>(result.value()) : 0; +} + static jintArray nativeGetVibratorIds(JNIEnv* env, jclass /* clazz */, jlong servicePtr) { NativeVibratorManagerService* service = reinterpret_cast<NativeVibratorManagerService*>(servicePtr); @@ -89,13 +120,61 @@ static jintArray nativeGetVibratorIds(JNIEnv* env, jclass /* clazz */, jlong ser return ids; } +static jboolean nativePrepareSynced(JNIEnv* env, jclass /* clazz */, jlong servicePtr, + jintArray vibratorIds) { + NativeVibratorManagerService* service = + reinterpret_cast<NativeVibratorManagerService*>(servicePtr); + if (service == nullptr) { + ALOGE("nativePrepareSynced failed because native service was not initialized"); + return JNI_FALSE; + } + jsize size = env->GetArrayLength(vibratorIds); + std::vector<int32_t> ids(size); + env->GetIntArrayRegion(vibratorIds, 0, size, reinterpret_cast<jint*>(ids.data())); + return service->hal()->prepareSynced(ids).isOk() ? JNI_TRUE : JNI_FALSE; +} + +static jboolean nativeTriggerSynced(JNIEnv* env, jclass /* clazz */, jlong servicePtr, + jlong vibrationId) { + NativeVibratorManagerService* service = + reinterpret_cast<NativeVibratorManagerService*>(servicePtr); + if (service == nullptr) { + ALOGE("nativeTriggerSynced failed because native service was not initialized"); + return JNI_FALSE; + } + auto callback = service->createCallback(vibrationId); + return service->hal()->triggerSynced(callback).isOk() ? JNI_TRUE : JNI_FALSE; +} + +static void nativeCancelSynced(JNIEnv* env, jclass /* clazz */, jlong servicePtr) { + NativeVibratorManagerService* service = + reinterpret_cast<NativeVibratorManagerService*>(servicePtr); + if (service == nullptr) { + ALOGE("nativeCancelSynced failed because native service was not initialized"); + return; + } + service->hal()->cancelSynced(); +} + static const JNINativeMethod method_table[] = { - {"nativeInit", "()J", (void*)nativeInit}, + {"nativeInit", + "(Lcom/android/server/VibratorManagerService$OnSyncedVibrationCompleteListener;)J", + (void*)nativeInit}, {"nativeGetFinalizer", "()J", (void*)nativeGetFinalizer}, + {"nativeGetCapabilities", "(J)J", (void*)nativeGetCapabilities}, {"nativeGetVibratorIds", "(J)[I", (void*)nativeGetVibratorIds}, + {"nativePrepareSynced", "(J[I)Z", (void*)nativePrepareSynced}, + {"nativeTriggerSynced", "(JJ)Z", (void*)nativeTriggerSynced}, + {"nativeCancelSynced", "(J)V", (void*)nativeCancelSynced}, }; -int register_android_server_VibratorManagerService(JNIEnv* env) { +int register_android_server_VibratorManagerService(JavaVM* jvm, JNIEnv* env) { + sJvm = jvm; + auto listenerClassName = + "com/android/server/VibratorManagerService$OnSyncedVibrationCompleteListener"; + jclass listenerClass = FindClassOrDie(env, listenerClassName); + sMethodIdOnComplete = GetMethodIDOrDie(env, listenerClass, "onComplete", "(J)V"); + return jniRegisterNativeMethods(env, "com/android/server/VibratorManagerService", method_table, NELEM(method_table)); } diff --git a/services/core/jni/com_android_server_VibratorManagerService.h b/services/core/jni/com_android_server_VibratorManagerService.h index 3f2a322b19ee..22950c5036e4 100644 --- a/services/core/jni/com_android_server_VibratorManagerService.h +++ b/services/core/jni/com_android_server_VibratorManagerService.h @@ -17,11 +17,11 @@ #ifndef _ANDROID_SERVER_VIBRATOR_MANAGER_SERVICE_H #define _ANDROID_SERVER_VIBRATOR_MANAGER_SERVICE_H -#include <vibratorservice/VibratorManagerHalWrapper.h> +#include <vibratorservice/VibratorManagerHalController.h> namespace android { -extern vibrator::ManagerHalWrapper* android_server_VibratorManagerService_getManager(); +extern vibrator::ManagerHalController* android_server_VibratorManagerService_getManager(); } // namespace android diff --git a/services/core/jni/com_android_server_vibrator_VibratorController.cpp b/services/core/jni/com_android_server_vibrator_VibratorController.cpp index 7ed37481f8cb..4e47984fa75c 100644 --- a/services/core/jni/com_android_server_vibrator_VibratorController.cpp +++ b/services/core/jni/com_android_server_vibrator_VibratorController.cpp @@ -77,7 +77,7 @@ static std::shared_ptr<vibrator::HalController> findVibrator(int32_t vibratorId) if (vibratorId < 0) { return std::move(std::make_unique<vibrator::HalController>()); } - vibrator::ManagerHalWrapper* manager = android_server_VibratorManagerService_getManager(); + vibrator::ManagerHalController* manager = android_server_VibratorManagerService_getManager(); if (manager == nullptr) { return nullptr; } diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index c5394f3aba69..34f604895721 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -39,7 +39,7 @@ int register_android_server_UsbMidiDevice(JNIEnv* env); int register_android_server_UsbHostManager(JNIEnv* env); int register_android_server_vr_VrManagerService(JNIEnv* env); int register_android_server_vibrator_VibratorController(JavaVM* vm, JNIEnv* env); -int register_android_server_VibratorManagerService(JNIEnv* env); +int register_android_server_VibratorManagerService(JavaVM* vm, JNIEnv* env); int register_android_server_location_GnssLocationProvider(JNIEnv* env); int register_android_server_devicepolicy_CryptoTestHelper(JNIEnv*); int register_android_server_tv_TvUinputBridge(JNIEnv* env); @@ -90,7 +90,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_UsbHostManager(env); register_android_server_vr_VrManagerService(env); register_android_server_vibrator_VibratorController(vm, env); - register_android_server_VibratorManagerService(env); + register_android_server_VibratorManagerService(vm, env); register_android_server_SystemServer(env); register_android_server_location_GnssLocationProvider(env); register_android_server_devicepolicy_CryptoTestHelper(env); diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 7a0cb8e5dead..31e2b645ac73 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -61,7 +61,7 @@ android_test { libs: [ "android.hardware.power-V1-java", "android.hardware.tv.cec-V1.0-java", - "android.hardware.vibrator-V1-java", + "android.hardware.vibrator-V2-java", "android.hidl.manager-V1.0-java", "android.test.mock", "android.test.base", diff --git a/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java index f7b24920f903..f0d7006633a2 100644 --- a/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/VibratorManagerServiceTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; @@ -32,6 +33,7 @@ import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -44,6 +46,7 @@ import android.content.pm.PackageManagerInternal; import android.hardware.input.IInputManager; import android.hardware.input.InputManager; import android.hardware.vibrator.IVibrator; +import android.hardware.vibrator.IVibratorManager; import android.media.AudioAttributes; import android.media.AudioManager; import android.os.CombinedVibrationEffect; @@ -204,7 +207,7 @@ public class VibratorManagerServiceTest { public void createService_initializesNativeManagerServiceAndVibrators() { mockVibrators(1, 2); createService(); - verify(mNativeWrapperMock).init(); + verify(mNativeWrapperMock).init(any()); assertTrue(mVibratorProviders.get(1).isInitialized()); assertTrue(mVibratorProviders.get(2).isInitialized()); } @@ -557,8 +560,6 @@ public class VibratorManagerServiceTest { @Test public void vibrate_withNativeCallbackTriggered_finishesVibration() throws Exception { mockVibrators(1); - mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS, - IVibrator.CAP_AMPLITUDE_CONTROL); mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK); VibratorManagerService service = createService(); // The native callback will be dispatched manually in this test. @@ -577,6 +578,139 @@ public class VibratorManagerServiceTest { assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); } + @Test + public void vibrate_withTriggerCallback_finishesVibration() throws Exception { + mockCapabilities(IVibratorManager.CAP_SYNC, IVibratorManager.CAP_PREPARE_COMPOSE); + mockVibrators(1, 2); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + VibratorManagerService service = createService(); + // The native callback will be dispatched manually in this test. + mTestLooper.stopAutoDispatchAndIgnoreExceptions(); + + ArgumentCaptor<VibratorManagerService.OnSyncedVibrationCompleteListener> listenerCaptor = + ArgumentCaptor.forClass( + VibratorManagerService.OnSyncedVibrationCompleteListener.class); + verify(mNativeWrapperMock).init(listenerCaptor.capture()); + + // Mock trigger callback on registered listener. + when(mNativeWrapperMock.prepareSynced(eq(new int[]{1, 2}))).thenReturn(true); + when(mNativeWrapperMock.triggerSynced(anyLong())).then(answer -> { + listenerCaptor.getValue().onComplete(answer.getArgument(0)); + return true; + }); + + VibrationEffect composed = VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 100) + .compose(); + CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced(composed); + + // Wait for vibration to start, it should finish right away with trigger callback. + vibrate(service, effect, ALARM_ATTRS); + + // VibrationThread will start this vibration async, so wait until callback is triggered. + assertTrue(waitUntil(s -> !listenerCaptor.getAllValues().isEmpty(), service, + TEST_TIMEOUT_MILLIS)); + + verify(mNativeWrapperMock).prepareSynced(eq(new int[]{1, 2})); + verify(mNativeWrapperMock).triggerSynced(anyLong()); + assertEquals(Arrays.asList(composed), mVibratorProviders.get(1).getEffects()); + assertEquals(Arrays.asList(composed), mVibratorProviders.get(2).getEffects()); + } + + @Test + public void vibrate_withMultipleVibratorsAndCapabilities_prepareAndTriggerCalled() + throws Exception { + mockCapabilities(IVibratorManager.CAP_SYNC, IVibratorManager.CAP_PREPARE_PERFORM, + IVibratorManager.CAP_PREPARE_COMPOSE, IVibratorManager.CAP_MIXED_TRIGGER_PERFORM, + IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE); + mockVibrators(1, 2); + when(mNativeWrapperMock.prepareSynced(eq(new int[]{1, 2}))).thenReturn(true); + when(mNativeWrapperMock.triggerSynced(anyLong())).thenReturn(true); + FakeVibratorControllerProvider fakeVibrator1 = mVibratorProviders.get(1); + fakeVibrator1.setSupportedEffects(VibrationEffect.EFFECT_CLICK); + mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + VibratorManagerService service = createService(); + + CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced() + .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) + .addVibrator(2, VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .compose()) + .combine(); + vibrate(service, effect, ALARM_ATTRS); + assertTrue(waitUntil(s -> !fakeVibrator1.getEffects().isEmpty(), service, + TEST_TIMEOUT_MILLIS)); + + verify(mNativeWrapperMock).prepareSynced(eq(new int[]{1, 2})); + verify(mNativeWrapperMock).triggerSynced(anyLong()); + verify(mNativeWrapperMock).cancelSynced(); // Trigger on service creation only. + } + + @Test + public void vibrate_withMultipleVibratorsWithoutCapabilities_skipPrepareAndTrigger() + throws Exception { + // Missing CAP_MIXED_TRIGGER_ON and CAP_MIXED_TRIGGER_PERFORM. + mockCapabilities(IVibratorManager.CAP_SYNC, IVibratorManager.CAP_PREPARE_ON, + IVibratorManager.CAP_PREPARE_PERFORM); + mockVibrators(1, 2); + FakeVibratorControllerProvider fakeVibrator1 = mVibratorProviders.get(1); + fakeVibrator1.setSupportedEffects(VibrationEffect.EFFECT_CLICK); + VibratorManagerService service = createService(); + + CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced() + .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) + .addVibrator(2, VibrationEffect.createOneShot(10, 100)) + .combine(); + vibrate(service, effect, ALARM_ATTRS); + assertTrue(waitUntil(s -> !fakeVibrator1.getEffects().isEmpty(), service, + TEST_TIMEOUT_MILLIS)); + + verify(mNativeWrapperMock, never()).prepareSynced(any()); + verify(mNativeWrapperMock, never()).triggerSynced(anyLong()); + verify(mNativeWrapperMock).cancelSynced(); // Trigger on service creation only. + } + + @Test + public void vibrate_withMultipleVibratorsPrepareFailed_skipTrigger() throws Exception { + mockCapabilities(IVibratorManager.CAP_SYNC, IVibratorManager.CAP_PREPARE_ON); + mockVibrators(1, 2); + when(mNativeWrapperMock.prepareSynced(any())).thenReturn(false); + VibratorManagerService service = createService(); + + CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced() + .addVibrator(1, VibrationEffect.createOneShot(10, 50)) + .addVibrator(2, VibrationEffect.createOneShot(10, 100)) + .combine(); + vibrate(service, effect, ALARM_ATTRS); + assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getEffects().isEmpty(), service, + TEST_TIMEOUT_MILLIS)); + + verify(mNativeWrapperMock).prepareSynced(eq(new int[]{1, 2})); + verify(mNativeWrapperMock, never()).triggerSynced(anyLong()); + verify(mNativeWrapperMock).cancelSynced(); // Trigger on service creation only. + } + + @Test + public void vibrate_withMultipleVibratorsTriggerFailed_cancelPreparedSynced() throws Exception { + mockCapabilities(IVibratorManager.CAP_SYNC, IVibratorManager.CAP_PREPARE_ON); + mockVibrators(1, 2); + when(mNativeWrapperMock.prepareSynced(eq(new int[]{1, 2}))).thenReturn(true); + when(mNativeWrapperMock.triggerSynced(anyLong())).thenReturn(false); + VibratorManagerService service = createService(); + + CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced() + .addVibrator(1, VibrationEffect.createOneShot(10, 50)) + .addVibrator(2, VibrationEffect.createOneShot(10, 100)) + .combine(); + vibrate(service, effect, ALARM_ATTRS); + assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getEffects().isEmpty(), service, + TEST_TIMEOUT_MILLIS)); + + verify(mNativeWrapperMock).prepareSynced(eq(new int[]{1, 2})); + verify(mNativeWrapperMock).triggerSynced(anyLong()); + verify(mNativeWrapperMock, times(2)).cancelSynced(); // Trigger on service creation too. + } @Test public void vibrate_withIntensitySettings_appliesSettingsToScaleVibrations() throws Exception { @@ -682,6 +816,11 @@ public class VibratorManagerServiceTest { assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); } + private void mockCapabilities(long... capabilities) { + when(mNativeWrapperMock.getCapabilities()).thenReturn( + Arrays.stream(capabilities).reduce(0, (a, b) -> a | b)); + } + private void mockVibrators(int... vibratorIds) { for (int vibratorId : vibratorIds) { mVibratorProviders.put(vibratorId, diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java index 7d208799ee88..3ff8e76a3707 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java @@ -19,6 +19,7 @@ package com.android.server.vibrator; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; @@ -27,8 +28,10 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.hardware.vibrator.IVibrator; +import android.hardware.vibrator.IVibratorManager; import android.os.CombinedVibrationEffect; import android.os.IBinder; import android.os.PowerManager; @@ -388,6 +391,20 @@ public class VibrationThreadTest { } @Test + public void vibrate_singleVibrator_skipsSyncedCallbacks() { + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + + long vibrationId = 1; + waitForCompletion(startThreadAndDispatcher(vibrationId++, + VibrationEffect.createOneShot(10, 100))); + + verify(mThreadCallbacks).onVibrationEnded(anyLong(), eq(Vibration.Status.FINISHED)); + verify(mThreadCallbacks, never()).prepareSyncedVibration(anyLong(), any()); + verify(mThreadCallbacks, never()).triggerSyncedVibration(anyLong()); + verify(mThreadCallbacks, never()).cancelSyncedVibration(); + } + + @Test public void vibrate_multipleExistingAndMissingVibrators_vibratesOnlyExistingOnes() throws Exception { mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_TICK); @@ -484,8 +501,7 @@ public class VibrationThreadTest { } @Test - public void vibrate_multipleSequential_runsVibrationInOrderWithDelays() - throws Exception { + public void vibrate_multipleSequential_runsVibrationInOrderWithDelays() throws Exception { mockVibrators(1, 2, 3); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); @@ -529,6 +545,126 @@ public class VibrationThreadTest { } @Test + public void vibrate_multipleSyncedCallbackTriggered_finishSteps() throws Exception { + int[] vibratorIds = new int[]{1, 2}; + long vibrationId = 1; + mockVibrators(vibratorIds); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + when(mThreadCallbacks.prepareSyncedVibration(anyLong(), eq(vibratorIds))).thenReturn(true); + when(mThreadCallbacks.triggerSyncedVibration(eq(vibrationId))).thenReturn(true); + + VibrationEffect composed = VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 100) + .compose(); + CombinedVibrationEffect effect = CombinedVibrationEffect.createSynced(composed); + VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); + + assertTrue(waitUntil(t -> !mVibratorProviders.get(1).getEffects().isEmpty() + && !mVibratorProviders.get(2).getEffects().isEmpty(), thread, TEST_TIMEOUT_MILLIS)); + thread.syncedVibrationComplete(); + waitForCompletion(thread); + + long expectedCap = IVibratorManager.CAP_SYNC | IVibratorManager.CAP_PREPARE_COMPOSE; + verify(mThreadCallbacks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds)); + verify(mThreadCallbacks).triggerSyncedVibration(eq(vibrationId)); + verify(mThreadCallbacks, never()).cancelSyncedVibration(); + verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED)); + + assertEquals(Arrays.asList(composed), mVibratorProviders.get(1).getEffects()); + assertEquals(Arrays.asList(composed), mVibratorProviders.get(2).getEffects()); + } + + @Test + public void vibrate_multipleSynced_callsPrepareAndTriggerCallbacks() { + int[] vibratorIds = new int[]{1, 2, 3, 4}; + mockVibrators(vibratorIds); + mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK); + mVibratorProviders.get(4).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); + when(mThreadCallbacks.prepareSyncedVibration(anyLong(), any())).thenReturn(true); + when(mThreadCallbacks.triggerSyncedVibration(anyLong())).thenReturn(true); + + long vibrationId = 1; + VibrationEffect composed = VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .compose(); + CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced() + .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) + .addVibrator(2, VibrationEffect.createOneShot(10, 100)) + .addVibrator(3, VibrationEffect.createWaveform(new long[]{10}, new int[]{100}, -1)) + .addVibrator(4, composed) + .combine(); + VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); + waitForCompletion(thread); + + long expectedCap = IVibratorManager.CAP_SYNC + | IVibratorManager.CAP_PREPARE_ON + | IVibratorManager.CAP_PREPARE_PERFORM + | IVibratorManager.CAP_PREPARE_COMPOSE + | IVibratorManager.CAP_MIXED_TRIGGER_ON + | IVibratorManager.CAP_MIXED_TRIGGER_PERFORM + | IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE; + verify(mThreadCallbacks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds)); + verify(mThreadCallbacks).triggerSyncedVibration(eq(vibrationId)); + verify(mThreadCallbacks, never()).cancelSyncedVibration(); + verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED)); + } + + @Test + public void vibrate_multipleSyncedPrepareFailed_skipTriggerStepAndVibrates() { + int[] vibratorIds = new int[]{1, 2}; + mockVibrators(vibratorIds); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + when(mThreadCallbacks.prepareSyncedVibration(anyLong(), any())).thenReturn(false); + + long vibrationId = 1; + CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced() + .addVibrator(1, VibrationEffect.createOneShot(10, 100)) + .addVibrator(2, VibrationEffect.createWaveform(new long[]{5}, new int[]{200}, -1)) + .combine(); + VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); + waitForCompletion(thread); + + long expectedCap = IVibratorManager.CAP_SYNC | IVibratorManager.CAP_PREPARE_ON; + verify(mThreadCallbacks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds)); + verify(mThreadCallbacks, never()).triggerSyncedVibration(eq(vibrationId)); + verify(mThreadCallbacks, never()).cancelSyncedVibration(); + + assertEquals(Arrays.asList(expectedOneShot(10)), mVibratorProviders.get(1).getEffects()); + assertEquals(Arrays.asList(100), mVibratorProviders.get(1).getAmplitudes()); + assertEquals(Arrays.asList(expectedOneShot(5)), mVibratorProviders.get(2).getEffects()); + assertEquals(Arrays.asList(200), mVibratorProviders.get(2).getAmplitudes()); + } + + @Test + public void vibrate_multipleSyncedTriggerFailed_cancelPreparedVibrationAndSkipSetAmplitude() { + int[] vibratorIds = new int[]{1, 2}; + mockVibrators(vibratorIds); + mVibratorProviders.get(2).setSupportedEffects(VibrationEffect.EFFECT_CLICK); + when(mThreadCallbacks.prepareSyncedVibration(anyLong(), any())).thenReturn(true); + when(mThreadCallbacks.triggerSyncedVibration(anyLong())).thenReturn(false); + + long vibrationId = 1; + CombinedVibrationEffect effect = CombinedVibrationEffect.startSynced() + .addVibrator(1, VibrationEffect.createOneShot(10, 100)) + .addVibrator(2, VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) + .combine(); + VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); + waitForCompletion(thread); + + long expectedCap = IVibratorManager.CAP_SYNC + | IVibratorManager.CAP_PREPARE_ON + | IVibratorManager.CAP_PREPARE_PERFORM + | IVibratorManager.CAP_MIXED_TRIGGER_ON + | IVibratorManager.CAP_MIXED_TRIGGER_PERFORM; + verify(mThreadCallbacks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds)); + verify(mThreadCallbacks).triggerSyncedVibration(eq(vibrationId)); + verify(mThreadCallbacks).cancelSyncedVibration(); + assertTrue(mVibratorProviders.get(1).getAmplitudes().isEmpty()); + } + + @Test public void vibrate_multipleWaveforms_playsWaveformsInParallel() throws Exception { mockVibrators(1, 2, 3); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); @@ -623,6 +759,7 @@ public class VibrationThreadTest { assertTrue(waitUntil(t -> t.getVibrators().get(2).isVibrating(), vibrationThread, TEST_TIMEOUT_MILLIS)); + assertTrue(vibrationThread.isAlive()); // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. |