blob: 45bd1521bc35d8fe01eb55fd839bb7a1eb7783c6 [file] [log] [blame]
/*
* Copyright (C) 2021 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 android.os.VibrationEffect.VibrationParameter.targetAmplitude;
import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.hardware.vibrator.IVibrator;
import android.os.BatteryStats;
import android.os.Binder;
import android.os.Build;
import android.os.CombinedVibration;
import android.os.ExternalVibration;
import android.os.Handler;
import android.os.IBinder;
import android.os.IExternalVibratorService;
import android.os.IVibratorManagerService;
import android.os.IVibratorStateListener;
import android.os.Looper;
import android.os.PowerManager;
import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.ShellCallback;
import android.os.ShellCommand;
import android.os.SystemClock;
import android.os.Trace;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.VibratorInfo;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.os.vibrator.VibratorInfoFactory;
import android.os.vibrator.persistence.ParsedVibration;
import android.os.vibrator.persistence.VibrationXmlParser;
import android.text.TextUtils;
import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
import android.view.Display;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.SystemService;
import libcore.util.NativeAllocationRegistry;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.lang.ref.WeakReference;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
/** System implementation of {@link IVibratorManagerService}. */
public class VibratorManagerService extends IVibratorManagerService.Stub {
private static final String TAG = "VibratorManagerService";
private static final String EXTERNAL_VIBRATOR_SERVICE = "external_vibrator_service";
private static final boolean DEBUG = false;
private static final VibrationAttributes DEFAULT_ATTRIBUTES =
new VibrationAttributes.Builder().build();
private static final int ATTRIBUTES_ALL_BYPASS_FLAGS =
VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY
| VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF;
/** Fixed large duration used to note repeating vibrations to {@link IBatteryStats}. */
private static final long BATTERY_STATS_REPEATING_VIBRATION_DURATION = 5_000;
/**
* Maximum millis to wait for a vibration thread cancellation to "clean up" and finish, when
* blocking for an external vibration. In practice, this should be plenty.
*/
private static final long VIBRATION_CANCEL_WAIT_MILLIS = 5000;
/** Lifecycle responsible for initializing this class at the right system server phases. */
public static class Lifecycle extends SystemService {
private VibratorManagerService mService;
public Lifecycle(Context context) {
super(context);
}
@Override
public void onStart() {
mService = new VibratorManagerService(getContext(), new Injector());
publishBinderService(Context.VIBRATOR_MANAGER_SERVICE, mService);
}
@Override
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
mService.systemReady();
}
}
}
private final Object mLock = new Object();
private final Context mContext;
private final Injector mInjector;
private final PowerManager.WakeLock mWakeLock;
private final IBatteryStats mBatteryStatsService;
private final VibratorFrameworkStatsLogger mFrameworkStatsLogger;
private final Handler mHandler;
private final VibrationThread mVibrationThread;
private final AppOpsManager mAppOps;
private final NativeWrapper mNativeWrapper;
private final VibratorManagerRecords mVibratorManagerRecords;
private final long mCapabilities;
private final int[] mVibratorIds;
private final SparseArray<VibratorController> mVibrators;
private final VibrationThreadCallbacks mVibrationThreadCallbacks =
new VibrationThreadCallbacks();
@GuardedBy("mLock")
private final SparseArray<AlwaysOnVibration> mAlwaysOnEffects = new SparseArray<>();
@GuardedBy("mLock")
private VibrationStepConductor mCurrentVibration;
@GuardedBy("mLock")
private VibrationStepConductor mNextVibration;
@GuardedBy("mLock")
private ExternalVibrationHolder mCurrentExternalVibration;
@GuardedBy("mLock")
private boolean mServiceReady;
private final VibrationSettings mVibrationSettings;
private final VibrationScaler mVibrationScaler;
private final InputDeviceDelegate mInputDeviceDelegate;
private final DeviceAdapter mDeviceAdapter;
@GuardedBy("mLock")
@Nullable private VibratorInfo mCombinedVibratorInfo;
@GuardedBy("mLock")
@Nullable private HapticFeedbackVibrationProvider mHapticFeedbackVibrationProvider;
private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
synchronized (mLock) {
// When the system is entering a non-interactive state, we want to cancel
// vibrations in case a misbehaving app has abandoned them.
if (shouldCancelOnScreenOffLocked(mNextVibration)) {
clearNextVibrationLocked(
new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SCREEN_OFF));
}
if (shouldCancelOnScreenOffLocked(mCurrentVibration)) {
mCurrentVibration.notifyCancelled(
new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SCREEN_OFF),
/* immediate= */ false);
}
}
}
}
};
static native long nativeInit(OnSyncedVibrationCompleteListener listener);
static native long nativeGetFinalizer();
static native long nativeGetCapabilities(long nativeServicePtr);
static native int[] nativeGetVibratorIds(long nativeServicePtr);
static native boolean nativePrepareSynced(long nativeServicePtr, int[] vibratorIds);
static native boolean nativeTriggerSynced(long nativeServicePtr, long vibrationId);
static native void nativeCancelSynced(long nativeServicePtr);
@VisibleForTesting
VibratorManagerService(Context context, Injector injector) {
mContext = context;
mInjector = injector;
mHandler = injector.createHandler(Looper.myLooper());
mVibrationSettings = new VibrationSettings(mContext, mHandler);
mVibrationScaler = new VibrationScaler(mContext, mVibrationSettings);
mInputDeviceDelegate = new InputDeviceDelegate(mContext, mHandler);
VibrationCompleteListener listener = new VibrationCompleteListener(this);
mNativeWrapper = injector.getNativeWrapper();
mNativeWrapper.init(listener);
int recentDumpSizeLimit = mContext.getResources().getInteger(
com.android.internal.R.integer.config_recentVibrationsDumpSizeLimit);
int dumpSizeLimit = mContext.getResources().getInteger(
com.android.internal.R.integer.config_previousVibrationsDumpSizeLimit);
int dumpAggregationTimeLimit = mContext.getResources().getInteger(
com.android.internal.R.integer
.config_previousVibrationsDumpAggregationTimeMillisLimit);
mVibratorManagerRecords = new VibratorManagerRecords(
recentDumpSizeLimit, dumpSizeLimit, dumpAggregationTimeLimit);
mBatteryStatsService = injector.getBatteryStatsService();
mFrameworkStatsLogger = injector.getFrameworkStatsLogger(mHandler);
mAppOps = mContext.getSystemService(AppOpsManager.class);
PowerManager pm = context.getSystemService(PowerManager.class);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*vibrator*");
mWakeLock.setReferenceCounted(true);
mVibrationThread = new VibrationThread(mWakeLock, mVibrationThreadCallbacks);
mVibrationThread.start();
// Load vibrator hardware info. The vibrator ids and manager capabilities are loaded only
// once and assumed unchanged for the lifecycle of this service. Each individual vibrator
// can still retry loading each individual vibrator hardware spec once more at systemReady.
mCapabilities = mNativeWrapper.getCapabilities();
int[] vibratorIds = mNativeWrapper.getVibratorIds();
if (vibratorIds == null) {
mVibratorIds = new int[0];
mVibrators = new SparseArray<>(0);
} else {
// Keep original vibrator id order, which might be meaningful.
mVibratorIds = vibratorIds;
mVibrators = new SparseArray<>(mVibratorIds.length);
for (int vibratorId : vibratorIds) {
mVibrators.put(vibratorId, injector.createVibratorController(vibratorId, listener));
}
}
// Load vibrator adapter, that depends on hardware info.
mDeviceAdapter = new DeviceAdapter(mVibrationSettings, mVibrators);
// Reset the hardware to a default state, in case this is a runtime restart instead of a
// fresh boot.
mNativeWrapper.cancelSynced();
for (int i = 0; i < mVibrators.size(); i++) {
mVibrators.valueAt(i).reset();
}
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_OFF);
context.registerReceiver(mIntentReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
injector.addService(EXTERNAL_VIBRATOR_SERVICE, new ExternalVibratorService());
}
/** Finish initialization at boot phase {@link SystemService#PHASE_SYSTEM_SERVICES_READY}. */
@VisibleForTesting
void systemReady() {
Slog.v(TAG, "Initializing VibratorManager service...");
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "systemReady");
try {
// Will retry to load each vibrator's info, if any request have failed.
for (int i = 0; i < mVibrators.size(); i++) {
mVibrators.valueAt(i).reloadVibratorInfoIfNeeded();
}
mVibrationSettings.onSystemReady();
mInputDeviceDelegate.onSystemReady();
mVibrationSettings.addListener(this::updateServiceState);
// Will update settings and input devices.
updateServiceState();
} finally {
synchronized (mLock) {
mServiceReady = true;
}
Slog.v(TAG, "VibratorManager service initialized");
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
@Override // Binder call
public int[] getVibratorIds() {
return Arrays.copyOf(mVibratorIds, mVibratorIds.length);
}
@Override // Binder call
@Nullable
public VibratorInfo getVibratorInfo(int vibratorId) {
final VibratorController controller = mVibrators.get(vibratorId);
if (controller == null) {
return null;
}
final VibratorInfo info = controller.getVibratorInfo();
synchronized (mLock) {
if (mServiceReady) {
return info;
}
}
// If the service is not ready and the load was unsuccessful then return null while waiting
// for the service to be ready. It will retry to load the complete info from the HAL.
return controller.isVibratorInfoLoadSuccessful() ? info : null;
}
@android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE)
@Override // Binder call
public boolean isVibrating(int vibratorId) {
isVibrating_enforcePermission();
VibratorController controller = mVibrators.get(vibratorId);
return controller != null && controller.isVibrating();
}
@android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE)
@Override // Binder call
public boolean registerVibratorStateListener(int vibratorId, IVibratorStateListener listener) {
registerVibratorStateListener_enforcePermission();
VibratorController controller = mVibrators.get(vibratorId);
if (controller == null) {
return false;
}
return controller.registerVibratorStateListener(listener);
}
@android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE)
@Override // Binder call
public boolean unregisterVibratorStateListener(int vibratorId,
IVibratorStateListener listener) {
unregisterVibratorStateListener_enforcePermission();
VibratorController controller = mVibrators.get(vibratorId);
if (controller == null) {
return false;
}
return controller.unregisterVibratorStateListener(listener);
}
@Override // Binder call
public boolean setAlwaysOnEffect(int uid, String opPkg, int alwaysOnId,
@Nullable CombinedVibration effect, @Nullable VibrationAttributes attrs) {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "setAlwaysOnEffect");
try {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.VIBRATE_ALWAYS_ON,
"setAlwaysOnEffect");
if (effect == null) {
synchronized (mLock) {
mAlwaysOnEffects.delete(alwaysOnId);
onAllVibratorsLocked(v -> {
if (v.hasCapability(IVibrator.CAP_ALWAYS_ON_CONTROL)) {
v.updateAlwaysOn(alwaysOnId, /* effect= */ null);
}
});
}
return true;
}
if (!isEffectValid(effect)) {
return false;
}
attrs = fixupVibrationAttributes(attrs, effect);
synchronized (mLock) {
SparseArray<PrebakedSegment> effects = fixupAlwaysOnEffectsLocked(effect);
if (effects == null) {
// Invalid effects set in CombinedVibrationEffect, or always-on capability is
// missing on individual vibrators.
return false;
}
AlwaysOnVibration alwaysOnVibration = new AlwaysOnVibration(alwaysOnId,
new Vibration.CallerInfo(attrs, uid, Display.DEFAULT_DISPLAY, opPkg,
null), effects);
mAlwaysOnEffects.put(alwaysOnId, alwaysOnVibration);
updateAlwaysOnLocked(alwaysOnVibration);
}
return true;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
@Override // Binder call
public void vibrate(int uid, int displayId, String opPkg, @NonNull CombinedVibration effect,
@Nullable VibrationAttributes attrs, String reason, IBinder token) {
vibrateWithPermissionCheck(uid, displayId, opPkg, effect, attrs, reason, token);
}
@Override // Binder call
public void performHapticFeedback(
int uid, int displayId, String opPkg, int constant, boolean always, String reason,
IBinder token) {
performHapticFeedbackInternal(uid, displayId, opPkg, constant, always, reason, token);
}
/**
* An internal-only version of performHapticFeedback that allows the caller access to the
* {@link HalVibration}.
* The Vibration is only returned if it is ongoing after this method returns.
*/
@VisibleForTesting
@Nullable
HalVibration performHapticFeedbackInternal(
int uid, int displayId, String opPkg, int constant, boolean always, String reason,
IBinder token) {
HapticFeedbackVibrationProvider hapticVibrationProvider = getHapticVibrationProvider();
if (hapticVibrationProvider == null) {
Slog.w(TAG, "performHapticFeedback; haptic vibration provider not ready.");
return null;
}
VibrationEffect effect = hapticVibrationProvider.getVibrationForHapticFeedback(constant);
if (effect == null) {
Slog.w(TAG, "performHapticFeedback; vibration absent for effect " + constant);
return null;
}
CombinedVibration combinedVibration = CombinedVibration.createParallel(effect);
VibrationAttributes attrs =
hapticVibrationProvider.getVibrationAttributesForHapticFeedback(
constant, /* bypassVibrationIntensitySetting= */ always);
return vibrateWithoutPermissionCheck(uid, displayId, opPkg, combinedVibration, attrs,
"performHapticFeedback: " + reason, token);
}
/**
* An internal-only version of vibrate that allows the caller access to the
* {@link HalVibration}.
* The Vibration is only returned if it is ongoing after this method returns.
*/
@VisibleForTesting
@Nullable
HalVibration vibrateWithPermissionCheck(int uid, int displayId, String opPkg,
@NonNull CombinedVibration effect, @Nullable VibrationAttributes attrs,
String reason, IBinder token) {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate, reason = " + reason);
try {
attrs = fixupVibrationAttributes(attrs, effect);
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.VIBRATE, "vibrate");
return vibrateInternal(uid, displayId, opPkg, effect, attrs, reason, token);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
HalVibration vibrateWithoutPermissionCheck(int uid, int displayId, String opPkg,
@NonNull CombinedVibration effect, @NonNull VibrationAttributes attrs,
String reason, IBinder token) {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate no perm check, reason = " + reason);
try {
return vibrateInternal(uid, displayId, opPkg, effect, attrs, reason, token);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
private HalVibration vibrateInternal(int uid, int displayId, String opPkg,
@NonNull CombinedVibration effect, @NonNull VibrationAttributes attrs,
String reason, IBinder token) {
if (token == null) {
Slog.e(TAG, "token must not be null");
return null;
}
enforceUpdateAppOpsStatsPermission(uid);
if (!isEffectValid(effect)) {
return null;
}
// Create Vibration.Stats as close to the received request as possible, for tracking.
HalVibration vib = new HalVibration(token, effect,
new Vibration.CallerInfo(attrs, uid, displayId, opPkg, reason));
fillVibrationFallbacks(vib, effect);
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 vibrate for vibration " + vib.id);
}
// Check if user settings or DnD is set to ignore this vibration.
Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationLocked(vib.callerInfo);
// Check if ongoing vibration is more important than this vibration.
if (vibrationEndInfo == null) {
vibrationEndInfo = shouldIgnoreVibrationForOngoingLocked(vib);
}
// If not ignored so far then try to start this vibration.
if (vibrationEndInfo == null) {
final long ident = Binder.clearCallingIdentity();
try {
if (mCurrentExternalVibration != null) {
mCurrentExternalVibration.mute();
vib.stats.reportInterruptedAnotherVibration(
mCurrentExternalVibration.callerInfo);
endExternalVibrateLocked(
new Vibration.EndInfo(Vibration.Status.CANCELLED_SUPERSEDED,
vib.callerInfo),
/* continueExternalControl= */ false);
} else if (mCurrentVibration != null) {
if (mCurrentVibration.getVibration().canPipelineWith(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
// don't need to consider it here as well.
if (DEBUG) {
Slog.d(TAG, "Pipelining vibration " + vib.id);
}
} else {
vib.stats.reportInterruptedAnotherVibration(
mCurrentVibration.getVibration().callerInfo);
mCurrentVibration.notifyCancelled(
new Vibration.EndInfo(Vibration.Status.CANCELLED_SUPERSEDED,
vib.callerInfo),
/* immediate= */ false);
}
}
vibrationEndInfo = startVibrationLocked(vib);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
// Ignored or failed to start the vibration, end it and report metrics right away.
if (vibrationEndInfo != null) {
endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ true);
}
return vib;
}
}
@Override // Binder call
public void cancelVibrate(int usageFilter, IBinder token) {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "cancelVibrate");
try {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.VIBRATE,
"cancelVibrate");
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "Canceling vibration");
}
Vibration.EndInfo cancelledByUserInfo =
new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER);
final long ident = Binder.clearCallingIdentity();
try {
if (mNextVibration != null
&& shouldCancelVibration(mNextVibration.getVibration(),
usageFilter, token)) {
clearNextVibrationLocked(cancelledByUserInfo);
}
if (mCurrentVibration != null
&& shouldCancelVibration(mCurrentVibration.getVibration(),
usageFilter, token)) {
mCurrentVibration.notifyCancelled(
cancelledByUserInfo, /* immediate= */false);
}
if (mCurrentExternalVibration != null
&& shouldCancelVibration(
mCurrentExternalVibration.externalVibration.getVibrationAttributes(),
usageFilter)) {
mCurrentExternalVibration.mute();
endExternalVibrateLocked(
cancelledByUserInfo, /* continueExternalControl= */ false);
}
} finally {
Binder.restoreCallingIdentity(ident);
}
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
final long ident = Binder.clearCallingIdentity();
boolean isDumpProto = false;
for (String arg : args) {
if (arg.equals("--proto")) {
isDumpProto = true;
}
}
try {
if (isDumpProto) {
dumpProto(fd);
} else {
dumpText(pw);
}
} finally {
Binder.restoreCallingIdentity(ident);
}
}
private void dumpText(PrintWriter w) {
if (DEBUG) {
Slog.d(TAG, "Dumping vibrator manager service to text...");
}
IndentingPrintWriter pw = new IndentingPrintWriter(w, /* singleIndent= */ " ");
synchronized (mLock) {
pw.println("Vibrator Manager Service:");
pw.increaseIndent();
mVibrationSettings.dump(pw);
pw.println();
pw.println("VibratorControllers:");
pw.increaseIndent();
for (int i = 0; i < mVibrators.size(); i++) {
mVibrators.valueAt(i).dump(pw);
}
pw.decreaseIndent();
pw.println();
pw.println("CurrentVibration:");
pw.increaseIndent();
if (mCurrentVibration != null) {
mCurrentVibration.getVibration().getDebugInfo().dump(pw);
} else {
pw.println("null");
}
pw.decreaseIndent();
pw.println();
pw.println("NextVibration:");
pw.increaseIndent();
if (mNextVibration != null) {
mNextVibration.getVibration().getDebugInfo().dump(pw);
} else {
pw.println("null");
}
pw.decreaseIndent();
pw.println();
pw.println("CurrentExternalVibration:");
pw.increaseIndent();
if (mCurrentExternalVibration != null) {
mCurrentExternalVibration.getDebugInfo().dump(pw);
} else {
pw.println("null");
}
pw.decreaseIndent();
}
pw.println();
pw.println();
mVibratorManagerRecords.dump(pw);
}
private void dumpProto(FileDescriptor fd) {
final ProtoOutputStream proto = new ProtoOutputStream(fd);
if (DEBUG) {
Slog.d(TAG, "Dumping vibrator manager service to proto...");
}
synchronized (mLock) {
mVibrationSettings.dump(proto);
if (mCurrentVibration != null) {
mCurrentVibration.getVibration().getDebugInfo().dump(proto,
VibratorManagerServiceDumpProto.CURRENT_VIBRATION);
}
if (mCurrentExternalVibration != null) {
mCurrentExternalVibration.getDebugInfo().dump(proto,
VibratorManagerServiceDumpProto.CURRENT_EXTERNAL_VIBRATION);
}
boolean isVibrating = false;
boolean isUnderExternalControl = false;
for (int i = 0; i < mVibrators.size(); i++) {
proto.write(VibratorManagerServiceDumpProto.VIBRATOR_IDS, mVibrators.keyAt(i));
isVibrating |= mVibrators.valueAt(i).isVibrating();
isUnderExternalControl |= mVibrators.valueAt(i).isUnderExternalControl();
}
proto.write(VibratorManagerServiceDumpProto.IS_VIBRATING, isVibrating);
proto.write(VibratorManagerServiceDumpProto.VIBRATOR_UNDER_EXTERNAL_CONTROL,
isUnderExternalControl);
}
mVibratorManagerRecords.dump(proto);
proto.flush();
}
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback cb, ResultReceiver resultReceiver) {
new VibratorManagerShellCommand(cb.getShellCallbackBinder())
.exec(this, in, out, err, args, cb, resultReceiver);
}
@VisibleForTesting
void updateServiceState() {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "Updating device state...");
}
boolean inputDevicesChanged = mInputDeviceDelegate.updateInputDeviceVibrators(
mVibrationSettings.shouldVibrateInputDevices());
for (int i = 0; i < mAlwaysOnEffects.size(); i++) {
updateAlwaysOnLocked(mAlwaysOnEffects.valueAt(i));
}
if (mCurrentVibration == null) {
return;
}
HalVibration vib = mCurrentVibration.getVibration();
Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationLocked(vib.callerInfo);
if (inputDevicesChanged || (vibrationEndInfo != null)) {
if (DEBUG) {
Slog.d(TAG, "Canceling vibration because settings changed: "
+ (inputDevicesChanged ? "input devices changed"
: vibrationEndInfo.status));
}
mCurrentVibration.notifyCancelled(
new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_SETTINGS_UPDATE),
/* immediate= */ false);
}
}
}
private void setExternalControl(boolean externalControl, VibrationStats vibrationStats) {
for (int i = 0; i < mVibrators.size(); i++) {
mVibrators.valueAt(i).setExternalControl(externalControl);
vibrationStats.reportSetExternalControl();
}
}
@GuardedBy("mLock")
private void updateAlwaysOnLocked(AlwaysOnVibration vib) {
for (int i = 0; i < vib.effects.size(); i++) {
VibratorController vibrator = mVibrators.get(vib.effects.keyAt(i));
PrebakedSegment effect = vib.effects.valueAt(i);
if (vibrator == null) {
continue;
}
Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationLocked(vib.callerInfo);
if (vibrationEndInfo == null) {
effect = mVibrationScaler.scale(effect, vib.callerInfo.attrs.getUsage());
} else {
// Vibration should not run, use null effect to remove registered effect.
effect = null;
}
vibrator.updateAlwaysOn(vib.alwaysOnId, effect);
}
}
@GuardedBy("mLock")
@Nullable
private Vibration.EndInfo startVibrationLocked(HalVibration vib) {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "startVibrationLocked");
try {
// Scale effect before dispatching it to the input devices or the vibration thread.
vib.scaleEffects(mVibrationScaler::scale);
boolean inputDevicesAvailable = mInputDeviceDelegate.vibrateIfAvailable(
vib.callerInfo, vib.getEffectToPlay());
if (inputDevicesAvailable) {
return new Vibration.EndInfo(Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
}
VibrationStepConductor conductor = new VibrationStepConductor(vib, mVibrationSettings,
mDeviceAdapter, mVibrationThreadCallbacks);
if (mCurrentVibration == null) {
return startVibrationOnThreadLocked(conductor);
}
// 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(
new Vibration.EndInfo(Vibration.Status.IGNORED_SUPERSEDED, vib.callerInfo));
mNextVibration = conductor;
return null;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
@GuardedBy("mLock")
@Nullable
private Vibration.EndInfo startVibrationOnThreadLocked(VibrationStepConductor conductor) {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "startVibrationThreadLocked");
try {
HalVibration vib = conductor.getVibration();
int mode = startAppOpModeLocked(vib.callerInfo);
switch (mode) {
case AppOpsManager.MODE_ALLOWED:
Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
// Make sure mCurrentVibration is set while triggering the VibrationThread.
mCurrentVibration = conductor;
if (!mVibrationThread.runVibrationOnVibrationThread(mCurrentVibration)) {
// Shouldn't happen. The method call already logs a wtf.
mCurrentVibration = null; // Aborted.
return new Vibration.EndInfo(Vibration.Status.IGNORED_ERROR_SCHEDULING);
}
return null;
case AppOpsManager.MODE_ERRORED:
Slog.w(TAG, "Start AppOpsManager operation errored for uid "
+ vib.callerInfo.uid);
return new Vibration.EndInfo(Vibration.Status.IGNORED_ERROR_APP_OPS);
default:
return new Vibration.EndInfo(Vibration.Status.IGNORED_APP_OPS);
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
@GuardedBy("mLock")
private void endVibrationLocked(HalVibration vib, Vibration.EndInfo vibrationEndInfo,
boolean shouldWriteStats) {
vib.end(vibrationEndInfo);
logVibrationStatus(vib.callerInfo.uid, vib.callerInfo.attrs,
vibrationEndInfo.status);
mVibratorManagerRecords.record(vib);
if (shouldWriteStats) {
mFrameworkStatsLogger.writeVibrationReportedAsync(
vib.getStatsInfo(/* completionUptimeMillis= */ SystemClock.uptimeMillis()));
}
}
@GuardedBy("mLock")
private void endVibrationAndWriteStatsLocked(ExternalVibrationHolder vib,
Vibration.EndInfo vibrationEndInfo) {
vib.end(vibrationEndInfo);
logVibrationStatus(vib.externalVibration.getUid(),
vib.externalVibration.getVibrationAttributes(), vibrationEndInfo.status);
mVibratorManagerRecords.record(vib);
mFrameworkStatsLogger.writeVibrationReportedAsync(
vib.getStatsInfo(/* completionUptimeMillis= */ SystemClock.uptimeMillis()));
}
private void logVibrationStatus(int uid, VibrationAttributes attrs,
Vibration.Status status) {
switch (status) {
case IGNORED_BACKGROUND:
Slog.e(TAG, "Ignoring incoming vibration as process with"
+ " uid= " + uid + " is background," + " attrs= " + attrs);
break;
case IGNORED_ERROR_APP_OPS:
Slog.w(TAG, "Would be an error: vibrate from uid " + uid);
break;
case IGNORED_FOR_EXTERNAL:
if (DEBUG) {
Slog.d(TAG, "Ignoring incoming vibration for current external vibration");
}
break;
case IGNORED_FOR_HIGHER_IMPORTANCE:
if (DEBUG) {
Slog.d(TAG, "Ignoring incoming vibration in favor of ongoing vibration"
+ " with higher importance");
}
break;
case IGNORED_FOR_ONGOING:
if (DEBUG) {
Slog.d(TAG, "Ignoring incoming vibration in favor of repeating vibration");
}
break;
case IGNORED_FOR_RINGER_MODE:
if (DEBUG) {
Slog.d(TAG, "Ignoring incoming vibration because of ringer mode, attrs="
+ attrs);
}
break;
case IGNORED_FROM_VIRTUAL_DEVICE:
if (DEBUG) {
Slog.d(TAG, "Ignoring incoming vibration because it came from a virtual"
+ " device, attrs= " + attrs);
}
break;
default:
if (DEBUG) {
Slog.d(TAG, "Vibration for uid=" + uid + " and with attrs=" + attrs
+ " ended with status " + status);
}
}
}
@GuardedBy("mLock")
private void reportFinishedVibrationLocked(Vibration.EndInfo vibrationEndInfo) {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "reportFinishVibrationLocked");
Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
try {
HalVibration vib = mCurrentVibration.getVibration();
if (DEBUG) {
Slog.d(TAG, "Reporting vibration " + vib.id + " finished with "
+ vibrationEndInfo);
}
// DO NOT write metrics at this point, wait for the VibrationThread to report the
// vibration was released, after all cleanup. The metrics will be reported then.
endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ false);
finishAppOpModeLocked(vib.callerInfo);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
private void onSyncedVibrationComplete(long vibrationId) {
synchronized (mLock) {
if (mCurrentVibration != null
&& mCurrentVibration.getVibration().id == vibrationId) {
if (DEBUG) {
Slog.d(TAG, "Synced vibration " + vibrationId + " complete, notifying thread");
}
mCurrentVibration.notifySyncedVibrationComplete();
}
}
}
private void onVibrationComplete(int vibratorId, long vibrationId) {
synchronized (mLock) {
if (mCurrentVibration != null
&& mCurrentVibration.getVibration().id == vibrationId) {
if (DEBUG) {
Slog.d(TAG, "Vibration " + vibrationId + " on vibrator " + vibratorId
+ " complete, notifying thread");
}
mCurrentVibration.notifyVibratorComplete(vibratorId);
}
}
}
/**
* Check if given vibration should be ignored by this service because of the ongoing vibration.
*
* @return a Vibration.EndInfo if the vibration should be ignored, null otherwise.
*/
@GuardedBy("mLock")
@Nullable
private Vibration.EndInfo shouldIgnoreVibrationForOngoingLocked(Vibration vib) {
if (mCurrentExternalVibration != null) {
return shouldIgnoreVibrationForOngoing(vib, mCurrentExternalVibration);
}
if (mNextVibration != null) {
Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationForOngoing(vib,
mNextVibration.getVibration());
if (vibrationEndInfo != null) {
// Next vibration has higher importance than the new one, so the new vibration
// should be ignored.
return vibrationEndInfo;
}
}
if (mCurrentVibration != null) {
HalVibration currentVibration = mCurrentVibration.getVibration();
if (currentVibration.hasEnded() || mCurrentVibration.wasNotifiedToCancel()) {
// Current vibration has ended or is cancelling, should not block incoming
// vibrations.
return null;
}
return shouldIgnoreVibrationForOngoing(vib, currentVibration);
}
return null;
}
/**
* Checks if the ongoing vibration has higher importance than the new one. If they have similar
* importance, then {@link Vibration#isRepeating()} is used as a tiebreaker.
*
* @return a Vibration.EndInfo if the vibration should be ignored, null otherwise.
*/
@Nullable
private static Vibration.EndInfo shouldIgnoreVibrationForOngoing(
@NonNull Vibration newVibration, @NonNull Vibration ongoingVibration) {
int newVibrationImportance = getVibrationImportance(newVibration);
int ongoingVibrationImportance = getVibrationImportance(ongoingVibration);
if (newVibrationImportance > ongoingVibrationImportance) {
// New vibration has higher importance and should not be ignored.
return null;
}
if (ongoingVibrationImportance > newVibrationImportance) {
// Existing vibration has higher importance and should not be cancelled.
return new Vibration.EndInfo(Vibration.Status.IGNORED_FOR_HIGHER_IMPORTANCE,
ongoingVibration.callerInfo);
}
// Same importance, use repeating as a tiebreaker.
if (ongoingVibration.isRepeating() && !newVibration.isRepeating()) {
// Ongoing vibration is repeating and new one is not, give priority to ongoing
return new Vibration.EndInfo(Vibration.Status.IGNORED_FOR_ONGOING,
ongoingVibration.callerInfo);
}
// New vibration is repeating or this is a complete tie between them,
// give priority to new vibration.
return null;
}
/**
* Gets the vibration importance based on usage. In the case where usage is unknown, it maps
* repeating vibrations to ringtones and non-repeating vibrations to touches.
*
* @return a numeric representation for the vibration importance, larger values represent a
* higher importance
*/
private static int getVibrationImportance(Vibration vibration) {
int usage = vibration.callerInfo.attrs.getUsage();
if (usage == VibrationAttributes.USAGE_UNKNOWN) {
if (vibration.isRepeating()) {
usage = VibrationAttributes.USAGE_RINGTONE;
} else {
usage = VibrationAttributes.USAGE_TOUCH;
}
}
switch (usage) {
case VibrationAttributes.USAGE_RINGTONE:
return 5;
case VibrationAttributes.USAGE_ALARM:
return 4;
case VibrationAttributes.USAGE_NOTIFICATION:
return 3;
case VibrationAttributes.USAGE_COMMUNICATION_REQUEST:
case VibrationAttributes.USAGE_ACCESSIBILITY:
return 2;
case VibrationAttributes.USAGE_HARDWARE_FEEDBACK:
case VibrationAttributes.USAGE_PHYSICAL_EMULATION:
return 1;
case VibrationAttributes.USAGE_MEDIA:
case VibrationAttributes.USAGE_TOUCH:
default:
return 0;
}
}
/**
* Check if given vibration should be ignored by this service.
*
* @return a Vibration.EndInfo if the vibration should be ignored, null otherwise.
*/
@GuardedBy("mLock")
@Nullable
private Vibration.EndInfo shouldIgnoreVibrationLocked(Vibration.CallerInfo callerInfo) {
Vibration.Status statusFromSettings = mVibrationSettings.shouldIgnoreVibration(callerInfo);
if (statusFromSettings != null) {
return new Vibration.EndInfo(statusFromSettings);
}
int mode = checkAppOpModeLocked(callerInfo);
if (mode != AppOpsManager.MODE_ALLOWED) {
if (mode == AppOpsManager.MODE_ERRORED) {
// We might be getting calls from within system_server, so we don't actually
// want to throw a SecurityException here.
return new Vibration.EndInfo(Vibration.Status.IGNORED_ERROR_APP_OPS);
} else {
return new Vibration.EndInfo(Vibration.Status.IGNORED_APP_OPS);
}
}
return null;
}
/**
* Return true if the vibration has the same token and usage belongs to given usage class.
*
* @param vib The ongoing or pending vibration to be cancelled.
* @param usageFilter The vibration usages to be cancelled, any bitwise combination of
* VibrationAttributes.USAGE_* values.
* @param token The binder token to identify the vibration origin. Only vibrations
* started with the same token can be cancelled with it.
*/
private boolean shouldCancelVibration(HalVibration vib, int usageFilter, IBinder token) {
return (vib.callerToken == token) && shouldCancelVibration(vib.callerInfo.attrs,
usageFilter);
}
/**
* Return true if the external vibration usage belongs to given usage class.
*
* @param attrs The attributes of an ongoing or pending vibration to be cancelled.
* @param usageFilter The vibration usages to be cancelled, any bitwise combination of
* VibrationAttributes.USAGE_* values.
*/
private boolean shouldCancelVibration(VibrationAttributes attrs, int usageFilter) {
if (attrs.getUsage() == VibrationAttributes.USAGE_UNKNOWN) {
// Special case, usage UNKNOWN would match all filters. Instead it should only match if
// it's cancelling that usage specifically, or if cancelling all usages.
return usageFilter == VibrationAttributes.USAGE_UNKNOWN
|| usageFilter == VibrationAttributes.USAGE_FILTER_MATCH_ALL;
}
return (usageFilter & attrs.getUsage()) == attrs.getUsage();
}
/**
* Check which mode should be set for a vibration with given {@code uid}, {@code opPkg} and
* {@code attrs}. This will return one of the AppOpsManager.MODE_*.
*/
@GuardedBy("mLock")
private int checkAppOpModeLocked(Vibration.CallerInfo callerInfo) {
int mode = mAppOps.checkAudioOpNoThrow(AppOpsManager.OP_VIBRATE,
callerInfo.attrs.getAudioUsage(), callerInfo.uid, callerInfo.opPkg);
int fixedMode = fixupAppOpModeLocked(mode, callerInfo.attrs);
if (mode != fixedMode && fixedMode == AppOpsManager.MODE_ALLOWED) {
// If we're just ignoring the vibration op then this is set by DND and we should ignore
// if we're asked to bypass. AppOps won't be able to record this operation, so make
// sure we at least note it in the logs for debugging.
Slog.d(TAG, "Bypassing DND for vibrate from uid " + callerInfo.uid);
}
return fixedMode;
}
/** Start an operation in {@link AppOpsManager}, if allowed. */
@GuardedBy("mLock")
private int startAppOpModeLocked(Vibration.CallerInfo callerInfo) {
return fixupAppOpModeLocked(
mAppOps.startOpNoThrow(AppOpsManager.OP_VIBRATE, callerInfo.uid, callerInfo.opPkg),
callerInfo.attrs);
}
/**
* Finish a previously started operation in {@link AppOpsManager}. This will be a noop if no
* operation with same uid was previously started.
*/
@GuardedBy("mLock")
private void finishAppOpModeLocked(Vibration.CallerInfo callerInfo) {
mAppOps.finishOp(AppOpsManager.OP_VIBRATE, callerInfo.uid, callerInfo.opPkg);
}
/**
* Enforces {@link android.Manifest.permission#UPDATE_APP_OPS_STATS} to incoming UID if it's
* different from the calling UID.
*/
private void enforceUpdateAppOpsStatsPermission(int uid) {
if (uid == Binder.getCallingUid()) {
return;
}
if (Binder.getCallingPid() == Process.myPid()) {
return;
}
mContext.enforcePermission(android.Manifest.permission.UPDATE_APP_OPS_STATS,
Binder.getCallingPid(), Binder.getCallingUid(), null);
}
/**
* Validate the incoming {@link CombinedVibration}.
*
* We can't throw exceptions here since we might be called from some system_server component,
* which would bring the whole system down.
*
* @return whether the CombinedVibrationEffect is non-null and valid
*/
private static boolean isEffectValid(@Nullable CombinedVibration effect) {
if (effect == null) {
Slog.wtf(TAG, "effect must not be null");
return false;
}
try {
effect.validate();
} catch (Exception e) {
Slog.wtf(TAG, "Encountered issue when verifying CombinedVibrationEffect.", e);
return false;
}
return true;
}
/**
* Sets fallback effects to all prebaked ones in given combination of effects, based on {@link
* VibrationSettings#getFallbackEffect}.
*/
private void fillVibrationFallbacks(HalVibration vib, CombinedVibration effect) {
if (effect instanceof CombinedVibration.Mono) {
fillVibrationFallbacks(vib, ((CombinedVibration.Mono) effect).getEffect());
} else if (effect instanceof CombinedVibration.Stereo) {
SparseArray<VibrationEffect> effects =
((CombinedVibration.Stereo) effect).getEffects();
for (int i = 0; i < effects.size(); i++) {
fillVibrationFallbacks(vib, effects.valueAt(i));
}
} else if (effect instanceof CombinedVibration.Sequential) {
List<CombinedVibration> effects =
((CombinedVibration.Sequential) effect).getEffects();
for (int i = 0; i < effects.size(); i++) {
fillVibrationFallbacks(vib, effects.get(i));
}
}
}
private void fillVibrationFallbacks(HalVibration vib, VibrationEffect effect) {
VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
int segmentCount = composed.getSegments().size();
for (int i = 0; i < segmentCount; i++) {
VibrationEffectSegment segment = composed.getSegments().get(i);
if (segment instanceof PrebakedSegment) {
PrebakedSegment prebaked = (PrebakedSegment) segment;
VibrationEffect fallback = mVibrationSettings.getFallbackEffect(
prebaked.getEffectId());
if (prebaked.shouldFallback() && fallback != null) {
vib.addFallback(prebaked.getEffectId(), fallback);
}
}
}
}
/**
* Return new {@link VibrationAttributes} that only applies flags that this user has permissions
* to use.
*/
@NonNull
private VibrationAttributes fixupVibrationAttributes(@Nullable VibrationAttributes attrs,
@Nullable CombinedVibration effect) {
if (attrs == null) {
attrs = DEFAULT_ATTRIBUTES;
}
int usage = attrs.getUsage();
if ((usage == VibrationAttributes.USAGE_UNKNOWN)
&& (effect != null) && effect.isHapticFeedbackCandidate()) {
usage = VibrationAttributes.USAGE_TOUCH;
}
int flags = attrs.getFlags();
if ((flags & ATTRIBUTES_ALL_BYPASS_FLAGS) != 0) {
if (!(hasPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
|| hasPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
|| hasPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING))) {
// Remove bypass flags from attributes if the app does not have permissions.
flags &= ~ATTRIBUTES_ALL_BYPASS_FLAGS;
}
}
if ((usage == attrs.getUsage()) && (flags == attrs.getFlags())) {
return attrs;
}
return new VibrationAttributes.Builder(attrs)
.setUsage(usage)
.setFlags(flags, attrs.getFlags())
.build();
}
@GuardedBy("mLock")
@Nullable
private SparseArray<PrebakedSegment> fixupAlwaysOnEffectsLocked(
CombinedVibration effect) {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "fixupAlwaysOnEffectsLocked");
try {
SparseArray<VibrationEffect> effects;
if (effect instanceof CombinedVibration.Mono) {
VibrationEffect syncedEffect = ((CombinedVibration.Mono) effect).getEffect();
effects = transformAllVibratorsLocked(unused -> syncedEffect);
} else if (effect instanceof CombinedVibration.Stereo) {
effects = ((CombinedVibration.Stereo) effect).getEffects();
} else {
// Only synced combinations can be used for always-on effects.
return null;
}
SparseArray<PrebakedSegment> result = new SparseArray<>();
for (int i = 0; i < effects.size(); i++) {
PrebakedSegment prebaked = extractPrebakedSegment(effects.valueAt(i));
if (prebaked == null) {
Slog.e(TAG, "Only prebaked effects supported for always-on.");
return null;
}
int vibratorId = effects.keyAt(i);
VibratorController vibrator = mVibrators.get(vibratorId);
if (vibrator != null && vibrator.hasCapability(IVibrator.CAP_ALWAYS_ON_CONTROL)) {
result.put(vibratorId, prebaked);
}
}
if (result.size() == 0) {
return null;
}
return result;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
@Nullable
private static PrebakedSegment extractPrebakedSegment(VibrationEffect effect) {
if (effect instanceof VibrationEffect.Composed) {
VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
if (composed.getSegments().size() == 1) {
VibrationEffectSegment segment = composed.getSegments().get(0);
if (segment instanceof PrebakedSegment) {
return (PrebakedSegment) segment;
}
}
}
return null;
}
/**
* Check given mode, one of the AppOpsManager.MODE_*, against {@link VibrationAttributes} to
* allow bypassing {@link AppOpsManager} checks.
*/
@GuardedBy("mLock")
private int fixupAppOpModeLocked(int mode, VibrationAttributes attrs) {
if (mode == AppOpsManager.MODE_IGNORED
&& attrs.isFlagSet(VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY)) {
return AppOpsManager.MODE_ALLOWED;
}
return mode;
}
private boolean hasPermission(String permission) {
return mContext.checkCallingOrSelfPermission(permission)
== PackageManager.PERMISSION_GRANTED;
}
@GuardedBy("mLock")
private boolean shouldCancelOnScreenOffLocked(@Nullable VibrationStepConductor conductor) {
if (conductor == null) {
return false;
}
HalVibration vib = conductor.getVibration();
return mVibrationSettings.shouldCancelVibrationOnScreenOff(vib.callerInfo,
vib.stats.getCreateUptimeMillis());
}
@GuardedBy("mLock")
private void onAllVibratorsLocked(Consumer<VibratorController> consumer) {
for (int i = 0; i < mVibrators.size(); i++) {
consumer.accept(mVibrators.valueAt(i));
}
}
@GuardedBy("mLock")
private <T> SparseArray<T> transformAllVibratorsLocked(Function<VibratorController, T> fn) {
SparseArray<T> ret = new SparseArray<>(mVibrators.size());
for (int i = 0; i < mVibrators.size(); i++) {
ret.put(mVibrators.keyAt(i), fn.apply(mVibrators.valueAt(i)));
}
return ret;
}
/** Point of injection for test dependencies */
@VisibleForTesting
static class Injector {
NativeWrapper getNativeWrapper() {
return new NativeWrapper();
}
Handler createHandler(Looper looper) {
return new Handler(looper);
}
IBatteryStats getBatteryStatsService() {
return IBatteryStats.Stub.asInterface(ServiceManager.getService(
BatteryStats.SERVICE_NAME));
}
VibratorFrameworkStatsLogger getFrameworkStatsLogger(Handler handler) {
return new VibratorFrameworkStatsLogger(handler);
}
VibratorController createVibratorController(int vibratorId,
VibratorController.OnVibrationCompleteListener listener) {
return new VibratorController(vibratorId, listener);
}
HapticFeedbackVibrationProvider createHapticFeedbackVibrationProvider(
Resources resources, VibratorInfo vibratorInfo) {
return new HapticFeedbackVibrationProvider(resources, vibratorInfo);
}
void addService(String name, IBinder service) {
ServiceManager.addService(name, service);
}
}
/**
* Implementation of {@link VibrationThread.VibratorManagerHooks} that controls synced
* vibrations and reports them when finished.
*/
private final class VibrationThreadCallbacks implements VibrationThread.VibratorManagerHooks {
@Override
public boolean prepareSyncedVibration(long requiredCapabilities, int[] vibratorIds) {
if ((mCapabilities & requiredCapabilities) != requiredCapabilities) {
// This sync step requires capabilities this device doesn't have, skipping sync...
return false;
}
return mNativeWrapper.prepareSynced(vibratorIds);
}
@Override
public boolean triggerSyncedVibration(long vibrationId) {
return mNativeWrapper.triggerSynced(vibrationId);
}
@Override
public void cancelSyncedVibration() {
mNativeWrapper.cancelSynced();
}
@Override
public void noteVibratorOn(int uid, long duration) {
try {
if (duration <= 0) {
// Tried to turn vibrator ON and got:
// duration == 0: Unsupported effect/method or zero-amplitude segment.
// duration < 0: Unexpected error triggering the vibrator.
// Skip battery stats and atom metric for VibratorStageChanged to ON.
return;
}
if (duration == Long.MAX_VALUE) {
// Repeating duration has started. Report a fixed duration here, noteVibratorOff
// should be called when this is cancelled.
duration = BATTERY_STATS_REPEATING_VIBRATION_DURATION;
}
mBatteryStatsService.noteVibratorOn(uid, duration);
mFrameworkStatsLogger.writeVibratorStateOnAsync(uid, duration);
} catch (RemoteException e) {
Slog.e(TAG, "Error logging VibratorStateChanged to ON", e);
}
}
@Override
public void noteVibratorOff(int uid) {
try {
mBatteryStatsService.noteVibratorOff(uid);
mFrameworkStatsLogger.writeVibratorStateOffAsync(uid);
} catch (RemoteException e) {
Slog.e(TAG, "Error logging VibratorStateChanged to OFF", e);
}
}
@Override
public void onVibrationCompleted(long vibrationId, Vibration.EndInfo vibrationEndInfo) {
if (DEBUG) {
Slog.d(TAG, "Vibration " + vibrationId + " finished with " + vibrationEndInfo);
}
synchronized (mLock) {
if (mCurrentVibration != null
&& mCurrentVibration.getVibration().id == vibrationId) {
reportFinishedVibrationLocked(vibrationEndInfo);
}
}
}
@Override
public void onVibrationThreadReleased(long vibrationId) {
if (DEBUG) {
Slog.d(TAG, "VibrationThread released after finished vibration");
}
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "Processing VibrationThread released callback");
}
if (Build.IS_DEBUGGABLE && mCurrentVibration != null
&& mCurrentVibration.getVibration().id != vibrationId) {
Slog.wtf(TAG, TextUtils.formatSimple(
"VibrationId mismatch on release. expected=%d, released=%d",
mCurrentVibration.getVibration().id, vibrationId));
}
if (mCurrentVibration != null) {
// This is when we consider the current vibration complete, so report metrics.
mFrameworkStatsLogger.writeVibrationReportedAsync(
mCurrentVibration.getVibration().getStatsInfo(
/* completionUptimeMillis= */ SystemClock.uptimeMillis()));
mCurrentVibration = null;
}
if (mNextVibration != null) {
VibrationStepConductor nextConductor = mNextVibration;
mNextVibration = null;
Vibration.EndInfo vibrationEndInfo = startVibrationOnThreadLocked(
nextConductor);
if (vibrationEndInfo != null) {
// Failed to start the vibration, end it and report metrics right away.
endVibrationLocked(nextConductor.getVibration(),
vibrationEndInfo, /* shouldWriteStats= */ true);
}
}
}
}
}
/** Listener for synced vibration completion callbacks from native. */
@VisibleForTesting
interface OnSyncedVibrationCompleteListener {
/** Callback triggered when synced vibration is complete. */
void onComplete(long vibrationId);
}
/**
* Implementation of listeners to native vibrators with a weak reference to this service.
*/
private static final class VibrationCompleteListener implements
VibratorController.OnVibrationCompleteListener, OnSyncedVibrationCompleteListener {
private WeakReference<VibratorManagerService> mServiceRef;
VibrationCompleteListener(VibratorManagerService service) {
mServiceRef = new WeakReference<>(service);
}
@Override
public void onComplete(long vibrationId) {
VibratorManagerService service = mServiceRef.get();
if (service != null) {
service.onSyncedVibrationComplete(vibrationId);
}
}
@Override
public void onComplete(int vibratorId, long vibrationId) {
VibratorManagerService service = mServiceRef.get();
if (service != null) {
service.onVibrationComplete(vibratorId, vibrationId);
}
}
}
/**
* Combination of prekabed vibrations on multiple vibrators, with the same {@link
* VibrationAttributes}, that can be set for always-on effects.
*/
private static final class AlwaysOnVibration {
public final int alwaysOnId;
public final Vibration.CallerInfo callerInfo;
public final SparseArray<PrebakedSegment> effects;
AlwaysOnVibration(int alwaysOnId, Vibration.CallerInfo callerInfo,
SparseArray<PrebakedSegment> effects) {
this.alwaysOnId = alwaysOnId;
this.callerInfo = callerInfo;
this.effects = effects;
}
}
/** Holder for a {@link ExternalVibration}. */
private final class ExternalVibrationHolder extends Vibration implements
IBinder.DeathRecipient {
public final ExternalVibration externalVibration;
public int scale;
private Vibration.Status mStatus;
private ExternalVibrationHolder(ExternalVibration externalVibration) {
super(externalVibration.getToken(), new Vibration.CallerInfo(
externalVibration.getVibrationAttributes(), externalVibration.getUid(),
// TODO(b/243604888): propagating displayID from IExternalVibration instead of
// using INVALID_DISPLAY for all external vibrations.
Display.INVALID_DISPLAY,
externalVibration.getPackage(), null));
this.externalVibration = externalVibration;
this.scale = IExternalVibratorService.SCALE_NONE;
mStatus = Vibration.Status.RUNNING;
}
public void mute() {
externalVibration.mute();
}
public void linkToDeath() {
externalVibration.linkToDeath(this);
}
public void unlinkToDeath() {
externalVibration.unlinkToDeath(this);
}
public boolean isHoldingSameVibration(ExternalVibration externalVibration) {
return this.externalVibration.equals(externalVibration);
}
public void end(Vibration.EndInfo info) {
if (mStatus != Vibration.Status.RUNNING) {
// Already ended, ignore this call
return;
}
mStatus = info.status;
stats.reportEnded(info.endedBy);
if (stats.hasStarted()) {
// External vibration doesn't have feedback from total time the vibrator was playing
// with non-zero amplitude, so we use the duration between start and end times of
// the vibration as the time the vibrator was ON, since the haptic channels are
// open for this duration and can receive vibration waveform data.
stats.reportVibratorOn(
stats.getEndUptimeMillis() - stats.getStartUptimeMillis());
}
}
public void binderDied() {
synchronized (mLock) {
if (mCurrentExternalVibration != null) {
if (DEBUG) {
Slog.d(TAG, "External vibration finished because binder died");
}
endExternalVibrateLocked(
new Vibration.EndInfo(Vibration.Status.CANCELLED_BINDER_DIED),
/* continueExternalControl= */ false);
}
}
}
public Vibration.DebugInfo getDebugInfo() {
return new Vibration.DebugInfo(mStatus, stats, /* playedEffect= */ null,
/* originalEffect= */ null, scale, callerInfo);
}
public VibrationStats.StatsInfo getStatsInfo(long completionUptimeMillis) {
return new VibrationStats.StatsInfo(
externalVibration.getUid(),
FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__EXTERNAL,
externalVibration.getVibrationAttributes().getUsage(), mStatus, stats,
completionUptimeMillis);
}
@Override
boolean isRepeating() {
// We don't currently know if the external vibration is repeating, so we just use a
// heuristic based on the usage. Ideally this would be propagated in the
// ExternalVibration.
int usage = externalVibration.getVibrationAttributes().getUsage();
return usage == VibrationAttributes.USAGE_RINGTONE
|| usage == VibrationAttributes.USAGE_ALARM;
}
}
/** Wrapper around the static-native methods of {@link VibratorManagerService} for tests. */
@VisibleForTesting
public static class NativeWrapper {
private long mNativeServicePtr = 0;
/** Returns native pointer to newly created controller and connects with HAL service. */
public void init(OnSyncedVibrationCompleteListener listener) {
mNativeServicePtr = nativeInit(listener);
long finalizerPtr = nativeGetFinalizer();
if (finalizerPtr != 0) {
NativeAllocationRegistry registry =
NativeAllocationRegistry.createMalloced(
VibratorManagerService.class.getClassLoader(), finalizerPtr);
registry.registerNativeAllocation(this, mNativeServicePtr);
}
}
/** Returns manager capabilities. */
public long getCapabilities() {
return nativeGetCapabilities(mNativeServicePtr);
}
/** Returns vibrator ids. */
public int[] getVibratorIds() {
return nativeGetVibratorIds(mNativeServicePtr);
}
/** Prepare vibrators for triggering vibrations in sync. */
public boolean prepareSynced(@NonNull int[] vibratorIds) {
return nativePrepareSynced(mNativeServicePtr, vibratorIds);
}
/** Trigger prepared synced vibration. */
public boolean triggerSynced(long vibrationId) {
return nativeTriggerSynced(mNativeServicePtr, vibrationId);
}
/** Cancel prepared synced vibration. */
public void cancelSynced() {
nativeCancelSynced(mNativeServicePtr);
}
}
/** Keep records of vibrations played and provide debug information for this service. */
private static final class VibratorManagerRecords {
private final VibrationRecords mAggregatedVibrationHistory;
private final VibrationRecords mRecentVibrations;
VibratorManagerRecords(int recentVibrationSizeLimit, int aggregationSizeLimit,
int aggregationTimeLimit) {
mAggregatedVibrationHistory =
new VibrationRecords(aggregationSizeLimit, aggregationTimeLimit);
mRecentVibrations = new VibrationRecords(
recentVibrationSizeLimit, /* aggregationTimeLimit= */ 0);
}
synchronized void record(HalVibration vib) {
record(vib.getDebugInfo());
}
synchronized void record(ExternalVibrationHolder vib) {
record(vib.getDebugInfo());
}
private synchronized void record(Vibration.DebugInfo info) {
AggregatedVibrationRecord removedRecord = mRecentVibrations.record(info);
if (removedRecord != null) {
mAggregatedVibrationHistory.record(removedRecord.mLatestVibration);
}
}
synchronized void dump(IndentingPrintWriter pw) {
pw.println("Recent vibrations:");
pw.increaseIndent();
mRecentVibrations.dump(pw);
pw.decreaseIndent();
pw.println();
pw.println();
pw.println("Aggregated vibration history:");
pw.increaseIndent();
mAggregatedVibrationHistory.dump(pw);
pw.decreaseIndent();
}
synchronized void dump(ProtoOutputStream proto) {
mRecentVibrations.dump(proto);
}
}
/** Keep records of vibrations played and provide debug information for this service. */
private static final class VibrationRecords {
private final SparseArray<LinkedList<AggregatedVibrationRecord>> mVibrations =
new SparseArray<>();
private final int mSizeLimit;
private final int mAggregationTimeLimit;
VibrationRecords(int sizeLimit, int aggregationTimeLimit) {
mSizeLimit = sizeLimit;
mAggregationTimeLimit = aggregationTimeLimit;
}
synchronized AggregatedVibrationRecord record(Vibration.DebugInfo info) {
int usage = info.mCallerInfo.attrs.getUsage();
if (!mVibrations.contains(usage)) {
mVibrations.put(usage, new LinkedList<>());
}
LinkedList<AggregatedVibrationRecord> records = mVibrations.get(usage);
if (mAggregationTimeLimit > 0 && !records.isEmpty()) {
AggregatedVibrationRecord lastRecord = records.getLast();
if (lastRecord.mayAggregate(info, mAggregationTimeLimit)) {
lastRecord.record(info);
return null;
}
}
AggregatedVibrationRecord removedRecord = null;
if (records.size() > mSizeLimit) {
removedRecord = records.removeFirst();
}
records.addLast(new AggregatedVibrationRecord(info));
return removedRecord;
}
synchronized void dump(IndentingPrintWriter pw) {
for (int i = 0; i < mVibrations.size(); i++) {
pw.println(VibrationAttributes.usageToString(mVibrations.keyAt(i)) + ":");
pw.increaseIndent();
for (AggregatedVibrationRecord info : mVibrations.valueAt(i)) {
info.dump(pw);
}
pw.decreaseIndent();
pw.println();
}
}
synchronized void dump(ProtoOutputStream proto) {
for (int i = 0; i < mVibrations.size(); i++) {
long fieldId;
switch (mVibrations.keyAt(i)) {
case VibrationAttributes.USAGE_RINGTONE:
fieldId = VibratorManagerServiceDumpProto.PREVIOUS_RING_VIBRATIONS;
break;
case VibrationAttributes.USAGE_NOTIFICATION:
fieldId = VibratorManagerServiceDumpProto.PREVIOUS_NOTIFICATION_VIBRATIONS;
break;
case VibrationAttributes.USAGE_ALARM:
fieldId = VibratorManagerServiceDumpProto.PREVIOUS_ALARM_VIBRATIONS;
break;
default:
fieldId = VibratorManagerServiceDumpProto.PREVIOUS_VIBRATIONS;
}
for (AggregatedVibrationRecord info : mVibrations.valueAt(i)) {
if (info.mLatestVibration.mPlayedEffect == null) {
// External vibrations are reported separately in the dump proto
info.dump(proto,
VibratorManagerServiceDumpProto.PREVIOUS_EXTERNAL_VIBRATIONS);
} else {
info.dump(proto, fieldId);
}
}
}
}
synchronized void dumpOnSingleField(ProtoOutputStream proto, long fieldId) {
for (int i = 0; i < mVibrations.size(); i++) {
for (AggregatedVibrationRecord info : mVibrations.valueAt(i)) {
info.dump(proto, fieldId);
}
}
}
}
/**
* Record that keeps the last {@link Vibration.DebugInfo} played, aggregating close vibrations
* from the same uid that have the same {@link VibrationAttributes} and {@link VibrationEffect}.
*/
private static final class AggregatedVibrationRecord {
private final Vibration.DebugInfo mFirstVibration;
private Vibration.DebugInfo mLatestVibration;
private int mVibrationCount;
AggregatedVibrationRecord(Vibration.DebugInfo info) {
mLatestVibration = mFirstVibration = info;
mVibrationCount = 1;
}
synchronized boolean mayAggregate(Vibration.DebugInfo info, long timeLimit) {
return Objects.equals(mLatestVibration.mCallerInfo.uid, info.mCallerInfo.uid)
&& Objects.equals(mLatestVibration.mCallerInfo.attrs, info.mCallerInfo.attrs)
&& Objects.equals(mLatestVibration.mPlayedEffect, info.mPlayedEffect)
&& Math.abs(mLatestVibration.mCreateTime - info.mCreateTime) < timeLimit;
}
synchronized void record(Vibration.DebugInfo vib) {
mLatestVibration = vib;
mVibrationCount++;
}
synchronized void dump(IndentingPrintWriter pw) {
mFirstVibration.dumpCompact(pw);
if (mVibrationCount == 1) {
return;
}
if (mVibrationCount > 2) {
pw.println(
"-> Skipping " + (mVibrationCount - 2) + " aggregated vibrations, latest:");
}
mLatestVibration.dumpCompact(pw);
}
synchronized void dump(ProtoOutputStream proto, long fieldId) {
mLatestVibration.dump(proto, fieldId);
}
}
/** Clears mNextVibration if set, ending it cleanly */
@GuardedBy("mLock")
private void clearNextVibrationLocked(Vibration.EndInfo vibrationEndInfo) {
if (mNextVibration != null) {
if (DEBUG) {
Slog.d(TAG, "Dropping pending vibration " + mNextVibration.getVibration().id
+ " with end info: " + vibrationEndInfo);
}
// Clearing next vibration before playing it, end it and report metrics right away.
endVibrationLocked(mNextVibration.getVibration(), vibrationEndInfo,
/* shouldWriteStats= */ true);
mNextVibration = null;
}
}
/**
* Ends the external vibration, and clears related service state.
*
* @param vibrationEndInfo the status and related info to end the associated Vibration
* @param continueExternalControl indicates whether external control will continue. If not, the
* HAL will have external control turned off.
*/
@GuardedBy("mLock")
private void endExternalVibrateLocked(Vibration.EndInfo vibrationEndInfo,
boolean continueExternalControl) {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "endExternalVibrateLocked");
try {
if (mCurrentExternalVibration == null) {
return;
}
mCurrentExternalVibration.unlinkToDeath();
if (!continueExternalControl) {
setExternalControl(false, mCurrentExternalVibration.stats);
}
// The external control was turned off, end it and report metrics right away.
endVibrationAndWriteStatsLocked(mCurrentExternalVibration, vibrationEndInfo);
mCurrentExternalVibration = null;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
private HapticFeedbackVibrationProvider getHapticVibrationProvider() {
synchronized (mLock) {
// Used a cached haptic vibration provider if one exists.
if (mHapticFeedbackVibrationProvider != null) {
return mHapticFeedbackVibrationProvider;
}
VibratorInfo combinedVibratorInfo = getCombinedVibratorInfo();
if (combinedVibratorInfo == null) {
return null;
}
return mHapticFeedbackVibrationProvider =
mInjector.createHapticFeedbackVibrationProvider(
mContext.getResources(), combinedVibratorInfo);
}
}
private VibratorInfo getCombinedVibratorInfo() {
synchronized (mLock) {
// Used a cached resolving vibrator if one exists.
if (mCombinedVibratorInfo != null) {
return mCombinedVibratorInfo;
}
// Return an empty resolving vibrator if the service has no vibrator.
if (mVibratorIds.length == 0) {
return mCombinedVibratorInfo = VibratorInfo.EMPTY_VIBRATOR_INFO;
}
// Combine the vibrator infos of all the service's vibrator to create a single resolving
// vibrator that is based on the combined info.
VibratorInfo[] infos = new VibratorInfo[mVibratorIds.length];
for (int i = 0; i < mVibratorIds.length; i++) {
VibratorInfo info = getVibratorInfo(mVibratorIds[i]);
// If any one of the service's vibrator does not have a valid vibrator info, stop
// trying to create and cache a combined resolving vibrator. Combine the infos only
// when infos for all vibrators are available.
if (info == null) {
return null;
}
infos[i] = info;
}
return mCombinedVibratorInfo = VibratorInfoFactory.create(/* id= */ -1, infos);
}
}
/** Implementation of {@link IExternalVibratorService} to be triggered on external control. */
@VisibleForTesting
final class ExternalVibratorService extends IExternalVibratorService.Stub {
@Override
public int onExternalVibrationStart(ExternalVibration vib) {
if (!hasExternalControlCapability()) {
return IExternalVibratorService.SCALE_MUTE;
}
if (ActivityManager.checkComponentPermission(android.Manifest.permission.VIBRATE,
vib.getUid(), -1 /*owningUid*/, true /*exported*/)
!= PackageManager.PERMISSION_GRANTED) {
Slog.w(TAG, "pkg=" + vib.getPackage() + ", uid=" + vib.getUid()
+ " tried to play externally controlled vibration"
+ " without VIBRATE permission, ignoring.");
return IExternalVibratorService.SCALE_MUTE;
}
// Create Vibration.Stats as close to the received request as possible, for tracking.
ExternalVibrationHolder vibHolder = new ExternalVibrationHolder(vib);
VibrationAttributes attrs = fixupVibrationAttributes(vib.getVibrationAttributes(),
/* effect= */ null);
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();
}
boolean alreadyUnderExternalControl = false;
boolean waitForCompletion = false;
synchronized (mLock) {
// TODO(b/243604888): propagating displayID from IExternalVibration instead of
// using INVALID_DISPLAY for all external vibrations.
Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationLocked(
vibHolder.callerInfo);
if (vibrationEndInfo == null
&& mCurrentExternalVibration != null
&& mCurrentExternalVibration.isHoldingSameVibration(vib)) {
// We are already playing this external vibration, so we can return the same
// scale calculated in the previous call to this method.
return mCurrentExternalVibration.scale;
}
if (vibrationEndInfo == null) {
// Check if ongoing vibration is more important than this vibration.
vibrationEndInfo = shouldIgnoreVibrationForOngoingLocked(vibHolder);
}
if (vibrationEndInfo != null) {
vibHolder.scale = IExternalVibratorService.SCALE_MUTE;
// Failed to start the vibration, end it and report metrics right away.
endVibrationAndWriteStatsLocked(vibHolder, vibrationEndInfo);
return vibHolder.scale;
}
if (mCurrentExternalVibration == null) {
// If we're not under external control right now, then cancel any normal
// vibration that may be playing and ready the vibrator for external control.
if (mCurrentVibration != null) {
vibHolder.stats.reportInterruptedAnotherVibration(
mCurrentVibration.getVibration().callerInfo);
clearNextVibrationLocked(
new Vibration.EndInfo(Vibration.Status.IGNORED_FOR_EXTERNAL,
vibHolder.callerInfo));
mCurrentVibration.notifyCancelled(
new Vibration.EndInfo(Vibration.Status.CANCELLED_SUPERSEDED,
vibHolder.callerInfo),
/* immediate= */ true);
waitForCompletion = true;
}
} else {
// At this point we have an externally controlled vibration playing already.
// Since the interface defines that only one externally controlled vibration can
// play at a time, we need to first mute the ongoing vibration and then return
// a scale from this function for the new one, so we can be assured that the
// ongoing will be muted in favor of the new vibration.
//
// Note that this doesn't support multiple concurrent external controls, as we
// would need to mute the old one still if it came from a different controller.
alreadyUnderExternalControl = true;
mCurrentExternalVibration.mute();
vibHolder.stats.reportInterruptedAnotherVibration(
mCurrentExternalVibration.callerInfo);
endExternalVibrateLocked(
new Vibration.EndInfo(Vibration.Status.CANCELLED_SUPERSEDED,
vibHolder.callerInfo),
/* continueExternalControl= */ true);
}
mCurrentExternalVibration = vibHolder;
vibHolder.linkToDeath();
vibHolder.scale = mVibrationScaler.getExternalVibrationScale(attrs.getUsage());
}
if (waitForCompletion) {
if (!mVibrationThread.waitForThreadIdle(VIBRATION_CANCEL_WAIT_MILLIS)) {
Slog.e(TAG, "Timed out waiting for vibration to cancel");
synchronized (mLock) {
// Trigger endExternalVibrateLocked to unlink to death recipient.
endExternalVibrateLocked(
new Vibration.EndInfo(Vibration.Status.IGNORED_ERROR_CANCELLING),
/* continueExternalControl= */ false);
}
return IExternalVibratorService.SCALE_MUTE;
}
}
if (!alreadyUnderExternalControl) {
if (DEBUG) {
Slog.d(TAG, "Vibrator going under external control.");
}
setExternalControl(true, vibHolder.stats);
}
if (DEBUG) {
Slog.d(TAG, "Playing external vibration: " + vib);
}
// Vibrator will start receiving data from external channels after this point.
// Report current time as the vibration start time, for debugging.
vibHolder.stats.reportStarted();
return vibHolder.scale;
}
@Override
public void onExternalVibrationStop(ExternalVibration vib) {
synchronized (mLock) {
if (mCurrentExternalVibration != null
&& mCurrentExternalVibration.isHoldingSameVibration(vib)) {
if (DEBUG) {
Slog.d(TAG, "Stopping external vibration: " + vib);
}
endExternalVibrateLocked(
new Vibration.EndInfo(Vibration.Status.FINISHED),
/* continueExternalControl= */ false);
}
}
}
private boolean hasExternalControlCapability() {
for (int i = 0; i < mVibrators.size(); i++) {
if (mVibrators.valueAt(i).hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) {
return true;
}
}
return false;
}
}
/** Provide limited functionality from {@link VibratorManagerService} as shell commands. */
private final class VibratorManagerShellCommand extends ShellCommand {
public static final String SHELL_PACKAGE_NAME = "com.android.shell";
private final class CommonOptions {
public boolean force = false;
public String description = "Shell command";
public boolean background = false;
CommonOptions() {
String nextArg;
while ((nextArg = peekNextArg()) != null) {
switch (nextArg) {
case "-f":
getNextArgRequired(); // consume "-f"
force = true;
break;
case "-B":
getNextArgRequired(); // consume "-B"
background = true;
break;
case "-d":
getNextArgRequired(); // consume "-d"
description = getNextArgRequired();
break;
default:
// nextArg is not a common option, finish reading.
return;
}
}
}
}
private final IBinder mShellCallbacksToken;
private VibratorManagerShellCommand(IBinder shellCallbacksToken) {
mShellCallbacksToken = shellCallbacksToken;
}
@Override
public int onCommand(String cmd) {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onCommand " + cmd);
try {
if ("list".equals(cmd)) {
return runListVibrators();
}
if ("synced".equals(cmd)) {
return runMono();
}
if ("combined".equals(cmd)) {
return runStereo();
}
if ("sequential".equals(cmd)) {
return runSequential();
}
if ("xml".equals(cmd)) {
return runXml();
}
if ("cancel".equals(cmd)) {
return runCancel();
}
if ("feedback".equals(cmd)) {
return runHapticFeedback();
}
return handleDefaultCommands(cmd);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
}
private int runListVibrators() {
try (PrintWriter pw = getOutPrintWriter();) {
if (mVibratorIds.length == 0) {
pw.println("No vibrator found");
} else {
for (int id : mVibratorIds) {
pw.println(id);
}
}
pw.println("");
return 0;
}
}
/**
* Runs a CombinedVibration using the configured common options and attributes.
*/
private void runVibrate(CommonOptions commonOptions, CombinedVibration combined) {
VibrationAttributes attrs = createVibrationAttributes(commonOptions);
// If running in the background, bind to death of the server binder rather than the
// client, and the cancel command likewise uses the server binder reference to
// only cancel background vibrations.
IBinder deathBinder = commonOptions.background ? VibratorManagerService.this
: mShellCallbacksToken;
HalVibration vib = vibrateWithPermissionCheck(Binder.getCallingUid(),
Display.DEFAULT_DISPLAY, SHELL_PACKAGE_NAME, combined, attrs,
commonOptions.description, deathBinder);
maybeWaitOnVibration(vib, commonOptions);
}
private int runMono() {
runVibrate(new CommonOptions(), CombinedVibration.createParallel(nextEffect()));
return 0;
}
private int runStereo() {
CommonOptions commonOptions = new CommonOptions();
CombinedVibration.ParallelCombination combination =
CombinedVibration.startParallel();
while ("-v".equals(getNextOption())) {
int vibratorId = Integer.parseInt(getNextArgRequired());
combination.addVibrator(vibratorId, nextEffect());
}
runVibrate(commonOptions, combination.combine());
return 0;
}
private int runSequential() {
CommonOptions commonOptions = new CommonOptions();
CombinedVibration.SequentialCombination combination =
CombinedVibration.startSequential();
while ("-v".equals(getNextOption())) {
int vibratorId = Integer.parseInt(getNextArgRequired());
combination.addNext(vibratorId, nextEffect());
}
runVibrate(commonOptions, combination.combine());
return 0;
}
private int runXml() {
CommonOptions commonOptions = new CommonOptions();
String xml = getNextArgRequired();
CombinedVibration vibration = parseXml(xml);
runVibrate(commonOptions, vibration);
return 0;
}
private int runCancel() {
// Cancel is only needed if the vibration was run in the background, otherwise it's
// terminated by the shell command ending. In these cases, the token was that of the
// service rather than the client.
cancelVibrate(VibrationAttributes.USAGE_FILTER_MATCH_ALL, VibratorManagerService.this);
return 0;
}
private int runHapticFeedback() {
CommonOptions commonOptions = new CommonOptions();
int constant = Integer.parseInt(getNextArgRequired());
IBinder deathBinder = commonOptions.background ? VibratorManagerService.this
: mShellCallbacksToken;
HalVibration vib = performHapticFeedbackInternal(Binder.getCallingUid(),
Display.DEFAULT_DISPLAY, SHELL_PACKAGE_NAME, constant,
/* always= */ commonOptions.force, /* reason= */ commonOptions.description,
deathBinder);
maybeWaitOnVibration(vib, commonOptions);
return 0;
}
private VibrationEffect nextEffect() {
VibrationEffect.Composition composition = VibrationEffect.startComposition();
String nextArg;
while ((nextArg = peekNextArg()) != null) {
if ("oneshot".equals(nextArg)) {
addOneShotToComposition(composition);
} else if ("waveform".equals(nextArg)) {
addWaveformToComposition(composition);
} else if ("prebaked".equals(nextArg)) {
addPrebakedToComposition(composition);
} else if ("primitives".equals(nextArg)) {
addPrimitivesToComposition(composition);
} else {
// nextArg is not an effect, finish reading.
break;
}
}
return composition.compose();
}
private void addOneShotToComposition(VibrationEffect.Composition composition) {
boolean hasAmplitude = false;
int delay = 0;
getNextArgRequired(); // consume "oneshot"
String nextOption;
while ((nextOption = getNextOption()) != null) {
if ("-a".equals(nextOption)) {
hasAmplitude = true;
} else if ("-w".equals(nextOption)) {
delay = Integer.parseInt(getNextArgRequired());
}
}
long duration = Long.parseLong(getNextArgRequired());
int amplitude = hasAmplitude ? Integer.parseInt(getNextArgRequired())
: VibrationEffect.DEFAULT_AMPLITUDE;
composition.addOffDuration(Duration.ofMillis(delay));
composition.addEffect(VibrationEffect.createOneShot(duration, amplitude));
}
private void addWaveformToComposition(VibrationEffect.Composition composition) {
boolean hasAmplitudes = false;
boolean hasFrequencies = false;
boolean isContinuous = false;
int repeat = -1;
int delay = 0;
getNextArgRequired(); // consume "waveform"
String nextOption;
while ((nextOption = getNextOption()) != null) {
if ("-a".equals(nextOption)) {
hasAmplitudes = true;
} else if ("-r".equals(nextOption)) {
repeat = Integer.parseInt(getNextArgRequired());
} else if ("-w".equals(nextOption)) {
delay = Integer.parseInt(getNextArgRequired());
} else if ("-f".equals(nextOption)) {
hasFrequencies = true;
} else if ("-c".equals(nextOption)) {
isContinuous = true;
}
}
List<Integer> durations = new ArrayList<>();
List<Float> amplitudes = new ArrayList<>();
List<Float> frequencies = new ArrayList<>();
float nextAmplitude = 0;
String nextArg;
while ((nextArg = peekNextArg()) != null) {
try {
durations.add(Integer.parseInt(nextArg));
getNextArgRequired(); // consume the duration
} catch (NumberFormatException e) {
// nextArg is not a duration, finish reading.
break;
}
if (hasAmplitudes) {
amplitudes.add(
Float.parseFloat(getNextArgRequired()) / VibrationEffect.MAX_AMPLITUDE);
} else {
amplitudes.add(nextAmplitude);
nextAmplitude = 1 - nextAmplitude;
}
if (hasFrequencies) {
frequencies.add(Float.parseFloat(getNextArgRequired()));
}
}
// Add delay before the waveform.
composition.addOffDuration(Duration.ofMillis(delay));
VibrationEffect.WaveformBuilder waveform = VibrationEffect.startWaveform();
for (int i = 0; i < durations.size(); i++) {
Duration transitionDuration = isContinuous
? Duration.ofMillis(durations.get(i))
: Duration.ZERO;
Duration sustainDuration = isContinuous
? Duration.ZERO
: Duration.ofMillis(durations.get(i));
if (hasFrequencies) {
waveform.addTransition(transitionDuration, targetAmplitude(amplitudes.get(i)),
targetFrequency(frequencies.get(i)));
} else {
waveform.addTransition(transitionDuration, targetAmplitude(amplitudes.get(i)));
}
if (!sustainDuration.isZero()) {
// Add sustain only takes positive durations. Skip this since we already
// did a transition to the desired values (even when duration is zero).
waveform.addSustain(sustainDuration);
}
if ((i > 0) && (i == repeat)) {
// Add segment that is not repeated to the composition and reset builder.
composition.addEffect(waveform.build());
if (hasFrequencies) {
waveform = VibrationEffect.startWaveform(targetAmplitude(amplitudes.get(i)),
targetFrequency(frequencies.get(i)));
} else {
waveform = VibrationEffect.startWaveform(
targetAmplitude(amplitudes.get(i)));
}
}
}
if (repeat < 0) {
composition.addEffect(waveform.build());
} else {
// The waveform was already split at the repeat index, just repeat what remains.
composition.repeatEffectIndefinitely(waveform.build());
}
}
private void addPrebakedToComposition(VibrationEffect.Composition composition) {
boolean shouldFallback = false;
int delay = 0;
getNextArgRequired(); // consume "prebaked"
String nextOption;
while ((nextOption = getNextOption()) != null) {
if ("-b".equals(nextOption)) {
shouldFallback = true;
} else if ("-w".equals(nextOption)) {
delay = Integer.parseInt(getNextArgRequired());
}
}
int effectId = Integer.parseInt(getNextArgRequired());
composition.addOffDuration(Duration.ofMillis(delay));
composition.addEffect(VibrationEffect.get(effectId, shouldFallback));
}
private void addPrimitivesToComposition(VibrationEffect.Composition composition) {
getNextArgRequired(); // consume "primitives"
String nextArg;
while ((nextArg = peekNextArg()) != null) {
int delay = 0;
if ("-w".equals(nextArg)) {
getNextArgRequired(); // consume "-w"
delay = Integer.parseInt(getNextArgRequired());
nextArg = peekNextArg();
}
try {
composition.addPrimitive(Integer.parseInt(nextArg), /* scale= */ 1, delay);
getNextArgRequired(); // consume the primitive id
} catch (NumberFormatException | NullPointerException e) {
// nextArg is not describing a primitive, leave it to be consumed by outer loops
break;
}
}
}
private VibrationAttributes createVibrationAttributes(CommonOptions commonOptions) {
// This will bypass user settings, Do Not Disturb and other interruption policies.
final int flags = commonOptions.force ? ATTRIBUTES_ALL_BYPASS_FLAGS : 0;
return new VibrationAttributes.Builder()
.setFlags(flags)
// Used to allow vibrations when the adb shell process is running in background.
// This will apply the NOTIFICATION_VIBRATION_INTENSITY setting.
.setUsage(VibrationAttributes.USAGE_COMMUNICATION_REQUEST)
.build();
}
private CombinedVibration parseXml(String xml) {
try {
ParsedVibration parsedVibration =
VibrationXmlParser.parseDocument(new StringReader(xml));
if (parsedVibration == null) {
throw new IllegalArgumentException("Error parsing vibration XML " + xml);
}
VibratorInfo combinedVibratorInfo = getCombinedVibratorInfo();
if (combinedVibratorInfo == null) {
throw new IllegalStateException(
"No combined vibrator info to parse vibration XML " + xml);
}
VibrationEffect effect = parsedVibration.resolve(combinedVibratorInfo);
if (effect == null) {
throw new IllegalArgumentException(
"Parsed vibration cannot be resolved for vibration XML " + xml);
}
return CombinedVibration.createParallel(effect);
} catch (IOException e) {
throw new RuntimeException("Error parsing vibration XML " + xml, e);
}
}
private void maybeWaitOnVibration(HalVibration vib, CommonOptions commonOptions) {
if (vib != null && !commonOptions.background) {
try {
// Waits for the client vibration to finish, but the VibrationThread may still
// do cleanup after this.
vib.waitForEnd();
} catch (InterruptedException e) {
}
}
}
@Override
public void onHelp() {
try (PrintWriter pw = getOutPrintWriter();) {
pw.println("Vibrator Manager commands:");
pw.println(" help");
pw.println(" Prints this help text.");
pw.println("");
pw.println(" list");
pw.println(" Prints the id of device vibrators. This does not include any ");
pw.println(" connected input device.");
pw.println(" synced [options] <effect>...");
pw.println(" Vibrates effect on all vibrators in sync.");
pw.println(" combined [options] (-v <vibrator-id> <effect>...)...");
pw.println(" Vibrates different effects on each vibrator in sync.");
pw.println(" sequential [options] (-v <vibrator-id> <effect>...)...");
pw.println(" Vibrates different effects on each vibrator in sequence.");
pw.println(" xml [options] <xml>");
pw.println(" Vibrates using combined vibration described in given XML string");
pw.println(" on all vibrators in sync. The XML could be:");
pw.println(" XML containing a single effect, or");
pw.println(" A vibration select XML containing multiple effects.");
pw.println(" Vibrates using combined vibration described in given XML string.");
pw.println(" XML containing a single effect it runs on all vibrators in sync.");
pw.println(" cancel");
pw.println(" Cancels any active vibration");
pw.println(" feedback [-f] [-d <description>] <constant>");
pw.println(" Performs a haptic feedback with the given constant.");
pw.println(" The force (-f) option enables the `always` configuration, which");
pw.println(" plays the haptic irrespective of the vibration intensity settings");
pw.println("");
pw.println("Effect commands:");
pw.println(" oneshot [-w delay] [-a] <duration> [<amplitude>]");
pw.println(" Vibrates for duration milliseconds; ignored when device is on ");
pw.println(" DND (Do Not Disturb) mode; touch feedback strength user setting ");
pw.println(" will be used to scale amplitude.");
pw.println(" If -w is provided, the effect will be played after the specified");
pw.println(" wait time in milliseconds.");
pw.println(" If -a is provided, the command accepts a second argument for ");
pw.println(" amplitude, in a scale of 1-255.");
pw.print(" waveform [-w delay] [-r index] [-a] [-f] [-c] ");
pw.println("(<duration> [<amplitude>] [<frequency>])...");
pw.println(" Vibrates for durations and amplitudes in list; ignored when ");
pw.println(" device is on DND (Do Not Disturb) mode; touch feedback strength ");
pw.println(" user setting will be used to scale amplitude.");
pw.println(" If -w is provided, the effect will be played after the specified");
pw.println(" wait time in milliseconds.");
pw.println(" If -r is provided, the waveform loops back to the specified");
pw.println(" index (e.g. 0 loops from the beginning)");
pw.println(" If -a is provided, the command expects amplitude to follow each");
pw.println(" duration; otherwise, it accepts durations only and alternates");
pw.println(" off/on");
pw.println(" If -f is provided, the command expects frequency to follow each");
pw.println(" amplitude or duration; otherwise, it uses resonant frequency");
pw.println(" If -c is provided, the waveform is continuous and will ramp");
pw.println(" between values; otherwise each entry is a fixed step.");
pw.println(" Duration is in milliseconds; amplitude is a scale of 1-255;");
pw.println(" frequency is an absolute value in hertz;");
pw.println(" prebaked [-w delay] [-b] <effect-id>");
pw.println(" Vibrates with prebaked effect; ignored when device is on DND ");
pw.println(" (Do Not Disturb) mode; touch feedback strength user setting ");
pw.println(" will be used to scale amplitude.");
pw.println(" If -w is provided, the effect will be played after the specified");
pw.println(" wait time in milliseconds.");
pw.println(" If -b is provided, the prebaked fallback effect will be played if");
pw.println(" the device doesn't support the given effect-id.");
pw.println(" primitives ([-w delay] <primitive-id>)...");
pw.println(" Vibrates with a composed effect; ignored when device is on DND ");
pw.println(" (Do Not Disturb) mode; touch feedback strength user setting ");
pw.println(" will be used to scale primitive intensities.");
pw.println(" If -w is provided, the next primitive will be played after the ");
pw.println(" specified wait time in milliseconds.");
pw.println("");
pw.println("Common Options:");
pw.println(" -f");
pw.println(" Force. Ignore Do Not Disturb setting.");
pw.println(" -B");
pw.println(" Run in the background; without this option the shell cmd will");
pw.println(" block until the vibration has completed.");
pw.println(" -d <description>");
pw.println(" Add description to the vibration.");
pw.println("");
}
}
}
}