Introduce VendorVibrationSession
Introduce Vibrator APIs to start vendor vibration sessions.
Vendor sessions will play vibration requests without resetting the
vibrator hardware state, allowing the vendor to customize the behaviour
between vibrations.
Vibration sessions can be ended by the client app or by the vibrator
service, allowing higher priority vibrations (e.g. ringtone, alarms) to
take control of the vibrator and play as usual.
Bug: 345414356
Test: FrameworksVibratorServicesTests
Flag: android.os.vibrator.vendor_vibration_effects
Change-Id: Id749cc240201bf398526c2416d5767a364b86cbf
diff --git a/services/art-profile b/services/art-profile
index 6fa4c88..ce1e2c6 100644
--- a/services/art-profile
+++ b/services/art-profile
@@ -5657,7 +5657,7 @@
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 47bdb13..1e3090f 100644
--- a/services/art-wear-profile
+++ b/services/art-wear-profile
@@ -1330,7 +1330,7 @@
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;->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 df44e50..a92ac67 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 @@
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 @@
}
@Override
+ public long getSessionId() {
+ return mSessionId;
+ }
+
+ @Override
public long getCreateUptimeMillis() {
return stats.getCreateUptimeMillis();
}
@@ -148,7 +154,12 @@
@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 @@
@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 67ba25f..628221b 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 @@
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 @@
}
@Override
+ public long getSessionId() {
+ return mSessionId;
+ }
+
+ @Override
public long getCreateUptimeMillis() {
return mVibration.stats.getCreateUptimeMillis();
}
@@ -155,9 +161,15 @@
}
@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 0000000..07478e3
--- /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 bb2a17c..27f92b2 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.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 @@
* 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 @@
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 b511ba8..ae95a70 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.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 @@
*/
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 @@
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 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 ff34911..4764481 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.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.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.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 @@
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 @@
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 @@
// 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 @@
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 @@
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 @@
// 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 @@
}
@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 @@
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 @@
// 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 @@
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 @@
}
} 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 @@
// 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,14 +707,14 @@
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);
@@ -699,6 +725,141 @@
}
}
+ @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);
+ }
+ }
+
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
@@ -747,8 +908,8 @@
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 @@
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 @@
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 @@
}
// 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 @@
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 @@
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 @@
}
@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 @@
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 @@
}
}
+ 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 @@
*/
@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 @@
}
}
- 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 @@
* @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 @@
* @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 @@
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 @@
+ " 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 @@
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,9 @@
+ " 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);
@@ -1638,19 +1830,75 @@
}
}
- /** Listener for synced vibration completion callbacks from native. */
+ /**
+ * 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);
+ }
+ }
+ }
+
+ /** 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 @@
}
@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 @@
}
@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 @@
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 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 @@
/** 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 @@
*
* @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 @@
synchronized (mLock) {
if (!hasExternalControlCapability()) {
- endVibrationLocked(session, Status.IGNORED_UNSUPPORTED);
+ endSessionLocked(session, Status.IGNORED_UNSUPPORTED);
return session.getScale();
}
@@ -1976,17 +2247,17 @@
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 @@
}
// 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 @@
// 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 @@
// 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 @@
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 a47ab9d..46be79e 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 "com_android_server_vibrator_VibratorManagerService.h"
+
#include <nativehelper/JNIHelp.h>
+#include <utils/Log.h>
+#include <utils/misc.h>
+#include <vibratorservice/VibratorManagerHalController.h>
+
+#include <unordered_map>
+
#include "android_runtime/AndroidRuntime.h"
#include "core_jni_helpers.h"
#include "jni.h"
-#include <utils/Log.h>
-#include <utils/misc.h>
-
-#include <vibratorservice/VibratorManagerHalController.h>
-
-#include "com_android_server_vibrator_VibratorManagerService.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 @@
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 @@
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 @@
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 @@
{"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 cdd41ab..977bd19 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 c0f514f..850884f 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 dfdd0cd..88ba9e3 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.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.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 @@
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 @@
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 @@
.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 @@
.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 @@
// 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 @@
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 @@
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 @@
}
@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 @@
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 @@
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;