diff options
26 files changed, 2305 insertions, 180 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index e4d55896744c..264599a493fe 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -388,6 +388,7 @@ package android { field public static final String START_CROSS_PROFILE_ACTIVITIES = "android.permission.START_CROSS_PROFILE_ACTIVITIES"; field public static final String START_REVIEW_PERMISSION_DECISIONS = "android.permission.START_REVIEW_PERMISSION_DECISIONS"; field public static final String START_TASKS_FROM_RECENTS = "android.permission.START_TASKS_FROM_RECENTS"; + field @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public static final String START_VIBRATION_SESSIONS = "android.permission.START_VIBRATION_SESSIONS"; field public static final String STATUS_BAR_SERVICE = "android.permission.STATUS_BAR_SERVICE"; field public static final String STOP_APP_SWITCHES = "android.permission.STOP_APP_SWITCHES"; field public static final String SUBSTITUTE_NOTIFICATION_APP_NAME = "android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"; @@ -11642,8 +11643,11 @@ package android.os { public abstract class Vibrator { method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void addVibratorStateListener(@NonNull android.os.Vibrator.OnVibratorStateChangedListener); method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void addVibratorStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.os.Vibrator.OnVibratorStateChangedListener); + method @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public boolean areVendorEffectsSupported(); + method @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public boolean areVendorSessionsSupported(); method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public boolean isVibrating(); method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void removeVibratorStateListener(@NonNull android.os.Vibrator.OnVibratorStateChangedListener); + method @FlaggedApi("android.os.vibrator.vendor_vibration_effects") @RequiresPermission(allOf={android.Manifest.permission.VIBRATE, android.Manifest.permission.VIBRATE_VENDOR_EFFECTS, android.Manifest.permission.START_VIBRATION_SESSIONS}) public void startVendorSession(@NonNull android.os.VibrationAttributes, @Nullable String, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.vibrator.VendorVibrationSession.Callback); } public static interface Vibrator.OnVibratorStateChangedListener { @@ -11795,6 +11799,28 @@ package android.os.storage { } +package android.os.vibrator { + + @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public final class VendorVibrationSession implements java.lang.AutoCloseable { + method public void cancel(); + method public void close(); + method @RequiresPermission(android.Manifest.permission.VIBRATE) public void vibrate(@NonNull android.os.VibrationEffect, @Nullable String); + field public static final int STATUS_CANCELED = 4; // 0x4 + field public static final int STATUS_IGNORED = 2; // 0x2 + field public static final int STATUS_SUCCESS = 1; // 0x1 + field public static final int STATUS_UNKNOWN = 0; // 0x0 + field public static final int STATUS_UNKNOWN_ERROR = 5; // 0x5 + field public static final int STATUS_UNSUPPORTED = 3; // 0x3 + } + + public static interface VendorVibrationSession.Callback { + method public void onFinished(int); + method public void onFinishing(); + method public void onStarted(@NonNull android.os.vibrator.VendorVibrationSession); + } + +} + package android.os.vibrator.persistence { @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class ParsedVibration { diff --git a/core/java/android/os/IVibratorManagerService.aidl b/core/java/android/os/IVibratorManagerService.aidl index 6aa9852314df..ecb5e6f1b29a 100644 --- a/core/java/android/os/IVibratorManagerService.aidl +++ b/core/java/android/os/IVibratorManagerService.aidl @@ -17,13 +17,17 @@ package android.os; import android.os.CombinedVibration; +import android.os.ICancellationSignal; import android.os.IVibratorStateListener; import android.os.VibrationAttributes; import android.os.VibratorInfo; +import android.os.vibrator.IVibrationSession; +import android.os.vibrator.IVibrationSessionCallback; /** {@hide} */ interface IVibratorManagerService { int[] getVibratorIds(); + int getCapabilities(); VibratorInfo getVibratorInfo(int vibratorId); @EnforcePermission("ACCESS_VIBRATOR_STATE") boolean isVibrating(int vibratorId); @@ -50,4 +54,9 @@ interface IVibratorManagerService { oneway void performHapticFeedbackForInputDevice(int uid, int deviceId, String opPkg, int constant, int inputDeviceId, int inputSource, String reason, int flags, int privFlags); + + @EnforcePermission(allOf={"VIBRATE", "VIBRATE_VENDOR_EFFECTS", "START_VIBRATION_SESSIONS"}) + ICancellationSignal startVendorVibrationSession(int uid, int deviceId, String opPkg, + in int[] vibratorIds, in VibrationAttributes attributes, String reason, + in IVibrationSessionCallback callback); } diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java index 011a3ee91ada..c3cddf32f063 100644 --- a/core/java/android/os/SystemVibrator.java +++ b/core/java/android/os/SystemVibrator.java @@ -18,8 +18,11 @@ package android.os; import android.annotation.CallbackExecutor; import android.annotation.NonNull; +import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; +import android.hardware.vibrator.IVibratorManager; +import android.os.vibrator.VendorVibrationSession; import android.os.vibrator.VibratorInfoFactory; import android.util.ArrayMap; import android.util.Log; @@ -53,6 +56,7 @@ public class SystemVibrator extends Vibrator { private final Object mLock = new Object(); @GuardedBy("mLock") private VibratorInfo mVibratorInfo; + private int[] mVibratorIds; @UnsupportedAppUsage public SystemVibrator(Context context) { @@ -71,7 +75,11 @@ public class SystemVibrator extends Vibrator { Log.w(TAG, "Failed to retrieve vibrator info; no vibrator manager."); return VibratorInfo.EMPTY_VIBRATOR_INFO; } - int[] vibratorIds = mVibratorManager.getVibratorIds(); + int[] vibratorIds = getVibratorIds(); + if (vibratorIds == null) { + Log.w(TAG, "Failed to retrieve vibrator info; error retrieving vibrator ids."); + return VibratorInfo.EMPTY_VIBRATOR_INFO; + } if (vibratorIds.length == 0) { // It is known that the device has no vibrator, so cache and return info that // reflects the lack of support for effects/primitives. @@ -95,20 +103,22 @@ public class SystemVibrator extends Vibrator { @Override public boolean hasVibrator() { - if (mVibratorManager == null) { + int[] vibratorIds = getVibratorIds(); + if (vibratorIds == null) { Log.w(TAG, "Failed to check if vibrator exists; no vibrator manager."); return false; } - return mVibratorManager.getVibratorIds().length > 0; + return vibratorIds.length > 0; } @Override public boolean isVibrating() { - if (mVibratorManager == null) { + int[] vibratorIds = getVibratorIds(); + if (vibratorIds == null) { Log.w(TAG, "Failed to vibrate; no vibrator manager."); return false; } - for (int vibratorId : mVibratorManager.getVibratorIds()) { + for (int vibratorId : vibratorIds) { if (mVibratorManager.getVibrator(vibratorId).isVibrating()) { return true; } @@ -136,6 +146,11 @@ public class SystemVibrator extends Vibrator { Log.w(TAG, "Failed to add vibrate state listener; no vibrator manager."); return; } + int[] vibratorIds = getVibratorIds(); + if (vibratorIds == null) { + Log.w(TAG, "Failed to add vibrate state listener; error retrieving vibrator ids."); + return; + } MultiVibratorStateListener delegate = null; try { synchronized (mRegisteredListeners) { @@ -145,7 +160,7 @@ public class SystemVibrator extends Vibrator { return; } delegate = new MultiVibratorStateListener(executor, listener); - delegate.register(mVibratorManager); + delegate.register(mVibratorManager, vibratorIds); mRegisteredListeners.put(listener, delegate); delegate = null; } @@ -184,6 +199,11 @@ public class SystemVibrator extends Vibrator { } @Override + public boolean areVendorSessionsSupported() { + return mVibratorManager.hasCapabilities(IVibratorManager.CAP_START_SESSIONS); + } + + @Override public boolean setAlwaysOnEffect(int uid, String opPkg, int alwaysOnId, VibrationEffect effect, VibrationAttributes attrs) { if (mVibratorManager == null) { @@ -243,6 +263,41 @@ public class SystemVibrator extends Vibrator { mVibratorManager.cancel(usageFilter); } + @Override + public void startVendorSession(@NonNull VibrationAttributes attrs, @Nullable String reason, + @Nullable CancellationSignal cancellationSignal, @NonNull Executor executor, + @NonNull VendorVibrationSession.Callback callback) { + if (mVibratorManager == null) { + Log.w(TAG, "Failed to start vibration session; no vibrator manager."); + executor.execute( + () -> callback.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR)); + return; + } + int[] vibratorIds = getVibratorIds(); + if (vibratorIds == null) { + Log.w(TAG, "Failed to start vibration session; error retrieving vibrator ids."); + executor.execute( + () -> callback.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR)); + return; + } + mVibratorManager.startVendorSession(vibratorIds, attrs, reason, cancellationSignal, + executor, callback); + } + + @Nullable + private int[] getVibratorIds() { + synchronized (mLock) { + if (mVibratorIds != null) { + return mVibratorIds; + } + if (mVibratorManager == null) { + Log.w(TAG, "Failed to retrieve vibrator ids; no vibrator manager."); + return null; + } + return mVibratorIds = mVibratorManager.getVibratorIds(); + } + } + /** * Tries to unregister individual {@link android.os.Vibrator.OnVibratorStateChangedListener} * that were left registered to vibrators after failures to register them to all vibrators. @@ -319,8 +374,7 @@ public class SystemVibrator extends Vibrator { } /** Registers a listener to all individual vibrators in {@link VibratorManager}. */ - public void register(VibratorManager vibratorManager) { - int[] vibratorIds = vibratorManager.getVibratorIds(); + public void register(VibratorManager vibratorManager, @NonNull int[] vibratorIds) { synchronized (mLock) { for (int i = 0; i < vibratorIds.length; i++) { int vibratorId = vibratorIds[i]; diff --git a/core/java/android/os/SystemVibratorManager.java b/core/java/android/os/SystemVibratorManager.java index a5697fb0e8a8..f9935d2870b0 100644 --- a/core/java/android/os/SystemVibratorManager.java +++ b/core/java/android/os/SystemVibratorManager.java @@ -22,6 +22,10 @@ import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.hardware.vibrator.IVibratorManager; +import android.os.vibrator.IVibrationSession; +import android.os.vibrator.IVibrationSessionCallback; +import android.os.vibrator.VendorVibrationSession; import android.util.ArrayMap; import android.util.Log; import android.util.SparseArray; @@ -47,6 +51,8 @@ public class SystemVibratorManager extends VibratorManager { @GuardedBy("mLock") private int[] mVibratorIds; @GuardedBy("mLock") + private int mCapabilities; + @GuardedBy("mLock") private final SparseArray<Vibrator> mVibrators = new SparseArray<>(); @GuardedBy("mLock") @@ -84,6 +90,11 @@ public class SystemVibratorManager extends VibratorManager { } } + @Override + public boolean hasCapabilities(int capabilities) { + return (getCapabilities() & capabilities) == capabilities; + } + @NonNull @Override public Vibrator getVibrator(int vibratorId) { @@ -173,7 +184,7 @@ public class SystemVibratorManager extends VibratorManager { int inputSource, String reason, int flags, int privFlags) { if (mService == null) { Log.w(TAG, "Failed to perform haptic feedback for input device;" - + " no vibrator manager service."); + + " no vibrator manager service."); return; } Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice"); @@ -197,6 +208,50 @@ public class SystemVibratorManager extends VibratorManager { cancelVibration(usageFilter); } + @Override + public void startVendorSession(@NonNull int[] vibratorIds, @NonNull VibrationAttributes attrs, + @Nullable String reason, @Nullable CancellationSignal cancellationSignal, + @NonNull Executor executor, @NonNull VendorVibrationSession.Callback callback) { + Objects.requireNonNull(vibratorIds); + VendorVibrationSessionCallbackDelegate callbackDelegate = + new VendorVibrationSessionCallbackDelegate(executor, callback); + if (mService == null) { + Log.w(TAG, "Failed to start vibration session; no vibrator manager service."); + callbackDelegate.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR); + return; + } + try { + ICancellationSignal remoteCancellationSignal = mService.startVendorVibrationSession( + mUid, mContext.getDeviceId(), mPackageName, vibratorIds, attrs, reason, + callbackDelegate); + if (cancellationSignal != null) { + cancellationSignal.setRemote(remoteCancellationSignal); + } + } catch (RemoteException e) { + Log.w(TAG, "Failed to start vibration session.", e); + callbackDelegate.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR); + } + } + + private int getCapabilities() { + synchronized (mLock) { + if (mCapabilities != 0) { + return mCapabilities; + } + try { + if (mService == null) { + Log.w(TAG, "Failed to retrieve vibrator manager capabilities;" + + " no vibrator manager service."); + } else { + return mCapabilities = mService.getCapabilities(); + } + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + return 0; + } + } + private void cancelVibration(int usageFilter) { if (mService == null) { Log.w(TAG, "Failed to cancel vibration; no vibrator manager service."); @@ -228,12 +283,45 @@ public class SystemVibratorManager extends VibratorManager { } } + /** Callback for vendor vibration sessions. */ + private static class VendorVibrationSessionCallbackDelegate extends + IVibrationSessionCallback.Stub { + private final Executor mExecutor; + private final VendorVibrationSession.Callback mCallback; + + VendorVibrationSessionCallbackDelegate( + @NonNull Executor executor, + @NonNull VendorVibrationSession.Callback callback) { + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + mExecutor = executor; + mCallback = callback; + } + + @Override + public void onStarted(IVibrationSession session) { + mExecutor.execute(() -> mCallback.onStarted(new VendorVibrationSession(session))); + } + + @Override + public void onFinishing() { + mExecutor.execute(() -> mCallback.onFinishing()); + } + + @Override + public void onFinished(int status) { + mExecutor.execute(() -> mCallback.onFinished(status)); + } + } + /** Controls vibrations on a single vibrator. */ private final class SingleVibrator extends Vibrator { private final VibratorInfo mVibratorInfo; + private final int[] mVibratorId; SingleVibrator(@NonNull VibratorInfo vibratorInfo) { mVibratorInfo = vibratorInfo; + mVibratorId = new int[]{mVibratorInfo.getId()}; } @Override @@ -252,6 +340,11 @@ public class SystemVibratorManager extends VibratorManager { } @Override + public boolean areVendorSessionsSupported() { + return SystemVibratorManager.this.hasCapabilities(IVibratorManager.CAP_START_SESSIONS); + } + + @Override public boolean setAlwaysOnEffect(int uid, String opPkg, int alwaysOnId, @Nullable VibrationEffect effect, @Nullable VibrationAttributes attrs) { CombinedVibration combined = CombinedVibration.startParallel() @@ -369,5 +462,13 @@ public class SystemVibratorManager extends VibratorManager { } } } + + @Override + public void startVendorSession(@NonNull VibrationAttributes attrs, String reason, + @Nullable CancellationSignal cancellationSignal, @NonNull Executor executor, + @NonNull VendorVibrationSession.Callback callback) { + SystemVibratorManager.this.startVendorSession(mVibratorId, attrs, reason, + cancellationSignal, executor, callback); + } } } diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java index c4c4580bf0a8..53f8a9267499 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -33,6 +33,7 @@ import android.content.res.Resources; import android.hardware.vibrator.IVibrator; import android.media.AudioAttributes; import android.os.vibrator.Flags; +import android.os.vibrator.VendorVibrationSession; import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibratorFrequencyProfile; import android.os.vibrator.VibratorFrequencyProfileLegacy; @@ -247,6 +248,34 @@ public abstract class Vibrator { } /** + * Check whether the vibrator has support for vendor-specific effects. + * + * <p>Vendor vibration effects can be created via {@link VibrationEffect#createVendorEffect}. + * + * @return True if the hardware can play vendor-specific vibration effects, false otherwise. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public boolean areVendorEffectsSupported() { + return getInfo().hasCapability(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); + } + + /** + * Check whether the vibrator has support for vendor-specific vibration sessions. + * + * <p>Vendor vibration sessions can be started via {@link #startVendorSession}. + * + * @return True if the hardware can play vendor-specific vibration sessions, false otherwise. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public boolean areVendorSessionsSupported() { + return false; + } + + /** * Check whether the vibrator can be controlled by an external service with the * {@link IExternalVibratorService}. * @@ -922,4 +951,44 @@ public abstract class Vibrator { @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void removeVibratorStateListener(@NonNull OnVibratorStateChangedListener listener) { } + + /** + * Starts a vibration session in this vibrator. + * + * <p>The session will start asynchronously once the vibrator control can be acquired. Once it's + * started the {@link VendorVibrationSession} will be provided to the callback. This session + * should be used to play vibrations until the session is ended or canceled. + * + * <p>The vendor app will have exclusive control over the vibrator during this session. This + * control can be revoked by the vibrator service, which will be notified to the same session + * callback with the {@link VendorVibrationSession#STATUS_CANCELED}. + * + * <p>The {@link VibrationAttributes} will be used to decide the priority of the vendor + * vibrations that will be performed in this session. All vibrations within this session will + * apply the same attributes. + * + * @param attrs The {@link VibrationAttributes} corresponding to the vibrations that will be + * performed in the session. This will be used to decide the priority of this + * session against other system vibrations. + * @param reason The description for this session, used for debugging purposes. + * @param cancellationSignal A signal to cancel the session before it starts. + * @param executor The executor for the session callbacks. + * @param callback The {@link VendorVibrationSession.Callback} for the started session. + * + * @see VendorVibrationSession + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @RequiresPermission(allOf = { + android.Manifest.permission.VIBRATE, + android.Manifest.permission.VIBRATE_VENDOR_EFFECTS, + android.Manifest.permission.START_VIBRATION_SESSIONS, + }) + public void startVendorSession(@NonNull VibrationAttributes attrs, @Nullable String reason, + @Nullable CancellationSignal cancellationSignal, @NonNull Executor executor, + @NonNull VendorVibrationSession.Callback callback) { + Log.w(TAG, "startVendorSession is not supported"); + executor.execute(() -> callback.onFinished(VendorVibrationSession.STATUS_UNSUPPORTED)); + } } diff --git a/core/java/android/os/VibratorManager.java b/core/java/android/os/VibratorManager.java index 0428876891f9..0072bc22ad8f 100644 --- a/core/java/android/os/VibratorManager.java +++ b/core/java/android/os/VibratorManager.java @@ -22,9 +22,12 @@ import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.app.ActivityThread; import android.content.Context; +import android.os.vibrator.VendorVibrationSession; import android.util.Log; import android.view.HapticFeedbackConstants; +import java.util.concurrent.Executor; + /** * Provides access to all vibrators from the device, as well as the ability to run them * in a synchronized fashion. @@ -62,6 +65,14 @@ public abstract class VibratorManager { public abstract int[] getVibratorIds(); /** + * Return true if the vibrator manager has all capabilities, false otherwise. + * @hide + */ + public boolean hasCapabilities(int capabilities) { + return false; + } + + /** * Retrieve a single vibrator by id. * * @param vibratorId The id of the vibrator to be retrieved. @@ -190,4 +201,30 @@ public abstract class VibratorManager { */ @RequiresPermission(android.Manifest.permission.VIBRATE) public abstract void cancel(int usageFilter); + + + /** + * Starts a vibration session on given vibrators. + * + * @param vibratorIds The vibrators that will be controlled by this session. + * @param attrs The {@link VibrationAttributes} corresponding to the vibrations that will + * be performed in the session. This will be used to decide the priority of + * this session against other system vibrations. + * @param reason The description for this session, used for debugging purposes. + * @param cancellationSignal A signal to cancel the session before it starts. + * @param executor The executor for the session callbacks. + * @param callback The {@link VendorVibrationSession.Callback} for the started session. + * @see Vibrator#startVendorSession + * @hide + */ + @RequiresPermission(allOf = { + android.Manifest.permission.VIBRATE, + android.Manifest.permission.VIBRATE_VENDOR_EFFECTS, + android.Manifest.permission.START_VIBRATION_SESSIONS, + }) + public void startVendorSession(@NonNull int[] vibratorIds, @NonNull VibrationAttributes attrs, + @Nullable String reason, @Nullable CancellationSignal cancellationSignal, + @NonNull Executor executor, @NonNull VendorVibrationSession.Callback callback) { + Log.w(TAG, "startVendorSession is not supported"); + } } diff --git a/core/java/android/os/vibrator/IVibrationSession.aidl b/core/java/android/os/vibrator/IVibrationSession.aidl new file mode 100644 index 000000000000..e8295492665d --- /dev/null +++ b/core/java/android/os/vibrator/IVibrationSession.aidl @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2024, 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.vibrator; + +import android.os.CombinedVibration; + +/** + * The communication channel by which an app control the system vibrators. + * + * In order to synchronize the places where vibrations might be controlled we provide this interface + * so the vibrator subsystem has a chance to: + * + * 1) Decide whether the current session should have the vibrator control. + * 2) Stop any on-going session for a new session/vibration, based on current system policy. + * {@hide} + */ +interface IVibrationSession { + const int STATUS_UNKNOWN = 0; + const int STATUS_SUCCESS = 1; + const int STATUS_IGNORED = 2; + const int STATUS_UNSUPPORTED = 3; + const int STATUS_CANCELED = 4; + const int STATUS_UNKNOWN_ERROR = 5; + + /** + * A method called to start a vibration within this session. This will fail if the session + * is finishing or was canceled. + */ + void vibrate(in CombinedVibration vibration, String reason); + + /** + * A method called by the app to stop this session gracefully. The vibrator will complete any + * ongoing vibration before the session is ended. + */ + void finishSession(); + + /** + * A method called by the app to stop this session immediatelly by interrupting any ongoing + * vibration. + */ + void cancelSession(); +} diff --git a/core/java/android/os/vibrator/IVibrationSessionCallback.aidl b/core/java/android/os/vibrator/IVibrationSessionCallback.aidl new file mode 100644 index 000000000000..36c3695a1bfe --- /dev/null +++ b/core/java/android/os/vibrator/IVibrationSessionCallback.aidl @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2024, 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.vibrator; + +import android.os.vibrator.IVibrationSession; + +/** + * Callback for vibration session state. + * {@hide} + */ +oneway interface IVibrationSessionCallback { + + /** + * A method called by the service after a vibration session has successfully started. After this + * is called the app has control over the vibrator through this given session. + */ + void onStarted(in IVibrationSession session); + + /** + * A method called by the service to indicate the session is ending and should no longer receive + * vibration requests. + */ + void onFinishing(); + + /** + * A method called by the service after the session has ended. This might be triggered by the + * app or the service. The status code indicates the end reason. + */ + void onFinished(int status); +} diff --git a/core/java/android/os/vibrator/VendorVibrationSession.java b/core/java/android/os/vibrator/VendorVibrationSession.java new file mode 100644 index 000000000000..c23f2ed1a303 --- /dev/null +++ b/core/java/android/os/vibrator/VendorVibrationSession.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2024 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.vibrator; + +import static android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.os.CombinedVibration; +import android.os.RemoteException; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * A vendor session that temporarily gains control over the system vibrators. + * + * <p>Vibration effects can be played by the vibrator in a vendor session via {@link #vibrate}. The + * effects will be forwarded to the vibrator hardware immediately. Any concurrency support is + * defined and controlled by the vibrator hardware implementation. + * + * <p>The session should be ended by {@link #close()}, which will wait until the last vibration ends + * and the vibrator is released. The end of the session will be notified to the {@link Callback} + * provided when the session was created. + * + * <p>Any ongoing session can be immediately interrupted by the vendor app via {@link #cancel()}, + * including after {@link #close()} was called and the session is tearing down. A session can also + * be canceled by the vibrator service when it needs to regain control of the system vibrators. + * + * @see Vibrator#startVendorSession + * @hide + */ +@FlaggedApi(FLAG_VENDOR_VIBRATION_EFFECTS) +@SystemApi +public final class VendorVibrationSession implements AutoCloseable { + private static final String TAG = "VendorVibrationSession"; + + /** + * The session ended successfully. + */ + public static final int STATUS_SUCCESS = IVibrationSession.STATUS_SUCCESS; + + /** + * The session was ignored. + * + * <p>This might be caused by user settings, vibration policies or the device state that + * prevents the app from performing vibrations for the requested + * {@link android.os.VibrationAttributes}. + */ + public static final int STATUS_IGNORED = IVibrationSession.STATUS_IGNORED; + + /** + * The session is not supported. + * + * <p>The support for vendor vibration sessions can be checked via + * {@link Vibrator#areVendorSessionsSupported()}. + */ + public static final int STATUS_UNSUPPORTED = IVibrationSession.STATUS_UNSUPPORTED; + + /** + * The session was canceled. + * + * <p>This might be triggered by the app after a session starts via {@link #cancel()}, or it + * can be triggered by the platform before or after the session has started. + */ + public static final int STATUS_CANCELED = IVibrationSession.STATUS_CANCELED; + + /** + * The session status is unknown. + */ + public static final int STATUS_UNKNOWN = IVibrationSession.STATUS_UNKNOWN; + + /** + * The session failed with unknown error. + * + * <p>This can be caused by a failure to start a vibration session or after it has started, to + * indicate it has ended unexpectedly because of a system failure. + */ + public static final int STATUS_UNKNOWN_ERROR = IVibrationSession.STATUS_UNKNOWN_ERROR; + + /** @hide */ + @IntDef(prefix = { "STATUS_" }, value = { + STATUS_SUCCESS, + STATUS_IGNORED, + STATUS_UNSUPPORTED, + STATUS_CANCELED, + STATUS_UNKNOWN, + STATUS_UNKNOWN_ERROR, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Status{} + + private final IVibrationSession mSession; + + /** @hide */ + public VendorVibrationSession(@NonNull IVibrationSession session) { + Objects.requireNonNull(session); + mSession = session; + } + + /** + * Vibrate with a given effect. + * + * <p>The vibration will be sent to the vibrator hardware immediately, without waiting for any + * previous vibration completion. The vendor should control the concurrency behavior at the + * hardware level (e.g. queueing, mixing, interrupting). + * + * <p>If the provided effect is played by the vibrator service with controlled timings (e.g. + * effects created via {@link VibrationEffect#createWaveform}), then triggering a new vibration + * will cause the ongoing playback to be interrupted in favor of the new vibration. If the + * effect is broken down into multiple consecutive commands (e.g. large primitive compositions) + * then the hardware commands will be triggered in succession without waiting for the completion + * callback. + * + * <p>The vendor app is responsible for timing the session requests and the vibrator hardware + * implementation is free to handle concurrency with different policies. + * + * @param effect The {@link VibrationEffect} describing the vibration to be performed. + * @param reason The description for the vibration reason, for debugging purposes. + */ + @RequiresPermission(android.Manifest.permission.VIBRATE) + public void vibrate(@NonNull VibrationEffect effect, @Nullable String reason) { + try { + mSession.vibrate(CombinedVibration.createParallel(effect), reason); + } catch (RemoteException e) { + Log.w(TAG, "Failed to vibrate in a vendor vibration session.", e); + e.rethrowFromSystemServer(); + } + } + + /** + * Cancel ongoing session. + * + * <p>This will stop the vibration immediately and return the vibrator control to the + * platform. This can also be triggered after {@link #close()} to immediately release the + * vibrator. + * + * <p>This will trigger {@link VendorVibrationSession.Callback#onFinished} directly with + * {@link #STATUS_CANCELED}. + */ + public void cancel() { + try { + mSession.cancelSession(); + } catch (RemoteException e) { + Log.w(TAG, "Failed to cancel vendor vibration session.", e); + e.rethrowFromSystemServer(); + } + } + + /** + * End ongoing session gracefully. + * + * <p>This might continue the vibration while it's ramping down and wrapping up the session + * in the vibrator hardware. No more vibration commands can be sent through this session + * after this method is called. + * + * <p>This will trigger {@link VendorVibrationSession.Callback#onFinishing()}. + */ + @Override + public void close() { + try { + mSession.finishSession(); + } catch (RemoteException e) { + Log.w(TAG, "Failed to finish vendor vibration session.", e); + e.rethrowFromSystemServer(); + } + } + + /** + * Callbacks for {@link VendorVibrationSession} events. + * + * @see Vibrator#startVendorSession + * @see VendorVibrationSession + */ + public interface Callback { + + /** + * New session was successfully started. + * + * <p>The vendor app can interact with the vibrator using the + * {@link VendorVibrationSession} provided. + */ + void onStarted(@NonNull VendorVibrationSession session); + + /** + * The session is ending and finishing any pending vibrations. + * + * <p>This is only invoked after {@link #onStarted(VendorVibrationSession)}. It will be + * triggered by both {@link VendorVibrationSession#cancel()} and + * {@link VendorVibrationSession#close()}. This might also be triggered if the platform + * cancels the ongoing session. + * + * <p>Session vibrations might be still ongoing in the vibrator hardware but the app can + * no longer send commands through the session. A finishing session can still be immediately + * stopped via calls to {@link VendorVibrationSession.Callback#cancel()}. + */ + void onFinishing(); + + /** + * The session is finished. + * + * <p>The vibrator has finished any vibration and returned to the platform's control. This + * might be triggered by the vendor app or by the vibrator service. + * + * <p>If this is triggered before {@link #onStarted} then the session was finished before + * starting, either because it was cancelled or failed to start. If the session has already + * started then this will be triggered after {@link #onFinishing()} to indicate all session + * vibrations are complete and the vibrator is no longer under the session's control. + * + * @param status The session status. + */ + void onFinished(@VendorVibrationSession.Status int status); + } +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 5044a300cc8e..d1dd1a650f1b 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2622,13 +2622,22 @@ <!-- @SystemApi Allows access to perform vendor effects in the vibrator. <p>Protection level: signature - @FlaggedApi("android.os.vibrator.vendor_vibration_effects") + @FlaggedApi(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) @hide --> <permission android:name="android.permission.VIBRATE_VENDOR_EFFECTS" android:protectionLevel="signature|privileged" android:featureFlag="android.os.vibrator.vendor_vibration_effects" /> + <!-- @SystemApi Allows access to start a vendor vibration session. + <p>Protection level: signature + @FlaggedApi(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @hide + --> + <permission android:name="android.permission.START_VIBRATION_SESSIONS" + android:protectionLevel="signature|privileged" + android:featureFlag="android.os.vibrator.vendor_vibration_effects" /> + <!-- @SystemApi Allows access to the vibrator state. <p>Protection level: signature @hide diff --git a/core/tests/vibrator/src/android/os/VibratorTest.java b/core/tests/vibrator/src/android/os/VibratorTest.java index 6210a00a5940..09bfadbf56a4 100644 --- a/core/tests/vibrator/src/android/os/VibratorTest.java +++ b/core/tests/vibrator/src/android/os/VibratorTest.java @@ -110,8 +110,9 @@ public class VibratorTest { @Test public void onVibratorStateChanged_noVibrator_registersNoListenerToVibratorManager() { + int[] vibratorIds = new int[0]; VibratorManager mockVibratorManager = mock(VibratorManager.class); - when(mockVibratorManager.getVibratorIds()).thenReturn(new int[0]); + when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds); Vibrator.OnVibratorStateChangedListener mockListener = mock(Vibrator.OnVibratorStateChangedListener.class); @@ -119,7 +120,7 @@ public class VibratorTest { new SystemVibrator.MultiVibratorStateListener( mTestLooper.getNewExecutor(), mockListener); - multiVibratorListener.register(mockVibratorManager); + multiVibratorListener.register(mockVibratorManager, vibratorIds); // Never tries to register a listener to an individual vibrator. assertFalse(multiVibratorListener.hasRegisteredListeners()); @@ -128,8 +129,9 @@ public class VibratorTest { @Test public void onVibratorStateChanged_singleVibrator_forwardsAllCallbacks() { + int[] vibratorIds = new int[] { 1 }; VibratorManager mockVibratorManager = mock(VibratorManager.class); - when(mockVibratorManager.getVibratorIds()).thenReturn(new int[] { 1 }); + when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds); when(mockVibratorManager.getVibrator(anyInt())).thenReturn(NullVibrator.getInstance()); Vibrator.OnVibratorStateChangedListener mockListener = @@ -138,7 +140,7 @@ public class VibratorTest { new SystemVibrator.MultiVibratorStateListener( mTestLooper.getNewExecutor(), mockListener); - multiVibratorListener.register(mockVibratorManager); + multiVibratorListener.register(mockVibratorManager, vibratorIds); assertTrue(multiVibratorListener.hasRegisteredListeners()); multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false); @@ -156,8 +158,9 @@ public class VibratorTest { @Test public void onVibratorStateChanged_multipleVibrators_triggersOnlyWhenAllVibratorsInitialized() { + int[] vibratorIds = new int[] { 1, 2 }; VibratorManager mockVibratorManager = mock(VibratorManager.class); - when(mockVibratorManager.getVibratorIds()).thenReturn(new int[] { 1, 2 }); + when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds); when(mockVibratorManager.getVibrator(anyInt())).thenReturn(NullVibrator.getInstance()); Vibrator.OnVibratorStateChangedListener mockListener = @@ -166,7 +169,7 @@ public class VibratorTest { new SystemVibrator.MultiVibratorStateListener( mTestLooper.getNewExecutor(), mockListener); - multiVibratorListener.register(mockVibratorManager); + multiVibratorListener.register(mockVibratorManager, vibratorIds); assertTrue(multiVibratorListener.hasRegisteredListeners()); multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false); @@ -181,8 +184,9 @@ public class VibratorTest { @Test public void onVibratorStateChanged_multipleVibrators_stateChangeIsDeduped() { + int[] vibratorIds = new int[] { 1, 2 }; VibratorManager mockVibratorManager = mock(VibratorManager.class); - when(mockVibratorManager.getVibratorIds()).thenReturn(new int[] { 1, 2 }); + when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds); when(mockVibratorManager.getVibrator(anyInt())).thenReturn(NullVibrator.getInstance()); Vibrator.OnVibratorStateChangedListener mockListener = @@ -191,7 +195,7 @@ public class VibratorTest { new SystemVibrator.MultiVibratorStateListener( mTestLooper.getNewExecutor(), mockListener); - multiVibratorListener.register(mockVibratorManager); + multiVibratorListener.register(mockVibratorManager, vibratorIds); assertTrue(multiVibratorListener.hasRegisteredListeners()); multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false); // none diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 56e55df3f27c..7ced809d2a3a 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -402,6 +402,9 @@ applications that come with the platform <permission name="android.permission.SHOW_CUSTOMIZED_RESOLVER"/> <!-- Permission required for access VIBRATOR_STATE. --> <permission name="android.permission.ACCESS_VIBRATOR_STATE"/> + <!-- Permission required for vendor vibration effects and sessions. --> + <permission name="android.permission.VIBRATE_VENDOR_EFFECTS"/> + <permission name="android.permission.START_VIBRATION_SESSIONS"/> <!-- Permission required for UsageStatsTest CTS test. --> <permission name="android.permission.MANAGE_NOTIFICATIONS"/> <!-- Permission required for CompanionDeviceManager CTS test. --> diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 1919572ff571..aa847ac3f8d8 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -263,6 +263,8 @@ <uses-permission android:name="android.permission.MANAGE_APP_OPS_MODES" /> <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.ACCESS_VIBRATOR_STATE" /> + <uses-permission android:name="android.permission.VIBRATE_VENDOR_EFFECTS" /> + <uses-permission android:name="android.permission.START_VIBRATION_SESSIONS" /> <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> <uses-permission android:name="android.permission.START_TASKS_FROM_RECENTS" /> <uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND" /> diff --git a/services/art-profile b/services/art-profile index 6fa4c88cb1f6..ce1e2c6f1397 100644 --- a/services/art-profile +++ b/services/art-profile @@ -5657,7 +5657,7 @@ Lcom/android/server/utils/WatchedSparseSetArray; Lcom/android/server/utils/Watcher; Lcom/android/server/vibrator/VibratorController$NativeWrapper; Lcom/android/server/vibrator/VibratorController$OnVibrationCompleteListener; -Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener; +Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks; Lcom/android/server/vibrator/VibratorManagerService; Lcom/android/server/vr/EnabledComponentsObserver$EnabledComponentChangeListener; Lcom/android/server/vr/VrManagerService; diff --git a/services/art-wear-profile b/services/art-wear-profile index 47bdb1385137..1e3090f9bf00 100644 --- a/services/art-wear-profile +++ b/services/art-wear-profile @@ -1330,7 +1330,7 @@ Lcom/android/server/utils/WatchedSparseSetArray; Lcom/android/server/utils/Watcher; Lcom/android/server/vibrator/VibratorController$NativeWrapper; Lcom/android/server/vibrator/VibratorController$OnVibrationCompleteListener; -Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener; +Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks; Lcom/android/server/vibrator/VibratorManagerService; Lcom/android/server/vr/EnabledComponentsObserver$EnabledComponentChangeListener; Lcom/android/server/vr/VrManagerService; @@ -24948,7 +24948,7 @@ PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;-><init>()V PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->cancelSynced()V PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->getCapabilities()J PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->getVibratorIds()[I -PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->init(Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener;)V +PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->init(Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks;)V PLcom/android/server/vibrator/VibratorManagerService$VibrationCompleteListener;-><init>(Lcom/android/server/vibrator/VibratorManagerService;)V PLcom/android/server/vibrator/VibratorManagerService$VibrationCompleteListener;->onComplete(IJ)V PLcom/android/server/vibrator/VibratorManagerService$VibrationRecords;-><init>(II)V diff --git a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java index df44e50d2839..a92ac679b0f4 100644 --- a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java +++ b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java @@ -45,6 +45,7 @@ final class ExternalVibrationSession extends Vibration void onExternalVibrationReleased(long vibrationId); } + private final long mSessionId = VibrationSession.nextSessionId(); private final ExternalVibration mExternalVibration; private final ExternalVibrationScale mScale = new ExternalVibrationScale(); private final VibratorManagerHooks mManagerHooks; @@ -65,6 +66,11 @@ final class ExternalVibrationSession extends Vibration } @Override + public long getSessionId() { + return mSessionId; + } + + @Override public long getCreateUptimeMillis() { return stats.getCreateUptimeMillis(); } @@ -148,7 +154,12 @@ final class ExternalVibrationSession extends Vibration @Override public void notifySyncedVibratorsCallback(long vibrationId) { - // ignored, external control does not expect callbacks from the vibrator manager + // ignored, external control does not expect callbacks from the vibrator manager for sync + } + + @Override + public void notifySessionCallback() { + // ignored, external control does not expect callbacks from the vibrator manager for session } boolean isHoldingSameVibration(ExternalVibration vib) { @@ -174,7 +185,8 @@ final class ExternalVibrationSession extends Vibration @Override public String toString() { return "ExternalVibrationSession{" - + "id=" + id + + "sessionId=" + mSessionId + + ", vibrationId=" + id + ", callerInfo=" + callerInfo + ", externalVibration=" + mExternalVibration + ", scale=" + mScale diff --git a/services/core/java/com/android/server/vibrator/SingleVibrationSession.java b/services/core/java/com/android/server/vibrator/SingleVibrationSession.java index 67ba25f6b0b9..628221b09d77 100644 --- a/services/core/java/com/android/server/vibrator/SingleVibrationSession.java +++ b/services/core/java/com/android/server/vibrator/SingleVibrationSession.java @@ -35,6 +35,7 @@ final class SingleVibrationSession implements VibrationSession, IBinder.DeathRec private static final String TAG = "SingleVibrationSession"; private final Object mLock = new Object(); + private final long mSessionId = VibrationSession.nextSessionId(); private final IBinder mCallerToken; private final HalVibration mVibration; @@ -58,6 +59,11 @@ final class SingleVibrationSession implements VibrationSession, IBinder.DeathRec } @Override + public long getSessionId() { + return mSessionId; + } + + @Override public long getCreateUptimeMillis() { return mVibration.stats.getCreateUptimeMillis(); } @@ -155,9 +161,15 @@ final class SingleVibrationSession implements VibrationSession, IBinder.DeathRec } @Override + public void notifySessionCallback() { + // ignored, external control does not expect callbacks from the vibrator manager for session + } + + @Override public String toString() { return "SingleVibrationSession{" - + "callerToken= " + mCallerToken + + "sessionId= " + mSessionId + + ", callerToken= " + mCallerToken + ", vibration=" + mVibration + '}'; } diff --git a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java new file mode 100644 index 000000000000..07478e360d27 --- /dev/null +++ b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java @@ -0,0 +1,493 @@ +/* + * Copyright (C) 2024 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 static com.android.server.vibrator.VibrationSession.DebugInfo.formatTime; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.media.AudioAttributes; +import android.os.CancellationSignal; +import android.os.CombinedVibration; +import android.os.ExternalVibration; +import android.os.Handler; +import android.os.IBinder; +import android.os.ICancellationSignal; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.VibrationAttributes; +import android.os.vibrator.IVibrationSession; +import android.os.vibrator.IVibrationSessionCallback; +import android.util.IndentingPrintWriter; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.GuardedBy; + +import java.util.Arrays; +import java.util.Locale; +import java.util.NoSuchElementException; + +/** + * A vibration session started by a vendor request that can trigger {@link CombinedVibration}. + */ +final class VendorVibrationSession extends IVibrationSession.Stub + implements VibrationSession, CancellationSignal.OnCancelListener, IBinder.DeathRecipient { + private static final String TAG = "VendorVibrationSession"; + + /** Calls into VibratorManager functionality needed for playing an {@link ExternalVibration}. */ + interface VibratorManagerHooks { + + /** Tells the manager to end the vibration session. */ + void endSession(long sessionId, boolean shouldAbort); + + /** + * Tells the manager that the vibration session is finished and the vibrators can now be + * used for another vibration. + */ + void onSessionReleased(long sessionId); + } + + private final Object mLock = new Object(); + private final long mSessionId = VibrationSession.nextSessionId(); + private final ICancellationSignal mCancellationSignal = CancellationSignal.createTransport(); + private final int[] mVibratorIds; + private final long mCreateUptime; + private final long mCreateTime; // for debugging + private final IVibrationSessionCallback mCallback; + private final CallerInfo mCallerInfo; + private final VibratorManagerHooks mManagerHooks; + private final Handler mHandler; + + @GuardedBy("mLock") + private Status mStatus = Status.RUNNING; + @GuardedBy("mLock") + private Status mEndStatusRequest; + @GuardedBy("mLock") + private long mStartTime; // for debugging + @GuardedBy("mLock") + private long mEndUptime; + @GuardedBy("mLock") + private long mEndTime; // for debugging + + VendorVibrationSession(@NonNull CallerInfo callerInfo, @NonNull Handler handler, + @NonNull VibratorManagerHooks managerHooks, @NonNull int[] vibratorIds, + @NonNull IVibrationSessionCallback callback) { + mCreateUptime = SystemClock.uptimeMillis(); + mCreateTime = System.currentTimeMillis(); + mVibratorIds = vibratorIds; + mHandler = handler; + mCallback = callback; + mCallerInfo = callerInfo; + mManagerHooks = managerHooks; + CancellationSignal.fromTransport(mCancellationSignal).setOnCancelListener(this); + } + + @Override + public void vibrate(CombinedVibration vibration, String reason) { + // TODO(b/345414356): implement vibration support + throw new UnsupportedOperationException("Vendor session vibrations not yet implemented"); + } + + @Override + public void finishSession() { + // Do not abort session in HAL, wait for ongoing vibration requests to complete. + // This might take a while to end the session, but it can be aborted by cancelSession. + requestEndSession(Status.FINISHED, /* shouldAbort= */ false); + } + + @Override + public void cancelSession() { + // Always abort session in HAL while cancelling it. + // This might be triggered after finishSession was already called. + requestEndSession(Status.CANCELLED_BY_USER, /* shouldAbort= */ true); + } + + @Override + public long getSessionId() { + return mSessionId; + } + + @Override + public long getCreateUptimeMillis() { + return mCreateUptime; + } + + @Override + public boolean isRepeating() { + return false; + } + + @Override + public CallerInfo getCallerInfo() { + return mCallerInfo; + } + + @Override + public IBinder getCallerToken() { + return mCallback.asBinder(); + } + + @Override + public DebugInfo getDebugInfo() { + synchronized (mLock) { + return new DebugInfoImpl(mStatus, mCallerInfo, mCreateUptime, mCreateTime, mStartTime, + mEndUptime, mEndTime); + } + } + + @Override + public boolean wasEndRequested() { + synchronized (mLock) { + return mEndStatusRequest != null; + } + } + + @Override + public void onCancel() { + Slog.d(TAG, "Cancellation signal received, cancelling vibration session..."); + requestEnd(Status.CANCELLED_BY_USER, /* endedBy= */ null, /* immediate= */ false); + } + + @Override + public void binderDied() { + Slog.d(TAG, "Binder died, cancelling vibration session..."); + requestEnd(Status.CANCELLED_BINDER_DIED, /* endedBy= */ null, /* immediate= */ false); + } + + @Override + public boolean linkToDeath() { + try { + mCallback.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + Slog.e(TAG, "Error linking session to token death", e); + return false; + } + return true; + } + + @Override + public void unlinkToDeath() { + try { + mCallback.asBinder().unlinkToDeath(this, 0); + } catch (NoSuchElementException e) { + Slog.wtf(TAG, "Failed to unlink session to token death", e); + } + } + + @Override + public void requestEnd(@NonNull Status status, @Nullable CallerInfo endedBy, + boolean immediate) { + // All requests to end a session should abort it to stop ongoing vibrations, even if + // immediate flag is false. Only the #finishSession API will not abort and wait for + // session vibrations to complete, which might take a long time. + requestEndSession(status, /* shouldAbort= */ true); + } + + @Override + public void notifyVibratorCallback(int vibratorId, long vibrationId) { + // TODO(b/345414356): implement vibration support + } + + @Override + public void notifySyncedVibratorsCallback(long vibrationId) { + // TODO(b/345414356): implement vibration support + } + + @Override + public void notifySessionCallback() { + synchronized (mLock) { + // If end was not requested then the HAL has cancelled the session. + maybeSetEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON); + maybeSetStatusToRequestedLocked(); + } + mManagerHooks.onSessionReleased(mSessionId); + } + + @Override + public String toString() { + synchronized (mLock) { + return "createTime: " + formatTime(mCreateTime, /*includeDate=*/ true) + + ", startTime: " + (mStartTime == 0 ? null : formatTime(mStartTime, + /* includeDate= */ true)) + + ", endTime: " + (mEndTime == 0 ? null : formatTime(mEndTime, + /* includeDate= */ true)) + + ", status: " + mStatus.name().toLowerCase(Locale.ROOT) + + ", callerInfo: " + mCallerInfo + + ", vibratorIds: " + Arrays.toString(mVibratorIds); + } + } + + public Status getStatus() { + synchronized (mLock) { + return mStatus; + } + } + + public boolean isStarted() { + synchronized (mLock) { + return mStartTime > 0; + } + } + + public boolean isEnded() { + synchronized (mLock) { + return mStatus != Status.RUNNING; + } + } + + public int[] getVibratorIds() { + return mVibratorIds; + } + + public ICancellationSignal getCancellationSignal() { + return mCancellationSignal; + } + + public void notifyStart() { + boolean isAlreadyEnded = false; + synchronized (mLock) { + if (isEnded()) { + // Session already ended, skip start callbacks. + isAlreadyEnded = true; + } else { + mStartTime = System.currentTimeMillis(); + // Run client callback in separate thread. + mHandler.post(() -> { + try { + mCallback.onStarted(this); + } catch (RemoteException e) { + Slog.e(TAG, "Error notifying vendor session started", e); + } + }); + } + } + if (isAlreadyEnded) { + // Session already ended, make sure we end it in the HAL. + mManagerHooks.endSession(mSessionId, /* shouldAbort= */ true); + } + } + + private void requestEndSession(Status status, boolean shouldAbort) { + boolean shouldTriggerSessionHook = false; + synchronized (mLock) { + maybeSetEndRequestLocked(status); + if (isStarted()) { + // Always trigger session hook after it has started, in case new request aborts an + // already finishing session. Wait for HAL callback before actually ending here. + shouldTriggerSessionHook = true; + } else { + // Session did not start in the HAL, end it right away. + maybeSetStatusToRequestedLocked(); + } + } + if (shouldTriggerSessionHook) { + mManagerHooks.endSession(mSessionId, shouldAbort); + } + } + + @GuardedBy("mLock") + private void maybeSetEndRequestLocked(Status status) { + if (mEndStatusRequest != null) { + // End already requested, keep first requested status and time. + return; + } + mEndStatusRequest = status; + mEndTime = System.currentTimeMillis(); + mEndUptime = SystemClock.uptimeMillis(); + if (isStarted()) { + // Only trigger "finishing" callback if session started. + // Run client callback in separate thread. + mHandler.post(() -> { + try { + mCallback.onFinishing(); + } catch (RemoteException e) { + Slog.e(TAG, "Error notifying vendor session is finishing", e); + } + }); + } + } + + @GuardedBy("mLock") + private void maybeSetStatusToRequestedLocked() { + if (isEnded()) { + // End already set, keep first requested status and time. + return; + } + if (mEndStatusRequest == null) { + // No end status was requested, nothing to set. + return; + } + mStatus = mEndStatusRequest; + // Run client callback in separate thread. + final Status endStatus = mStatus; + mHandler.post(() -> { + try { + mCallback.onFinished(toSessionStatus(endStatus)); + } catch (RemoteException e) { + Slog.e(TAG, "Error notifying vendor session is finishing", e); + } + }); + } + + @android.os.vibrator.VendorVibrationSession.Status + private static int toSessionStatus(Status status) { + // Exhaustive switch to cover all possible internal status. + return switch (status) { + case FINISHED + -> android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS; + case IGNORED_UNSUPPORTED + -> STATUS_UNSUPPORTED; + case CANCELLED_BINDER_DIED, CANCELLED_BY_APP_OPS, CANCELLED_BY_USER, + CANCELLED_SUPERSEDED, CANCELLED_BY_FOREGROUND_USER, CANCELLED_BY_SCREEN_OFF, + CANCELLED_BY_SETTINGS_UPDATE, CANCELLED_BY_UNKNOWN_REASON + -> android.os.vibrator.VendorVibrationSession.STATUS_CANCELED; + case IGNORED_APP_OPS, IGNORED_BACKGROUND, IGNORED_FOR_EXTERNAL, IGNORED_FOR_ONGOING, + IGNORED_FOR_POWER, IGNORED_FOR_SETTINGS, IGNORED_FOR_HIGHER_IMPORTANCE, + IGNORED_FOR_RINGER_MODE, IGNORED_FROM_VIRTUAL_DEVICE, IGNORED_SUPERSEDED, + IGNORED_MISSING_PERMISSION, IGNORED_ON_WIRELESS_CHARGER + -> android.os.vibrator.VendorVibrationSession.STATUS_IGNORED; + case UNKNOWN, IGNORED_ERROR_APP_OPS, IGNORED_ERROR_CANCELLING, IGNORED_ERROR_SCHEDULING, + IGNORED_ERROR_TOKEN, FORWARDED_TO_INPUT_DEVICES, FINISHED_UNEXPECTED, RUNNING + -> android.os.vibrator.VendorVibrationSession.STATUS_UNKNOWN_ERROR; + }; + } + + /** + * Holds lightweight debug information about the session that could potentially be kept in + * memory for a long time for bugreport dumpsys operations. + * + * Since DebugInfo can be kept in memory for a long time, it shouldn't hold any references to + * potentially expensive or resource-linked objects, such as {@link IBinder}. + */ + static final class DebugInfoImpl implements VibrationSession.DebugInfo { + private final Status mStatus; + private final CallerInfo mCallerInfo; + + private final long mCreateUptime; + private final long mCreateTime; + private final long mStartTime; + private final long mEndTime; + private final long mDurationMs; + + DebugInfoImpl(Status status, CallerInfo callerInfo, long createUptime, long createTime, + long startTime, long endUptime, long endTime) { + mStatus = status; + mCallerInfo = callerInfo; + mCreateUptime = createUptime; + mCreateTime = createTime; + mStartTime = startTime; + mEndTime = endTime; + mDurationMs = endUptime > 0 ? endUptime - createUptime : -1; + } + + @Override + public Status getStatus() { + return mStatus; + } + + @Override + public long getCreateUptimeMillis() { + return mCreateUptime; + } + + @Override + public CallerInfo getCallerInfo() { + return mCallerInfo; + } + + @Nullable + @Override + public Object getDumpAggregationKey() { + return null; // No aggregation. + } + + @Override + public void logMetrics(VibratorFrameworkStatsLogger statsLogger) { + } + + @Override + public void dump(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + proto.write(VibrationProto.END_TIME, mEndTime); + proto.write(VibrationProto.DURATION_MS, mDurationMs); + proto.write(VibrationProto.STATUS, mStatus.ordinal()); + + final long attrsToken = proto.start(VibrationProto.ATTRIBUTES); + final VibrationAttributes attrs = mCallerInfo.attrs; + proto.write(VibrationAttributesProto.USAGE, attrs.getUsage()); + proto.write(VibrationAttributesProto.AUDIO_USAGE, attrs.getAudioUsage()); + proto.write(VibrationAttributesProto.FLAGS, attrs.getFlags()); + proto.end(attrsToken); + + proto.end(token); + } + + @Override + public void dump(IndentingPrintWriter pw) { + pw.println("VibrationSession:"); + pw.increaseIndent(); + pw.println("status = " + mStatus.name().toLowerCase(Locale.ROOT)); + pw.println("durationMs = " + mDurationMs); + pw.println("createTime = " + formatTime(mCreateTime, /*includeDate=*/ true)); + pw.println("startTime = " + formatTime(mStartTime, /*includeDate=*/ true)); + pw.println("endTime = " + (mEndTime == 0 ? null + : formatTime(mEndTime, /*includeDate=*/ true))); + pw.println("callerInfo = " + mCallerInfo); + pw.decreaseIndent(); + } + + @Override + public void dumpCompact(IndentingPrintWriter pw) { + // Follow pattern from Vibration.DebugInfoImpl for better debugging from dumpsys. + String timingsStr = String.format(Locale.ROOT, + "%s | %8s | %20s | duration: %5dms | start: %12s | end: %12s", + formatTime(mCreateTime, /*includeDate=*/ true), + "session", + mStatus.name().toLowerCase(Locale.ROOT), + mDurationMs, + mStartTime == 0 ? "" : formatTime(mStartTime, /*includeDate=*/ false), + mEndTime == 0 ? "" : formatTime(mEndTime, /*includeDate=*/ false)); + String paramStr = String.format(Locale.ROOT, + " | flags: %4s | usage: %s", + Long.toBinaryString(mCallerInfo.attrs.getFlags()), + mCallerInfo.attrs.usageToString()); + // Optional, most vibrations should not be defined via AudioAttributes + // so skip them to simplify the logs + String audioUsageStr = + mCallerInfo.attrs.getOriginalAudioUsage() != AudioAttributes.USAGE_UNKNOWN + ? " | audioUsage=" + AudioAttributes.usageToString( + mCallerInfo.attrs.getOriginalAudioUsage()) + : ""; + String callerStr = String.format(Locale.ROOT, + " | %s (uid=%d, deviceId=%d) | reason: %s", + mCallerInfo.opPkg, mCallerInfo.uid, mCallerInfo.deviceId, mCallerInfo.reason); + pw.println(timingsStr + paramStr + audioUsageStr + callerStr); + } + + @Override + public String toString() { + return "createTime: " + formatTime(mCreateTime, /* includeDate= */ true) + + ", startTime: " + formatTime(mStartTime, /* includeDate= */ true) + + ", endTime: " + (mEndTime == 0 ? null : formatTime(mEndTime, + /* includeDate= */ true)) + + ", durationMs: " + mDurationMs + + ", status: " + mStatus.name().toLowerCase(Locale.ROOT) + + ", callerInfo: " + mCallerInfo; + } + } +} diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java index bb2a17c698ee..27f92b2080e6 100644 --- a/services/core/java/com/android/server/vibrator/Vibration.java +++ b/services/core/java/com/android/server/vibrator/Vibration.java @@ -16,6 +16,8 @@ package com.android.server.vibrator; +import static com.android.server.vibrator.VibrationSession.DebugInfo.formatTime; + import android.annotation.NonNull; import android.annotation.Nullable; import android.media.AudioAttributes; @@ -31,9 +33,6 @@ import android.os.vibrator.VibrationEffectSegment; import android.util.IndentingPrintWriter; import android.util.proto.ProtoOutputStream; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; @@ -42,11 +41,6 @@ import java.util.concurrent.atomic.AtomicInteger; * The base class for all vibrations. */ abstract class Vibration { - private static final DateTimeFormatter DEBUG_TIME_FORMATTER = DateTimeFormatter.ofPattern( - "HH:mm:ss.SSS"); - private static final DateTimeFormatter DEBUG_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern( - "MM-dd HH:mm:ss.SSS"); - // Used to generate globally unique vibration ids. private static final AtomicInteger sNextVibrationId = new AtomicInteger(1); // 0 = no callback @@ -399,12 +393,5 @@ abstract class Vibration { proto.write(PrimitiveSegmentProto.DELAY, segment.getDelay()); proto.end(token); } - - private String formatTime(long timeInMillis, boolean includeDate) { - return (includeDate ? DEBUG_DATE_TIME_FORMATTER : DEBUG_TIME_FORMATTER) - // Ensure timezone is retrieved at formatting time - .withZone(ZoneId.systemDefault()) - .format(Instant.ofEpochMilli(timeInMillis)); - } } } diff --git a/services/core/java/com/android/server/vibrator/VibrationSession.java b/services/core/java/com/android/server/vibrator/VibrationSession.java index b511ba8be405..ae95a70e2a4f 100644 --- a/services/core/java/com/android/server/vibrator/VibrationSession.java +++ b/services/core/java/com/android/server/vibrator/VibrationSession.java @@ -25,7 +25,11 @@ import android.util.IndentingPrintWriter; import android.util.proto.ProtoOutputStream; import java.io.PrintWriter; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; /** * Represents a generic vibration session that plays one or more vibration requests. @@ -39,6 +43,16 @@ import java.util.Objects; */ interface VibrationSession { + // Used to generate globally unique session ids. + AtomicInteger sNextSessionId = new AtomicInteger(1); // 0 = no callback + + static long nextSessionId() { + return sNextSessionId.getAndIncrement(); + } + + /** Returns the session id. */ + long getSessionId(); + /** Returns the session creation time from {@link android.os.SystemClock#uptimeMillis()}. */ long getCreateUptimeMillis(); @@ -105,6 +119,14 @@ interface VibrationSession { void notifySyncedVibratorsCallback(long vibrationId); /** + * Notify vibrator manager have completed the vibration session. + * + * <p>This will be called by the vibrator manager hardware callback indicating the session + * is complete, either because it was ended or cancelled by the service or the vendor. + */ + void notifySessionCallback(); + + /** * Session status with reference to values from vibratormanagerservice.proto for logging. */ enum Status { @@ -212,6 +234,17 @@ interface VibrationSession { */ interface DebugInfo { + DateTimeFormatter DEBUG_TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); + DateTimeFormatter DEBUG_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern( + "MM-dd HH:mm:ss.SSS"); + + static String formatTime(long timeInMillis, boolean includeDate) { + return (includeDate ? DEBUG_DATE_TIME_FORMATTER : DEBUG_TIME_FORMATTER) + // Ensure timezone is retrieved at formatting time + .withZone(ZoneId.systemDefault()) + .format(Instant.ofEpochMilli(timeInMillis)); + } + /** Return the vibration session status. */ Status getStatus(); diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index ff3491182a5f..476448148e28 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -32,6 +32,7 @@ import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Resources; import android.hardware.vibrator.IVibrator; +import android.hardware.vibrator.IVibratorManager; import android.os.BatteryStats; import android.os.Binder; import android.os.Build; @@ -40,6 +41,7 @@ import android.os.ExternalVibration; import android.os.ExternalVibrationScale; import android.os.Handler; import android.os.IBinder; +import android.os.ICancellationSignal; import android.os.IExternalVibratorService; import android.os.IVibratorManagerService; import android.os.IVibratorStateListener; @@ -57,6 +59,7 @@ import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.VibratorInfo; import android.os.vibrator.Flags; +import android.os.vibrator.IVibrationSessionCallback; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibrationEffectSegment; @@ -103,7 +106,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private static final String EXTERNAL_VIBRATOR_SERVICE = "external_vibrator_service"; private static final String VIBRATOR_CONTROL_SERVICE = "android.frameworks.vibrator.IVibratorControlService/default"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; private static final VibrationAttributes DEFAULT_ATTRIBUTES = new VibrationAttributes.Builder().build(); private static final int ATTRIBUTES_ALL_BYPASS_FLAGS = @@ -159,12 +162,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { new VibrationThreadCallbacks(); private final ExternalVibrationCallbacks mExternalVibrationCallbacks = new ExternalVibrationCallbacks(); + private final VendorVibrationSessionCallbacks mVendorVibrationSessionCallbacks = + new VendorVibrationSessionCallbacks(); @GuardedBy("mLock") private final SparseArray<AlwaysOnVibration> mAlwaysOnEffects = new SparseArray<>(); @GuardedBy("mLock") - private VibrationSession mCurrentVibration; + private VibrationSession mCurrentSession; @GuardedBy("mLock") - private VibrationSession mNextVibration; + private VibrationSession mNextSession; @GuardedBy("mLock") private boolean mServiceReady; @@ -191,14 +196,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // When the system is entering a non-interactive state, we want to cancel // vibrations in case a misbehaving app has abandoned them. synchronized (mLock) { - maybeClearCurrentAndNextVibrationsLocked( + maybeClearCurrentAndNextSessionsLocked( VibratorManagerService.this::shouldCancelOnScreenOffLocked, Status.CANCELLED_BY_SCREEN_OFF); } } else if (android.multiuser.Flags.addUiForSoundsFromBackgroundUsers() && intent.getAction().equals(BackgroundUserSoundNotifier.ACTION_MUTE_SOUND)) { synchronized (mLock) { - maybeClearCurrentAndNextVibrationsLocked( + maybeClearCurrentAndNextSessionsLocked( VibratorManagerService.this::shouldCancelOnFgUserRequest, Status.CANCELLED_BY_FOREGROUND_USER); } @@ -215,14 +220,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { return; } synchronized (mLock) { - maybeClearCurrentAndNextVibrationsLocked( + maybeClearCurrentAndNextSessionsLocked( VibratorManagerService.this::shouldCancelAppOpModeChangedLocked, Status.CANCELLED_BY_APP_OPS); } } }; - static native long nativeInit(OnSyncedVibrationCompleteListener listener); + static native long nativeInit(VibratorManagerNativeCallbacks listener); static native long nativeGetFinalizer(); @@ -236,6 +241,13 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { static native void nativeCancelSynced(long nativeServicePtr); + static native boolean nativeStartSession(long nativeServicePtr, long sessionId, + int[] vibratorIds); + + static native void nativeEndSession(long nativeServicePtr, long sessionId, boolean shouldAbort); + + static native void nativeClearSessions(long nativeServicePtr); + @VisibleForTesting VibratorManagerService(Context context, Injector injector) { mContext = context; @@ -303,6 +315,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // Reset the hardware to a default state, in case this is a runtime restart instead of a // fresh boot. mNativeWrapper.cancelSynced(); + if (Flags.vendorVibrationEffects()) { + mNativeWrapper.clearSessions(); + } for (int i = 0; i < mVibrators.size(); i++) { mVibrators.valueAt(i).reset(); } @@ -363,6 +378,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @Override // Binder call + public int getCapabilities() { + return (int) mCapabilities; + } + + @Override // Binder call @Nullable public VibratorInfo getVibratorInfo(int vibratorId) { final VibratorController controller = mVibrators.get(vibratorId); @@ -590,11 +610,17 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_ERROR_TOKEN); return null; } - if (effect.hasVendorEffects() - && !hasPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS)) { - Slog.e(TAG, "vibrate; no permission for vendor effects"); - logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_MISSING_PERMISSION); - return null; + if (effect.hasVendorEffects()) { + if (!Flags.vendorVibrationEffects()) { + Slog.e(TAG, "vibrate; vendor effects feature disabled"); + logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_UNSUPPORTED); + return null; + } + if (!hasPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS)) { + Slog.e(TAG, "vibrate; no permission for vendor effects"); + logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_MISSING_PERMISSION); + return null; + } } enforceUpdateAppOpsStatsPermission(uid); if (!isEffectValid(effect)) { @@ -623,7 +649,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // Check if ongoing vibration is more important than this vibration. if (ignoreStatus == null) { - Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationForOngoingLocked(session); + Vibration.EndInfo vibrationEndInfo = shouldIgnoreForOngoingLocked(session); if (vibrationEndInfo != null) { ignoreStatus = vibrationEndInfo.status; ignoredBy = vibrationEndInfo.endedBy; @@ -634,8 +660,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (ignoreStatus == null) { final long ident = Binder.clearCallingIdentity(); try { - if (mCurrentVibration != null) { - if (shouldPipelineVibrationLocked(mCurrentVibration, vib)) { + if (mCurrentSession != null) { + if (shouldPipelineVibrationLocked(mCurrentSession, vib)) { // Don't cancel the current vibration if it's pipeline-able. // Note that if there is a pending next vibration that can't be // pipelined, it will have already cancelled the current one, so we @@ -645,12 +671,12 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } else { vib.stats.reportInterruptedAnotherVibration( - mCurrentVibration.getCallerInfo()); - mCurrentVibration.requestEnd(Status.CANCELLED_SUPERSEDED, callerInfo, + mCurrentSession.getCallerInfo()); + mCurrentSession.requestEnd(Status.CANCELLED_SUPERSEDED, callerInfo, /* immediate= */ false); } } - clearNextVibrationLocked(Status.CANCELLED_SUPERSEDED, callerInfo); + clearNextSessionLocked(Status.CANCELLED_SUPERSEDED, callerInfo); ignoreStatus = startVibrationLocked(session); } finally { Binder.restoreCallingIdentity(ident); @@ -659,7 +685,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // Ignored or failed to start the vibration, end it and report metrics right away. if (ignoreStatus != null) { - endVibrationLocked(session, ignoreStatus, ignoredBy); + endSessionLocked(session, ignoreStatus, ignoredBy); } return vib; } @@ -681,19 +707,154 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { try { // TODO(b/370948466): investigate why token not checked on external vibrations. IBinder cancelToken = - (mNextVibration instanceof ExternalVibrationSession) ? null : token; - if (shouldCancelVibration(mNextVibration, usageFilter, cancelToken)) { - clearNextVibrationLocked(Status.CANCELLED_BY_USER); + (mNextSession instanceof ExternalVibrationSession) ? null : token; + if (shouldCancelSession(mNextSession, usageFilter, cancelToken)) { + clearNextSessionLocked(Status.CANCELLED_BY_USER); } cancelToken = - (mCurrentVibration instanceof ExternalVibrationSession) ? null : token; - if (shouldCancelVibration(mCurrentVibration, usageFilter, cancelToken)) { - mCurrentVibration.requestEnd(Status.CANCELLED_BY_USER); + (mCurrentSession instanceof ExternalVibrationSession) ? null : token; + if (shouldCancelSession(mCurrentSession, usageFilter, cancelToken)) { + mCurrentSession.requestEnd(Status.CANCELLED_BY_USER); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } finally { + Trace.traceEnd(TRACE_TAG_VIBRATOR); + } + } + + @android.annotation.EnforcePermission(allOf = { + android.Manifest.permission.VIBRATE, + android.Manifest.permission.VIBRATE_VENDOR_EFFECTS, + android.Manifest.permission.START_VIBRATION_SESSIONS, + }) + @Override // Binder call + public ICancellationSignal startVendorVibrationSession(int uid, int deviceId, String opPkg, + int[] vibratorIds, VibrationAttributes attrs, String reason, + IVibrationSessionCallback callback) { + startVendorVibrationSession_enforcePermission(); + Trace.traceBegin(TRACE_TAG_VIBRATOR, "startVibrationSession"); + try { + VendorVibrationSession session = startVendorVibrationSessionInternal( + uid, deviceId, opPkg, vibratorIds, attrs, reason, callback); + return session == null ? null : session.getCancellationSignal(); + } finally { + Trace.traceEnd(TRACE_TAG_VIBRATOR); + } + } + + VendorVibrationSession startVendorVibrationSessionInternal(int uid, int deviceId, String opPkg, + int[] vibratorIds, VibrationAttributes attrs, String reason, + IVibrationSessionCallback callback) { + if (!Flags.vendorVibrationEffects()) { + throw new UnsupportedOperationException("Vibration sessions not supported"); + } + attrs = fixupVibrationAttributes(attrs, /* effect= */ null); + CallerInfo callerInfo = new CallerInfo(attrs, uid, deviceId, opPkg, reason); + if (callback == null) { + Slog.e(TAG, "session callback must not be null"); + logAndRecordSessionAttempt(callerInfo, Status.IGNORED_ERROR_TOKEN); + return null; + } + if (vibratorIds == null) { + vibratorIds = new int[0]; + } + enforceUpdateAppOpsStatsPermission(uid); + VendorVibrationSession session = new VendorVibrationSession(callerInfo, mHandler, + mVendorVibrationSessionCallbacks, vibratorIds, callback); + + if (attrs.isFlagSet(VibrationAttributes.FLAG_INVALIDATE_SETTINGS_CACHE)) { + // Force update of user settings before checking if this vibration effect should + // be ignored or scaled. + mVibrationSettings.update(); + } + + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "Starting session " + session.getSessionId()); + } + + Status ignoreStatus = null; + CallerInfo ignoredBy = null; + + // Check if HAL has capability to start sessions. + if ((mCapabilities & IVibratorManager.CAP_START_SESSIONS) == 0) { + if (DEBUG) { + Slog.d(TAG, "Missing capability to start sessions, ignoring request"); + } + ignoreStatus = Status.IGNORED_UNSUPPORTED; + } + + // Check if any vibrator ID was requested. + if (ignoreStatus == null && vibratorIds.length == 0) { + if (DEBUG) { + Slog.d(TAG, "Empty vibrator ids to start session, ignoring request"); + } + ignoreStatus = Status.IGNORED_UNSUPPORTED; + } + + // Check if user settings or DnD is set to ignore this session. + if (ignoreStatus == null) { + ignoreStatus = shouldIgnoreVibrationLocked(callerInfo); + } + + // Check if ongoing vibration is more important than this session. + if (ignoreStatus == null) { + Vibration.EndInfo vibrationEndInfo = shouldIgnoreForOngoingLocked(session); + if (vibrationEndInfo != null) { + ignoreStatus = vibrationEndInfo.status; + ignoredBy = vibrationEndInfo.endedBy; + } + } + + if (ignoreStatus == null) { + final long ident = Binder.clearCallingIdentity(); + try { + // If not ignored so far then stop ongoing sessions before starting this one. + clearNextSessionLocked(Status.CANCELLED_SUPERSEDED, callerInfo); + if (mCurrentSession != null) { + mNextSession = session; + mCurrentSession.requestEnd(Status.CANCELLED_SUPERSEDED, callerInfo, + /* immediate= */ false); + } else { + ignoreStatus = startVendorSessionLocked(session); } } finally { Binder.restoreCallingIdentity(ident); } } + + // Ignored or failed to start the session, end it and report metrics right away. + if (ignoreStatus != null) { + endSessionLocked(session, ignoreStatus, ignoredBy); + } + return session; + } + } + + @GuardedBy("mLock") + @Nullable + private Status startVendorSessionLocked(VendorVibrationSession session) { + Trace.traceBegin(TRACE_TAG_VIBRATOR, "startSessionLocked"); + try { + if (session.isEnded()) { + // Session already ended, possibly cancelled by app cancellation signal. + return session.getStatus(); + } + if (!session.linkToDeath()) { + return Status.IGNORED_ERROR_TOKEN; + } + if (!mNativeWrapper.startSession(session.getSessionId(), session.getVibratorIds())) { + Slog.e(TAG, "Error starting session " + session.getSessionId() + + " on vibrators " + Arrays.toString(session.getVibratorIds())); + session.unlinkToDeath(); + return Status.IGNORED_UNSUPPORTED; + } + session.notifyStart(); + mCurrentSession = session; + return null; } finally { Trace.traceEnd(TRACE_TAG_VIBRATOR); } @@ -747,8 +908,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { pw.println("CurrentVibration:"); pw.increaseIndent(); - if (mCurrentVibration != null) { - mCurrentVibration.getDebugInfo().dump(pw); + if (mCurrentSession != null) { + mCurrentSession.getDebugInfo().dump(pw); } else { pw.println("null"); } @@ -757,8 +918,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { pw.println("NextVibration:"); pw.increaseIndent(); - if (mNextVibration != null) { - mNextVibration.getDebugInfo().dump(pw); + if (mNextSession != null) { + mNextSession.getDebugInfo().dump(pw); } else { pw.println("null"); } @@ -782,8 +943,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { synchronized (mLock) { mVibrationSettings.dump(proto); mVibrationScaler.dump(proto); - if (mCurrentVibration != null) { - mCurrentVibration.getDebugInfo().dump(proto, + if (mCurrentSession != null) { + mCurrentSession.getDebugInfo().dump(proto, VibratorManagerServiceDumpProto.CURRENT_VIBRATION); } for (int i = 0; i < mVibrators.size(); i++) { @@ -816,18 +977,18 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } // TODO(b/372241975): investigate why external vibrations were not handled here before - if (mCurrentVibration == null - || (mCurrentVibration instanceof ExternalVibrationSession)) { + if (mCurrentSession == null + || (mCurrentSession instanceof ExternalVibrationSession)) { return; } - Status ignoreStatus = shouldIgnoreVibrationLocked(mCurrentVibration.getCallerInfo()); + Status ignoreStatus = shouldIgnoreVibrationLocked(mCurrentSession.getCallerInfo()); if (inputDevicesChanged || (ignoreStatus != null)) { if (DEBUG) { Slog.d(TAG, "Canceling vibration because settings changed: " + (inputDevicesChanged ? "input devices changed" : ignoreStatus)); } - mCurrentVibration.requestEnd(Status.CANCELLED_BY_SETTINGS_UPDATE); + mCurrentSession.requestEnd(Status.CANCELLED_BY_SETTINGS_UPDATE); } } } @@ -866,15 +1027,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (mInputDeviceDelegate.isAvailable()) { return startVibrationOnInputDevicesLocked(session.getVibration()); } - if (mCurrentVibration == null) { + if (mCurrentSession == null) { return startVibrationOnThreadLocked(session); } // If there's already a vibration queued (waiting for the previous one to finish // cancelling), end it cleanly and replace it with the new one. // Note that we don't consider pipelining here, because new pipelined ones should // replace pending non-executing pipelined ones anyway. - clearNextVibrationLocked(Status.IGNORED_SUPERSEDED, session.getCallerInfo()); - mNextVibration = session; + clearNextSessionLocked(Status.IGNORED_SUPERSEDED, session.getCallerInfo()); + mNextSession = session; return null; } finally { Trace.traceEnd(TRACE_TAG_VIBRATOR); @@ -891,16 +1052,16 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { case AppOpsManager.MODE_ALLOWED: Trace.asyncTraceBegin(TRACE_TAG_VIBRATOR, "vibration", 0); // Make sure mCurrentVibration is set while triggering the VibrationThread. - mCurrentVibration = session; - if (!mCurrentVibration.linkToDeath()) { + mCurrentSession = session; + if (!mCurrentSession.linkToDeath()) { // Shouldn't happen. The method call already logs. - mCurrentVibration = null; // Aborted. + mCurrentSession = null; // Aborted. return Status.IGNORED_ERROR_TOKEN; } if (!mVibrationThread.runVibrationOnVibrationThread(conductor)) { // Shouldn't happen. The method call already logs. session.setVibrationConductor(null); // Rejected by thread, clear it in session. - mCurrentVibration = null; // Aborted. + mCurrentSession = null; // Aborted. return Status.IGNORED_ERROR_SCHEDULING; } return null; @@ -914,23 +1075,29 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @GuardedBy("mLock") - private void maybeStartNextSingleVibrationLocked() { - if (mNextVibration instanceof SingleVibrationSession session) { - mNextVibration = null; + private void maybeStartNextSessionLocked() { + if (mNextSession instanceof SingleVibrationSession session) { + mNextSession = null; Status errorStatus = startVibrationOnThreadLocked(session); if (errorStatus != null) { - endVibrationLocked(session, errorStatus); + endSessionLocked(session, errorStatus); } - } + } else if (mNextSession instanceof VendorVibrationSession session) { + mNextSession = null; + Status errorStatus = startVendorSessionLocked(session); + if (errorStatus != null) { + endSessionLocked(session, errorStatus); + } + } // External vibrations cannot be started asynchronously. } @GuardedBy("mLock") - private void endVibrationLocked(VibrationSession session, Status status) { - endVibrationLocked(session, status, /* endedBy= */ null); + private void endSessionLocked(VibrationSession session, Status status) { + endSessionLocked(session, status, /* endedBy= */ null); } @GuardedBy("mLock") - private void endVibrationLocked(VibrationSession session, Status status, CallerInfo endedBy) { + private void endSessionLocked(VibrationSession session, Status status, CallerInfo endedBy) { session.requestEnd(status, endedBy, /* immediate= */ false); logAndRecordVibration(session.getDebugInfo()); } @@ -975,6 +1142,13 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { VibrationScaler.ADAPTIVE_SCALE_NONE)); } + private void logAndRecordSessionAttempt(CallerInfo callerInfo, Status status) { + logAndRecordVibration( + new VendorVibrationSession.DebugInfoImpl(status, callerInfo, + SystemClock.uptimeMillis(), System.currentTimeMillis(), + /* startTime= */ 0, /* endUptime= */ 0, /* endTime= */ 0)); + } + private void logAndRecordVibration(DebugInfo info) { info.logMetrics(mFrameworkStatsLogger); logVibrationStatus(info.getCallerInfo().uid, info.getCallerInfo().attrs, info.getStatus()); @@ -1026,25 +1200,40 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + private void onVibrationSessionComplete(long sessionId) { + synchronized (mLock) { + if (mCurrentSession == null || mCurrentSession.getSessionId() != sessionId) { + if (DEBUG) { + Slog.d(TAG, "Vibration session " + sessionId + " callback ignored"); + } + return; + } + if (DEBUG) { + Slog.d(TAG, "Vibration session " + sessionId + " complete, notifying session"); + } + mCurrentSession.notifySessionCallback(); + } + } + private void onSyncedVibrationComplete(long vibrationId) { synchronized (mLock) { - if (mCurrentVibration != null) { + if (mCurrentSession != null) { if (DEBUG) { Slog.d(TAG, "Synced vibration " + vibrationId + " complete, notifying thread"); } - mCurrentVibration.notifySyncedVibratorsCallback(vibrationId); + mCurrentSession.notifySyncedVibratorsCallback(vibrationId); } } } private void onVibrationComplete(int vibratorId, long vibrationId) { synchronized (mLock) { - if (mCurrentVibration != null) { + if (mCurrentSession != null) { if (DEBUG) { Slog.d(TAG, "Vibration " + vibrationId + " on vibrator " + vibratorId + " complete, notifying thread"); } - mCurrentVibration.notifyVibratorCallback(vibratorId, vibrationId); + mCurrentSession.notifyVibratorCallback(vibratorId, vibrationId); } } } @@ -1056,10 +1245,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { */ @GuardedBy("mLock") @Nullable - private Vibration.EndInfo shouldIgnoreVibrationForOngoingLocked(VibrationSession session) { - if (mNextVibration != null) { - Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationForOngoing(session, - mNextVibration); + private Vibration.EndInfo shouldIgnoreForOngoingLocked(VibrationSession session) { + if (mNextSession != null) { + Vibration.EndInfo vibrationEndInfo = shouldIgnoreForOngoing(session, + mNextSession); if (vibrationEndInfo != null) { // Next vibration has higher importance than the new one, so the new vibration // should be ignored. @@ -1067,13 +1256,13 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } - if (mCurrentVibration != null) { - if (mCurrentVibration.wasEndRequested()) { + if (mCurrentSession != null) { + if (mCurrentSession.wasEndRequested()) { // Current session has ended or is cancelling, should not block incoming vibrations. return null; } - return shouldIgnoreVibrationForOngoing(session, mCurrentVibration); + return shouldIgnoreForOngoing(session, mCurrentSession); } return null; @@ -1086,7 +1275,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { * @return a Vibration.EndInfo if the vibration should be ignored, null otherwise. */ @Nullable - private static Vibration.EndInfo shouldIgnoreVibrationForOngoing( + private static Vibration.EndInfo shouldIgnoreForOngoing( @NonNull VibrationSession newSession, @NonNull VibrationSession ongoingSession) { int newSessionImportance = getVibrationImportance(newSession); @@ -1214,11 +1403,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { * @param tokenFilter The binder token to identify the vibration origin. Only vibrations * started with the same token can be cancelled with it. */ - private boolean shouldCancelVibration(@Nullable VibrationSession session, int usageFilter, + private boolean shouldCancelSession(@Nullable VibrationSession session, int usageFilter, @Nullable IBinder tokenFilter) { if (session == null) { return false; } + if (session instanceof VendorVibrationSession) { + // Vendor sessions should not be cancelled by Vibrator.cancel API. + return false; + } if ((tokenFilter != null) && (tokenFilter != session.getCallerToken())) { // Vibration from a different app, this should not cancel it. return false; @@ -1572,10 +1765,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { Trace.traceBegin(TRACE_TAG_VIBRATOR, "onVibrationThreadReleased"); try { synchronized (mLock) { - if (!(mCurrentVibration instanceof SingleVibrationSession session)) { + if (!(mCurrentSession instanceof SingleVibrationSession session)) { if (Build.IS_DEBUGGABLE) { Slog.wtf(TAG, "VibrationSession invalid on vibration thread release." - + " currentSession=" + mCurrentVibration); + + " currentSession=" + mCurrentSession); } // Only single vibration sessions are ended by thread being released. Abort. return; @@ -1586,11 +1779,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { + " expected=%d, released=%d", session.getVibration().id, vibrationId)); } - finishAppOpModeLocked(mCurrentVibration.getCallerInfo()); - clearCurrentVibrationLocked(); + finishAppOpModeLocked(mCurrentSession.getCallerInfo()); + clearCurrentSessionLocked(); Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0); - // Start next vibration if it's a single vibration waiting for the thread. - maybeStartNextSingleVibrationLocked(); + // Start next vibration if it's waiting for the thread. + maybeStartNextSessionLocked(); } } finally { Trace.traceEnd(TRACE_TAG_VIBRATOR); @@ -1613,10 +1806,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationReleased"); try { synchronized (mLock) { - if (!(mCurrentVibration instanceof ExternalVibrationSession session)) { + if (!(mCurrentSession instanceof ExternalVibrationSession session)) { if (Build.IS_DEBUGGABLE) { Slog.wtf(TAG, "VibrationSession invalid on external vibration release." - + " currentSession=" + mCurrentVibration); + + " currentSession=" + mCurrentSession); } // Only external vibration sessions are ended by this callback. Abort. return; @@ -1627,10 +1820,62 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { + " expected=%d, released=%d", session.id, vibrationId)); } setExternalControl(false, session.stats); - clearCurrentVibrationLocked(); - // Start next vibration if it's a single vibration waiting for the external - // control to be over. - maybeStartNextSingleVibrationLocked(); + clearCurrentSessionLocked(); + // Start next vibration if it's waiting for the external control to be over. + maybeStartNextSessionLocked(); + } + } finally { + Trace.traceEnd(TRACE_TAG_VIBRATOR); + } + } + } + + /** + * Implementation of {@link ExternalVibrationSession.VibratorManagerHooks} that controls + * external vibrations and reports them when finished. + */ + private final class VendorVibrationSessionCallbacks + implements VendorVibrationSession.VibratorManagerHooks { + + @Override + public void endSession(long sessionId, boolean shouldAbort) { + if (DEBUG) { + Slog.d(TAG, "Vibration session " + sessionId + + (shouldAbort ? " aborting" : " ending")); + } + Trace.traceBegin(TRACE_TAG_VIBRATOR, "endSession"); + try { + mNativeWrapper.endSession(sessionId, shouldAbort); + } finally { + Trace.traceEnd(TRACE_TAG_VIBRATOR); + } + } + + @Override + public void onSessionReleased(long sessionId) { + if (DEBUG) { + Slog.d(TAG, "Vibration session " + sessionId + " released"); + } + Trace.traceBegin(TRACE_TAG_VIBRATOR, "onVendorSessionReleased"); + try { + synchronized (mLock) { + if (!(mCurrentSession instanceof VendorVibrationSession session)) { + if (Build.IS_DEBUGGABLE) { + Slog.wtf(TAG, "VibrationSession invalid on vibration session release." + + " currentSession=" + mCurrentSession); + } + // Only vendor vibration sessions are ended by this callback. Abort. + return; + } + if (Build.IS_DEBUGGABLE && (session.getSessionId() != sessionId)) { + Slog.wtf(TAG, TextUtils.formatSimple( + "SessionId mismatch on vendor vibration session release." + + " expected=%d, released=%d", + session.getSessionId(), sessionId)); + } + clearCurrentSessionLocked(); + // Start next vibration if it's waiting for the HAL session to be over. + maybeStartNextSessionLocked(); } } finally { Trace.traceEnd(TRACE_TAG_VIBRATOR); @@ -1638,19 +1883,22 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } - /** Listener for synced vibration completion callbacks from native. */ + /** Listener for vibrator manager completion callbacks from native. */ @VisibleForTesting - interface OnSyncedVibrationCompleteListener { + interface VibratorManagerNativeCallbacks { /** Callback triggered when synced vibration is complete. */ - void onComplete(long vibrationId); + void onSyncedVibrationComplete(long vibrationId); + + /** Callback triggered when vibration session is complete. */ + void onVibrationSessionComplete(long sessionId); } /** * Implementation of listeners to native vibrators with a weak reference to this service. */ private static final class VibrationCompleteListener implements - VibratorController.OnVibrationCompleteListener, OnSyncedVibrationCompleteListener { + VibratorController.OnVibrationCompleteListener, VibratorManagerNativeCallbacks { private WeakReference<VibratorManagerService> mServiceRef; VibrationCompleteListener(VibratorManagerService service) { @@ -1658,7 +1906,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @Override - public void onComplete(long vibrationId) { + public void onSyncedVibrationComplete(long vibrationId) { VibratorManagerService service = mServiceRef.get(); if (service != null) { service.onSyncedVibrationComplete(vibrationId); @@ -1666,6 +1914,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @Override + public void onVibrationSessionComplete(long sessionId) { + VibratorManagerService service = mServiceRef.get(); + if (service != null) { + service.onVibrationSessionComplete(sessionId); + } + } + + @Override public void onComplete(int vibratorId, long vibrationId) { VibratorManagerService service = mServiceRef.get(); if (service != null) { @@ -1698,7 +1954,7 @@ 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(OnSyncedVibrationCompleteListener listener) { + public void init(VibratorManagerNativeCallbacks listener) { mNativeServicePtr = nativeInit(listener); long finalizerPtr = nativeGetFinalizer(); @@ -1734,6 +1990,21 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { public void cancelSynced() { nativeCancelSynced(mNativeServicePtr); } + + /** Start vibration session. */ + public boolean startSession(long sessionId, @NonNull int[] vibratorIds) { + return nativeStartSession(mNativeServicePtr, sessionId, vibratorIds); + } + + /** End vibration session. */ + public void endSession(long sessionId, boolean shouldAbort) { + nativeEndSession(mNativeServicePtr, sessionId, shouldAbort); + } + + /** Clear vibration sessions. */ + public void clearSessions() { + nativeClearSessions(mNativeServicePtr); + } } /** Keep records of vibrations played and provide debug information for this service. */ @@ -1853,46 +2124,46 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { /** Clears mNextVibration if set, ending it cleanly */ @GuardedBy("mLock") - private void clearNextVibrationLocked(Status status) { - clearNextVibrationLocked(status, /* endedBy= */ null); + private void clearNextSessionLocked(Status status) { + clearNextSessionLocked(status, /* endedBy= */ null); } /** Clears mNextVibration if set, ending it cleanly */ @GuardedBy("mLock") - private void clearNextVibrationLocked(Status status, CallerInfo endedBy) { - if (mNextVibration != null) { + private void clearNextSessionLocked(Status status, CallerInfo endedBy) { + if (mNextSession != null) { if (DEBUG) { - Slog.d(TAG, "Dropping pending vibration from " + mNextVibration.getCallerInfo() + Slog.d(TAG, "Dropping pending vibration from " + mNextSession.getCallerInfo() + " with status: " + status); } // Clearing next vibration before playing it, end it and report metrics right away. - endVibrationLocked(mNextVibration, status, endedBy); - mNextVibration = null; + endSessionLocked(mNextSession, status, endedBy); + mNextSession = null; } } /** Clears mCurrentVibration if set, reporting metrics */ @GuardedBy("mLock") - private void clearCurrentVibrationLocked() { - if (mCurrentVibration != null) { - mCurrentVibration.unlinkToDeath(); - logAndRecordVibration(mCurrentVibration.getDebugInfo()); - mCurrentVibration = null; + private void clearCurrentSessionLocked() { + if (mCurrentSession != null) { + mCurrentSession.unlinkToDeath(); + logAndRecordVibration(mCurrentSession.getDebugInfo()); + mCurrentSession = null; mLock.notify(); // Notify if waiting for current vibration to end. } } @GuardedBy("mLock") - private void maybeClearCurrentAndNextVibrationsLocked( + private void maybeClearCurrentAndNextSessionsLocked( Predicate<VibrationSession> shouldEndSessionPredicate, Status endStatus) { // TODO(b/372241975): investigate why external vibrations were not handled here before - if (!(mNextVibration instanceof ExternalVibrationSession) - && shouldEndSessionPredicate.test(mNextVibration)) { - clearNextVibrationLocked(endStatus); + if (!(mNextSession instanceof ExternalVibrationSession) + && shouldEndSessionPredicate.test(mNextSession)) { + clearNextSessionLocked(endStatus); } - if (!(mCurrentVibration instanceof ExternalVibrationSession) - && shouldEndSessionPredicate.test(mCurrentVibration)) { - mCurrentVibration.requestEnd(endStatus); + if (!(mCurrentSession instanceof ExternalVibrationSession) + && shouldEndSessionPredicate.test(mCurrentSession)) { + mCurrentSession.requestEnd(endStatus); } } @@ -1902,12 +2173,12 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { * * @return true if the vibration completed, or false if waiting timed out. */ - public boolean waitForCurrentVibrationEnd(long maxWaitMillis) { + public boolean waitForCurrentSessionEnd(long maxWaitMillis) { long now = SystemClock.elapsedRealtime(); long deadline = now + maxWaitMillis; synchronized (mLock) { while (true) { - if (mCurrentVibration == null) { + if (mCurrentSession == null) { return true; // Done } if (now >= deadline) { // Note that thread.wait(0) waits indefinitely. @@ -1965,7 +2236,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { synchronized (mLock) { if (!hasExternalControlCapability()) { - endVibrationLocked(session, Status.IGNORED_UNSUPPORTED); + endSessionLocked(session, Status.IGNORED_UNSUPPORTED); return session.getScale(); } @@ -1976,17 +2247,17 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { Slog.w(TAG, "pkg=" + vib.getPackage() + ", uid=" + vib.getUid() + " tried to play externally controlled vibration" + " without VIBRATE permission, ignoring."); - endVibrationLocked(session, Status.IGNORED_MISSING_PERMISSION); + endSessionLocked(session, Status.IGNORED_MISSING_PERMISSION); return session.getScale(); } Status ignoreStatus = shouldIgnoreVibrationLocked(session.callerInfo); if (ignoreStatus != null) { - endVibrationLocked(session, ignoreStatus); + endSessionLocked(session, ignoreStatus); return session.getScale(); } - if ((mCurrentVibration instanceof ExternalVibrationSession evs) + if ((mCurrentSession instanceof ExternalVibrationSession evs) && evs.isHoldingSameVibration(vib)) { // We are already playing this external vibration, so we can return the same // scale calculated in the previous call to this method. @@ -1994,17 +2265,17 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } // Check if ongoing vibration is more important than this vibration. - Vibration.EndInfo ignoreInfo = shouldIgnoreVibrationForOngoingLocked(session); + Vibration.EndInfo ignoreInfo = shouldIgnoreForOngoingLocked(session); if (ignoreInfo != null) { - endVibrationLocked(session, ignoreInfo.status, ignoreInfo.endedBy); + endSessionLocked(session, ignoreInfo.status, ignoreInfo.endedBy); return session.getScale(); } // First clear next request, so it won't start when the current one ends. - clearNextVibrationLocked(Status.IGNORED_FOR_EXTERNAL, session.callerInfo); - mNextVibration = session; + clearNextSessionLocked(Status.IGNORED_FOR_EXTERNAL, session.callerInfo); + mNextSession = session; - if (mCurrentVibration != null) { + if (mCurrentSession != null) { // Cancel any vibration that may be playing and ready the vibrator, even if // we have an externally controlled vibration playing already. // Since the interface defines that only one externally controlled @@ -2016,36 +2287,36 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // as we would need to mute the old one still if it came from a different // controller. session.stats.reportInterruptedAnotherVibration( - mCurrentVibration.getCallerInfo()); - mCurrentVibration.requestEnd(Status.CANCELLED_SUPERSEDED, + mCurrentSession.getCallerInfo()); + mCurrentSession.requestEnd(Status.CANCELLED_SUPERSEDED, session.callerInfo, /* immediate= */ true); waitForCompletion = true; } } // Wait for lock and interact with HAL to set external control outside main lock. if (waitForCompletion) { - if (!waitForCurrentVibrationEnd(VIBRATION_CANCEL_WAIT_MILLIS)) { + if (!waitForCurrentSessionEnd(VIBRATION_CANCEL_WAIT_MILLIS)) { Slog.e(TAG, "Timed out waiting for vibration to cancel"); synchronized (mLock) { - if (mNextVibration == session) { - mNextVibration = null; + if (mNextSession == session) { + mNextSession = null; } - endVibrationLocked(session, Status.IGNORED_ERROR_CANCELLING); + endSessionLocked(session, Status.IGNORED_ERROR_CANCELLING); return session.getScale(); } } } synchronized (mLock) { - if (mNextVibration == session) { + if (mNextSession == session) { // This is still the next vibration to be played. - mNextVibration = null; + mNextSession = null; } else { // A new request took the place of this one, maybe with higher importance. // Next vibration already cleared with the right status, just return here. return session.getScale(); } if (!session.linkToDeath()) { - endVibrationLocked(session, Status.IGNORED_ERROR_TOKEN); + endSessionLocked(session, Status.IGNORED_ERROR_TOKEN); return session.getScale(); } if (DEBUG) { @@ -2062,7 +2333,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // should be ignored or scaled. mVibrationSettings.update(); } - mCurrentVibration = session; + mCurrentSession = session; session.scale(mVibrationScaler, attrs.getUsage()); // Vibrator will start receiving data from external channels after this point. @@ -2080,12 +2351,12 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationStop"); try { synchronized (mLock) { - if ((mCurrentVibration instanceof ExternalVibrationSession evs) + if ((mCurrentSession instanceof ExternalVibrationSession evs) && evs.isHoldingSameVibration(vib)) { if (DEBUG) { Slog.d(TAG, "Stopping external vibration: " + vib); } - mCurrentVibration.requestEnd(Status.FINISHED); + mCurrentSession.requestEnd(Status.FINISHED); } } } finally { diff --git a/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp b/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp index a47ab9d27c17..46be79e7c097 100644 --- a/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp +++ b/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp @@ -16,27 +16,32 @@ #define LOG_TAG "VibratorManagerService" -#include <nativehelper/JNIHelp.h> -#include "android_runtime/AndroidRuntime.h" -#include "core_jni_helpers.h" -#include "jni.h" +#include "com_android_server_vibrator_VibratorManagerService.h" +#include <nativehelper/JNIHelp.h> #include <utils/Log.h> #include <utils/misc.h> - #include <vibratorservice/VibratorManagerHalController.h> -#include "com_android_server_vibrator_VibratorManagerService.h" +#include <unordered_map> + +#include "android_runtime/AndroidRuntime.h" +#include "core_jni_helpers.h" +#include "jni.h" namespace android { static JavaVM* sJvm = nullptr; -static jmethodID sMethodIdOnComplete; +static jmethodID sMethodIdOnSyncedVibrationComplete; +static jmethodID sMethodIdOnVibrationSessionComplete; static std::mutex gManagerMutex; static vibrator::ManagerHalController* gManager GUARDED_BY(gManagerMutex) = nullptr; class NativeVibratorManagerService { public: + using IVibrationSession = aidl::android::hardware::vibrator::IVibrationSession; + using VibrationSessionConfig = aidl::android::hardware::vibrator::VibrationSessionConfig; + NativeVibratorManagerService(JNIEnv* env, jobject callbackListener) : mHal(std::make_unique<vibrator::ManagerHalController>()), mCallbackListener(env->NewGlobalRef(callbackListener)) { @@ -52,15 +57,69 @@ public: vibrator::ManagerHalController* hal() const { return mHal.get(); } - std::function<void()> createCallback(jlong vibrationId) { + std::function<void()> createSyncedVibrationCallback(jlong vibrationId) { return [vibrationId, this]() { auto jniEnv = GetOrAttachJNIEnvironment(sJvm); - jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnComplete, vibrationId); + jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnSyncedVibrationComplete, + vibrationId); }; } + std::function<void()> createVibrationSessionCallback(jlong sessionId) { + return [sessionId, this]() { + auto jniEnv = GetOrAttachJNIEnvironment(sJvm); + jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnVibrationSessionComplete, + sessionId); + std::lock_guard<std::mutex> lock(mSessionMutex); + auto it = mSessions.find(sessionId); + if (it != mSessions.end()) { + mSessions.erase(it); + } + }; + } + + bool startSession(jlong sessionId, const std::vector<int32_t>& vibratorIds) { + VibrationSessionConfig config; + auto callback = createVibrationSessionCallback(sessionId); + auto result = hal()->startSession(vibratorIds, config, callback); + if (!result.isOk()) { + return false; + } + + std::lock_guard<std::mutex> lock(mSessionMutex); + mSessions[sessionId] = std::move(result.value()); + return true; + } + + void closeSession(jlong sessionId) { + std::lock_guard<std::mutex> lock(mSessionMutex); + auto it = mSessions.find(sessionId); + if (it != mSessions.end()) { + it->second->close(); + // Keep session, it can still be aborted. + } + } + + void abortSession(jlong sessionId) { + std::lock_guard<std::mutex> lock(mSessionMutex); + auto it = mSessions.find(sessionId); + if (it != mSessions.end()) { + it->second->abort(); + mSessions.erase(it); + } + } + + void clearSessions() { + hal()->clearSessions(); + std::lock_guard<std::mutex> lock(mSessionMutex); + mSessions.clear(); + } + private: + std::mutex mSessionMutex; const std::unique_ptr<vibrator::ManagerHalController> mHal; + std::unordered_map<jlong, std::shared_ptr<IVibrationSession>> mSessions + GUARDED_BY(mSessionMutex); const jobject mCallbackListener; }; @@ -142,7 +201,7 @@ static jboolean nativeTriggerSynced(JNIEnv* env, jclass /* clazz */, jlong servi ALOGE("nativeTriggerSynced failed because native service was not initialized"); return JNI_FALSE; } - auto callback = service->createCallback(vibrationId); + auto callback = service->createSyncedVibrationCallback(vibrationId); return service->hal()->triggerSynced(callback).isOk() ? JNI_TRUE : JNI_FALSE; } @@ -156,8 +215,47 @@ static void nativeCancelSynced(JNIEnv* env, jclass /* clazz */, jlong servicePtr service->hal()->cancelSynced(); } +static jboolean nativeStartSession(JNIEnv* env, jclass /* clazz */, jlong servicePtr, + jlong sessionId, jintArray vibratorIds) { + NativeVibratorManagerService* service = + reinterpret_cast<NativeVibratorManagerService*>(servicePtr); + if (service == nullptr) { + ALOGE("nativeStartSession 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->startSession(sessionId, ids) ? JNI_TRUE : JNI_FALSE; +} + +static void nativeEndSession(JNIEnv* env, jclass /* clazz */, jlong servicePtr, jlong sessionId, + jboolean shouldAbort) { + NativeVibratorManagerService* service = + reinterpret_cast<NativeVibratorManagerService*>(servicePtr); + if (service == nullptr) { + ALOGE("nativeEndSession failed because native service was not initialized"); + return; + } + if (shouldAbort) { + service->abortSession(sessionId); + } else { + service->closeSession(sessionId); + } +} + +static void nativeClearSessions(JNIEnv* env, jclass /* clazz */, jlong servicePtr) { + NativeVibratorManagerService* service = + reinterpret_cast<NativeVibratorManagerService*>(servicePtr); + if (service == nullptr) { + ALOGE("nativeClearSessions failed because native service was not initialized"); + return; + } + service->clearSessions(); +} + inline static constexpr auto sNativeInitMethodSignature = - "(Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener;)J"; + "(Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks;)J"; static const JNINativeMethod method_table[] = { {"nativeInit", sNativeInitMethodSignature, (void*)nativeInit}, @@ -167,15 +265,20 @@ static const JNINativeMethod method_table[] = { {"nativePrepareSynced", "(J[I)Z", (void*)nativePrepareSynced}, {"nativeTriggerSynced", "(JJ)Z", (void*)nativeTriggerSynced}, {"nativeCancelSynced", "(J)V", (void*)nativeCancelSynced}, + {"nativeStartSession", "(JJ[I)Z", (void*)nativeStartSession}, + {"nativeEndSession", "(JJZ)V", (void*)nativeEndSession}, + {"nativeClearSessions", "(J)V", (void*)nativeClearSessions}, }; int register_android_server_vibrator_VibratorManagerService(JavaVM* jvm, JNIEnv* env) { sJvm = jvm; auto listenerClassName = - "com/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener"; + "com/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks"; jclass listenerClass = FindClassOrDie(env, listenerClassName); - sMethodIdOnComplete = GetMethodIDOrDie(env, listenerClass, "onComplete", "(J)V"); - + sMethodIdOnSyncedVibrationComplete = + GetMethodIDOrDie(env, listenerClass, "onSyncedVibrationComplete", "(J)V"); + sMethodIdOnVibrationSessionComplete = + GetMethodIDOrDie(env, listenerClass, "onVibrationSessionComplete", "(J)V"); return jniRegisterNativeMethods(env, "com/android/server/vibrator/VibratorManagerService", method_table, NELEM(method_table)); } diff --git a/services/proguard.flags b/services/proguard.flags index cdd41abf6c7c..977bd19a7236 100644 --- a/services/proguard.flags +++ b/services/proguard.flags @@ -82,7 +82,7 @@ -keep,allowoptimization,allowaccessmodification class com.android.server.usb.UsbAlsaJackDetector { *; } -keep,allowoptimization,allowaccessmodification class com.android.server.usb.UsbAlsaMidiDevice { *; } -keep,allowoptimization,allowaccessmodification class com.android.server.vibrator.VibratorController$OnVibrationCompleteListener { *; } --keep,allowoptimization,allowaccessmodification class com.android.server.vibrator.VibratorManagerService$OnSyncedVibrationCompleteListener { *; } +-keep,allowoptimization,allowaccessmodification class com.android.server.vibrator.VibratorManagerService$VibratorManagerNativeCallbacks { *; } -keepclasseswithmembers,allowoptimization,allowaccessmodification class com.android.server.** { *** *FromNative(...); } diff --git a/services/tests/vibrator/AndroidManifest.xml b/services/tests/vibrator/AndroidManifest.xml index c0f514fb9673..850884f84b01 100644 --- a/services/tests/vibrator/AndroidManifest.xml +++ b/services/tests/vibrator/AndroidManifest.xml @@ -32,6 +32,9 @@ <uses-permission android:name="android.permission.VIBRATE_ALWAYS_ON" /> <!-- Required to play system-only haptic feedback constants --> <uses-permission android:name="android.permission.VIBRATE_SYSTEM_CONSTANTS" /> + <!-- Required to play vendor effects and start vendor sessions --> + <uses-permission android:name="android.permission.VIBRATE_VENDOR_EFFECTS" /> + <uses-permission android:name="android.permission.START_VIBRATION_SESSIONS" /> <application android:debuggable="true"> <uses-library android:name="android.test.mock" android:required="true" /> diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java index dfdd0cde6aba..88ba9e3af6df 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -35,6 +35,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -83,6 +84,8 @@ import android.os.Vibrator; import android.os.VibratorInfo; import android.os.test.FakeVibrator; import android.os.test.TestLooper; +import android.os.vibrator.IVibrationSession; +import android.os.vibrator.IVibrationSessionCallback; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.StepSegment; @@ -195,6 +198,7 @@ public class VibratorManagerServiceTest { new SparseArray<>(); private final List<HalVibration> mPendingVibrations = new ArrayList<>(); + private final List<VendorVibrationSession> mPendingSessions = new ArrayList<>(); private VibratorManagerService mService; private Context mContextSpy; @@ -264,6 +268,11 @@ public class VibratorManagerServiceTest { grantPermission(android.Manifest.permission.VIBRATE); // Cancel any pending vibration from tests, including external vibrations. cancelVibrate(mService); + // End pending sessions. + for (VendorVibrationSession session : mPendingSessions) { + session.cancelSession(); + } + mTestLooper.dispatchAll(); // Wait until pending vibrations end asynchronously. for (HalVibration vibration : mPendingVibrations) { vibration.waitForEnd(); @@ -1229,6 +1238,36 @@ public class VibratorManagerServiceTest { .anyMatch(PrebakedSegment.class::isInstance)); } + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @Test + public void vibrate_withOngoingHigherImportanceSession_ignoresEffect() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + assertThat(session.getStatus()).isEqualTo(Status.RUNNING); + verify(callback).onStarted(any(IVibrationSession.class)); + + HalVibration vibration = vibrateAndWaitUntilFinished(service, + VibrationEffect.get(VibrationEffect.EFFECT_CLICK), + HAPTIC_FEEDBACK_ATTRS); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.RUNNING); + assertThat(vibration.getStatus()).isEqualTo(Status.IGNORED_FOR_HIGHER_IMPORTANCE); + verify(callback, never()).onFinishing(); + verify(callback, never()).onFinished(anyInt()); + // The second vibration shouldn't have played any prebaked segment. + assertFalse(fakeVibrator.getAllEffectSegments().stream() + .anyMatch(PrebakedSegment.class::isInstance)); + } + @Test public void vibrate_withOngoingLowerImportanceVibration_cancelsOngoingEffect() throws Exception { @@ -1289,6 +1328,36 @@ public class VibratorManagerServiceTest { .filter(PrebakedSegment.class::isInstance).count()); } + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @Test + public void vibrate_withOngoingLowerImportanceSession_cancelsOngoingSession() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VendorVibrationSession session = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + assertThat(session.getStatus()).isEqualTo(Status.RUNNING); + verify(callback).onStarted(any(IVibrationSession.class)); + + HalVibration vibration = vibrateAndWaitUntilFinished(service, + VibrationEffect.get(VibrationEffect.EFFECT_CLICK), + HAPTIC_FEEDBACK_ATTRS); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED); + assertThat(vibration.getStatus()).isEqualTo(Status.FINISHED); + verify(callback).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED)); + // One segment played is the prebaked CLICK from the new vibration. + assertEquals(1, mVibratorProviders.get(1).getAllEffectSegments().stream() + .filter(PrebakedSegment.class::isInstance).count()); + } + @Test public void vibrate_withOngoingSameImportancePipelinedVibration_continuesOngoingEffect() throws Exception { @@ -1416,16 +1485,16 @@ public class VibratorManagerServiceTest { // The native callback will be dispatched manually in this test. mTestLooper.stopAutoDispatchAndIgnoreExceptions(); - ArgumentCaptor<VibratorManagerService.OnSyncedVibrationCompleteListener> listenerCaptor = + ArgumentCaptor<VibratorManagerService.VibratorManagerNativeCallbacks> listenerCaptor = ArgumentCaptor.forClass( - VibratorManagerService.OnSyncedVibrationCompleteListener.class); + VibratorManagerService.VibratorManagerNativeCallbacks.class); verify(mNativeWrapperMock).init(listenerCaptor.capture()); CountDownLatch triggerCountDown = new CountDownLatch(1); // Mock trigger callback on registered listener right after the synced vibration starts. when(mNativeWrapperMock.prepareSynced(eq(new int[]{1, 2}))).thenReturn(true); when(mNativeWrapperMock.triggerSynced(anyLong())).then(answer -> { - listenerCaptor.getValue().onComplete(answer.getArgument(0)); + listenerCaptor.getValue().onSyncedVibrationComplete(answer.getArgument(0)); triggerCountDown.countDown(); return true; }); @@ -2318,6 +2387,34 @@ public class VibratorManagerServiceTest { assertEquals(Arrays.asList(false), mVibratorProviders.get(1).getExternalControlStates()); } + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @Test + public void onExternalVibration_withOngoingHigherImportanceSession_ignoreNewVibration() + throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + verify(callback).onStarted(any(IVibrationSession.class)); + + ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, + AUDIO_ALARM_ATTRS, mock(IExternalVibrationController.class)); + ExternalVibrationScale scale = + mExternalVibratorService.onExternalVibrationStart(externalVibration); + // External vibration is ignored. + assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); + + // Session still running. + assertThat(session.getStatus()).isEqualTo(Status.RUNNING); + verify(callback, never()).onFinishing(); + verify(callback, never()).onFinished(anyInt()); + } + @Test public void onExternalVibration_withNewSameImportanceButRepeating_cancelsOngoingVibration() throws Exception { @@ -2373,6 +2470,36 @@ public class VibratorManagerServiceTest { assertEquals(Arrays.asList(false), mVibratorProviders.get(1).getExternalControlStates()); } + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @Test + public void onExternalVibration_withOngoingLowerImportanceSession_cancelsOngoingSession() + throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VendorVibrationSession session = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + verify(callback).onStarted(any(IVibrationSession.class)); + + ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, + AUDIO_ALARM_ATTRS, mock(IExternalVibrationController.class)); + ExternalVibrationScale scale = + mExternalVibratorService.onExternalVibrationStart(externalVibration); + assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); + mTestLooper.dispatchAll(); + + // Session is cancelled. + assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED); + verify(callback).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED)); + assertEquals(Arrays.asList(false, true), + mVibratorProviders.get(1).getExternalControlStates()); + } + @Test public void onExternalVibration_withRingtone_usesRingerModeSettings() { mockVibrators(1); @@ -2638,6 +2765,376 @@ public class VibratorManagerServiceTest { } @Test + @DisableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withoutFeatureFlag_throwsException() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + int vibratorId = 1; + mockVibrators(vibratorId); + VibratorManagerService service = createSystemReadyService(); + + IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class); + assertThrows("Expected starting session without feature flag to fail!", + UnsupportedOperationException.class, + () -> startSession(service, RINGTONE_ATTRS, callback, vibratorId)); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class)); + verify(callback, never()).onStarted(any(IVibrationSession.class)); + verify(callback, never()).onFinishing(); + verify(callback, never()).onFinished(anyInt()); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withoutCapability_doesNotStart() throws Exception { + int vibratorId = 1; + mockVibrators(vibratorId); + VibratorManagerService service = createSystemReadyService(); + + IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class); + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, + callback, vibratorId); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED); + verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class)); + verify(callback, never()).onFinishing(); + verify(callback) + .onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_UNSUPPORTED)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withoutCallback_doesNotStart() { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + int vibratorId = 1; + mockVibrators(vibratorId); + VibratorManagerService service = createSystemReadyService(); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, + /* callback= */ null, vibratorId); + mTestLooper.dispatchAll(); + + assertThat(session).isNull(); + verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withoutVibratorIds_doesNotStart() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + VibratorManagerService service = createSystemReadyService(); + + int[] nullIds = null; + IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class); + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, nullIds); + assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED); + + int[] emptyIds = {}; + session = startSession(service, RINGTONE_ATTRS, callback, emptyIds); + assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED); + + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class)); + verify(callback, never()).onFinishing(); + verify(callback, times(2)) + .onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_UNSUPPORTED)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_badVibratorId_failsToStart() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1, 2); + when(mNativeWrapperMock.startSession(anyLong(), any(int[].class))).thenReturn(false); + doReturn(false).when(mNativeWrapperMock).startSession(anyLong(), eq(new int[] {1, 3})); + doReturn(true).when(mNativeWrapperMock).startSession(anyLong(), eq(new int[] {1, 2})); + VibratorManagerService service = createSystemReadyService(); + + IBinder token = mock(IBinder.class); + IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class); + doReturn(token).when(callback).asBinder(); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 3); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED); + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 3})); + verify(callback, never()).onStarted(any(IVibrationSession.class)); + verify(callback, never()).onFinishing(); + verify(callback) + .onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_UNSUPPORTED)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_thenFinish_returnsSuccessAfterCallback() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1, 2); + VibratorManagerService service = createSystemReadyService(); + int sessionFinishDelayMs = 200; + IVibrationSessionCallback callback = mockSessionCallbacks(sessionFinishDelayMs); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2})); + ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class); + verify(callback).onStarted(captor.capture()); + + captor.getValue().finishSession(); + + // Session not ended until HAL callback. + assertThat(session.getStatus()).isEqualTo(Status.RUNNING); + + // Dispatch HAL callbacks. + mTestLooper.moveTimeForward(sessionFinishDelayMs); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.FINISHED); + verify(callback).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_thenSendCancelSignal_cancelsSession() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1, 2); + VibratorManagerService service = createSystemReadyService(); + int sessionFinishDelayMs = 200; + IVibrationSessionCallback callback = mockSessionCallbacks(sessionFinishDelayMs); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2})); + ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class); + verify(callback).onStarted(captor.capture()); + + session.getCancellationSignal().cancel(); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_BY_USER); + verify(callback).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_thenCancel_returnsCancelStatus() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1, 2); + VibratorManagerService service = createSystemReadyService(); + // Delay not applied when session is aborted. + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2})); + ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class); + verify(callback).onStarted(captor.capture()); + + captor.getValue().cancelSession(); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_BY_USER); + verify(callback).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_finishThenCancel_returnsRightAwayWithFinishedStatus() + throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1, 2); + VibratorManagerService service = createSystemReadyService(); + // Delay not applied when session is aborted. + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2})); + ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class); + verify(callback).onStarted(captor.capture()); + + captor.getValue().finishSession(); + mTestLooper.dispatchAll(); + assertThat(session.getStatus()).isEqualTo(Status.RUNNING); + + captor.getValue().cancelSession(); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.FINISHED); + verify(callback).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_thenHalCancels_returnsCancelStatus() + throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1, 2); + VibratorManagerService service = createSystemReadyService(); + ArgumentCaptor<VibratorManagerService.VibratorManagerNativeCallbacks> listenerCaptor = + ArgumentCaptor.forClass( + VibratorManagerService.VibratorManagerNativeCallbacks.class); + verify(mNativeWrapperMock).init(listenerCaptor.capture()); + doReturn(true).when(mNativeWrapperMock).startSession(anyLong(), any(int[].class)); + + IBinder token = mock(IBinder.class); + IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class); + doReturn(token).when(callback).asBinder(); + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2})); + verify(callback).onStarted(any(IVibrationSession.class)); + + // Mock HAL ending session unexpectedly. + listenerCaptor.getValue().onVibrationSessionComplete(session.getSessionId()); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_BY_UNKNOWN_REASON); + verify(callback).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withPowerMode_usesPowerModeState() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE); + VendorVibrationSession session1 = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1); + VendorVibrationSession session2 = startSession(service, RINGTONE_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + + ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class); + verify(callback).onStarted(captor.capture()); + captor.getValue().cancelSession(); + mTestLooper.dispatchAll(); + + mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE); + VendorVibrationSession session3 = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock, never()) + .startSession(eq(session1.getSessionId()), any(int[].class)); + verify(mNativeWrapperMock).startSession(eq(session2.getSessionId()), eq(new int[] {1})); + verify(mNativeWrapperMock).startSession(eq(session3.getSessionId()), eq(new int[] {1})); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withOngoingHigherImportanceVibration_ignoresSession() + throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VibrationEffect effect = VibrationEffect.createWaveform( + new long[]{10, 10_000}, new int[]{128, 255}, -1); + vibrate(service, effect, ALARM_ATTRS); + + // VibrationThread will start this vibration async. + // Wait until second step started to ensure the noteVibratorOn was triggered. + assertTrue(waitUntil(s -> fakeVibrator.getAmplitudes().size() == 2, + service, TEST_TIMEOUT_MILLIS)); + + VendorVibrationSession session = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock, never()) + .startSession(eq(session.getSessionId()), any(int[].class)); + assertThat(session.getStatus()).isEqualTo(Status.IGNORED_FOR_HIGHER_IMPORTANCE); + verify(callback, never()).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_IGNORED)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withOngoingLowerImportanceVibration_cancelsOngoing() + throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VibrationEffect effect = VibrationEffect.createWaveform( + new long[]{10, 10_000}, new int[]{128, 255}, -1); + HalVibration vibration = vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS); + + // VibrationThread will start this vibration async. + // Wait until second step started to ensure the noteVibratorOn was triggered. + assertTrue(waitUntil(s -> fakeVibrator.getAmplitudes().size() == 2, service, + TEST_TIMEOUT_MILLIS)); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1); + vibration.waitForEnd(); + assertTrue(waitUntil(s -> session.isStarted(), service, TEST_TIMEOUT_MILLIS)); + mTestLooper.dispatchAll(); + + assertThat(vibration.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED); + assertThat(session.getStatus()).isEqualTo(Status.RUNNING); + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] { 1 })); + verify(callback).onStarted(any(IVibrationSession.class)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withOngoingLowerImportanceExternalVibration_cancelsOngoing() + throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); + mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK); + setRingerMode(AudioManager.RINGER_MODE_NORMAL); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + IBinder firstToken = mock(IBinder.class); + IExternalVibrationController controller = mock(IExternalVibrationController.class); + ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, + AUDIO_ALARM_ATTRS, + controller, firstToken); + ExternalVibrationScale scale = + mExternalVibratorService.onExternalVibrationStart(externalVibration); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + + assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); + // The external vibration should have been cancelled + verify(controller).mute(); + assertEquals(Arrays.asList(false, true, false), + mVibratorProviders.get(1).getExternalControlStates()); + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] { 1 })); + verify(callback).onStarted(any(IVibrationSession.class)); + } + + @Test public void frameworkStats_externalVibration_reportsAllMetrics() throws Exception { mockVibrators(1); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); @@ -3050,6 +3547,30 @@ public class VibratorManagerServiceTest { when(mNativeWrapperMock.getVibratorIds()).thenReturn(vibratorIds); } + private IVibrationSessionCallback mockSessionCallbacks(long delayToEndSessionMillis) { + Handler handler = new Handler(mTestLooper.getLooper()); + ArgumentCaptor<VibratorManagerService.VibratorManagerNativeCallbacks> listenerCaptor = + ArgumentCaptor.forClass( + VibratorManagerService.VibratorManagerNativeCallbacks.class); + verify(mNativeWrapperMock).init(listenerCaptor.capture()); + doReturn(true).when(mNativeWrapperMock).startSession(anyLong(), any(int[].class)); + doAnswer(args -> { + handler.postDelayed( + () -> listenerCaptor.getValue().onVibrationSessionComplete(args.getArgument(0)), + delayToEndSessionMillis); + return null; + }).when(mNativeWrapperMock).endSession(anyLong(), eq(false)); + doAnswer(args -> { + listenerCaptor.getValue().onVibrationSessionComplete(args.getArgument(0)); + return null; + }).when(mNativeWrapperMock).endSession(anyLong(), eq(true)); + + IBinder token = mock(IBinder.class); + IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class); + doReturn(token).when(callback).asBinder(); + return callback; + } + private void cancelVibrate(VibratorManagerService service) { service.cancelVibrate(VibrationAttributes.USAGE_FILTER_MATCH_ALL, service); } @@ -3157,6 +3678,16 @@ public class VibratorManagerServiceTest { return vib; } + private VendorVibrationSession startSession(VibratorManagerService service, + VibrationAttributes attrs, IVibrationSessionCallback callback, int... vibratorIds) { + VendorVibrationSession session = service.startVendorVibrationSessionInternal(UID, + Context.DEVICE_ID_DEFAULT, PACKAGE_NAME, vibratorIds, attrs, "reason", callback); + if (session != null) { + mPendingSessions.add(session); + } + return session; + } + private boolean waitUntil(Predicate<VibratorManagerService> predicate, VibratorManagerService service, long timeout) throws InterruptedException { long timeoutTimestamp = SystemClock.uptimeMillis() + timeout; diff --git a/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java b/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java index 07b733830bd3..0da4521fca71 100644 --- a/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java +++ b/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java @@ -143,6 +143,38 @@ public class VibratorManagerServicePermissionTest { } @Test + public void testStartVendorVibrationSessionWithoutVibratePermissionFails() throws Exception { + getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( + Manifest.permission.VIBRATE_VENDOR_EFFECTS, + Manifest.permission.START_VIBRATION_SESSIONS); + expectSecurityException("VIBRATE"); + mVibratorService.startVendorVibrationSession(Process.myUid(), DEVICE_ID, PACKAGE_NAME, + new int[] { 1 }, ATTRS, "testVibrate", null); + } + + @Test + public void testStartVendorVibrationSessionWithoutVibrateVendorEffectsPermissionFails() + throws Exception { + getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( + Manifest.permission.VIBRATE, + Manifest.permission.START_VIBRATION_SESSIONS); + expectSecurityException("VIBRATE"); + mVibratorService.startVendorVibrationSession(Process.myUid(), DEVICE_ID, PACKAGE_NAME, + new int[] { 1 }, ATTRS, "testVibrate", null); + } + + @Test + public void testStartVendorVibrationSessionWithoutStartSessionPermissionFails() + throws Exception { + getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( + Manifest.permission.VIBRATE, + Manifest.permission.VIBRATE_VENDOR_EFFECTS); + expectSecurityException("VIBRATE"); + mVibratorService.startVendorVibrationSession(Process.myUid(), DEVICE_ID, PACKAGE_NAME, + new int[] { 1 }, ATTRS, "testVibrate", null); + } + + @Test public void testCancelVibrateFails() throws RemoteException { expectSecurityException("VIBRATE"); mVibratorService.cancelVibrate(/* usageFilter= */ -1, new Binder()); |