diff options
Diffstat (limited to 'services')
12 files changed, 1607 insertions, 162 deletions
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; |