blob: c54d490b51628b2b5eef9c4ab139747eeb79f5eb [file] [log] [blame]
/*
* Copyright (C) 2020 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.VibrationAttributes.USAGE_ACCESSIBILITY;
import static android.os.VibrationAttributes.USAGE_ALARM;
import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST;
import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK;
import static android.os.VibrationAttributes.USAGE_MEDIA;
import static android.os.VibrationAttributes.USAGE_NOTIFICATION;
import static android.os.VibrationAttributes.USAGE_PHYSICAL_EMULATION;
import static android.os.VibrationAttributes.USAGE_RINGTONE;
import static android.os.VibrationAttributes.USAGE_TOUCH;
import static android.os.VibrationAttributes.USAGE_UNKNOWN;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.IUidObserver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Handler;
import android.os.PowerManager;
import android.os.PowerManagerInternal;
import android.os.PowerSaveState;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.Vibrator.VibrationIntensity;
import android.os.vibrator.VibrationConfig;
import android.provider.Settings;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** Controls all the system settings related to vibration. */
final class VibrationSettings {
private static final String TAG = "VibrationSettings";
/**
* Set of usages allowed for vibrations from background processes.
*
* <p>Some examples are notification, ringtone or alarm vibrations, that are allowed to vibrate
* unexpectedly as they are meant to grab the user's attention. Hardware feedback and physical
* emulation are also supported, as the trigger process might still be in the background when
* the user interaction wakes the device.
*/
private static final Set<Integer> BACKGROUND_PROCESS_USAGE_ALLOWLIST = new HashSet<>(
Arrays.asList(
USAGE_RINGTONE,
USAGE_ALARM,
USAGE_NOTIFICATION,
USAGE_COMMUNICATION_REQUEST,
USAGE_HARDWARE_FEEDBACK,
USAGE_PHYSICAL_EMULATION));
/**
* Set of usages allowed for vibrations in battery saver mode (low power).
*
* <p>Some examples are ringtone or alarm vibrations, that have high priority and should vibrate
* even when the device is saving battery.
*/
private static final Set<Integer> BATTERY_SAVER_USAGE_ALLOWLIST = new HashSet<>(
Arrays.asList(
USAGE_RINGTONE,
USAGE_ALARM,
USAGE_COMMUNICATION_REQUEST));
/** Listener for changes on vibration settings. */
interface OnVibratorSettingsChanged {
/** Callback triggered when any of the vibrator settings change. */
void onChange();
}
private final Object mLock = new Object();
private final Context mContext;
private final SettingsObserver mSettingObserver;
@VisibleForTesting
final UidObserver mUidObserver;
@VisibleForTesting
final UserObserver mUserReceiver;
@GuardedBy("mLock")
private final List<OnVibratorSettingsChanged> mListeners = new ArrayList<>();
private final SparseArray<VibrationEffect> mFallbackEffects;
private final VibrationConfig mVibrationConfig;
@GuardedBy("mLock")
@Nullable
private AudioManager mAudioManager;
@GuardedBy("mLock")
private boolean mVibrateInputDevices;
@GuardedBy("mLock")
private SparseIntArray mCurrentVibrationIntensities = new SparseIntArray();
@GuardedBy("mLock")
private boolean mBatterySaverMode;
VibrationSettings(Context context, Handler handler) {
this(context, handler, new VibrationConfig(context.getResources()));
}
@VisibleForTesting
VibrationSettings(Context context, Handler handler, VibrationConfig config) {
mContext = context;
mVibrationConfig = config;
mSettingObserver = new SettingsObserver(handler);
mUidObserver = new UidObserver();
mUserReceiver = new UserObserver();
VibrationEffect clickEffect = createEffectFromResource(
com.android.internal.R.array.config_virtualKeyVibePattern);
VibrationEffect doubleClickEffect = createEffectFromResource(
com.android.internal.R.array.config_doubleClickVibePattern);
VibrationEffect heavyClickEffect = createEffectFromResource(
com.android.internal.R.array.config_longPressVibePattern);
VibrationEffect tickEffect = createEffectFromResource(
com.android.internal.R.array.config_clockTickVibePattern);
mFallbackEffects = new SparseArray<>();
mFallbackEffects.put(VibrationEffect.EFFECT_CLICK, clickEffect);
mFallbackEffects.put(VibrationEffect.EFFECT_DOUBLE_CLICK, doubleClickEffect);
mFallbackEffects.put(VibrationEffect.EFFECT_TICK, tickEffect);
mFallbackEffects.put(VibrationEffect.EFFECT_HEAVY_CLICK, heavyClickEffect);
mFallbackEffects.put(VibrationEffect.EFFECT_TEXTURE_TICK,
VibrationEffect.get(VibrationEffect.EFFECT_TICK, false));
// Update with current values from settings.
updateSettings();
}
public void onSystemReady() {
synchronized (mLock) {
mAudioManager = mContext.getSystemService(AudioManager.class);
}
try {
ActivityManager.getService().registerUidObserver(mUidObserver,
ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE,
ActivityManager.PROCESS_STATE_UNKNOWN, null);
} catch (RemoteException e) {
// ignored; both services live in system_server
}
PowerManagerInternal pm = LocalServices.getService(PowerManagerInternal.class);
pm.registerLowPowerModeObserver(
new PowerManagerInternal.LowPowerModeListener() {
@Override
public int getServiceType() {
return PowerManager.ServiceType.VIBRATION;
}
@Override
public void onLowPowerModeChanged(PowerSaveState result) {
boolean shouldNotifyListeners;
synchronized (mLock) {
shouldNotifyListeners = result.batterySaverEnabled != mBatterySaverMode;
mBatterySaverMode = result.batterySaverEnabled;
}
if (shouldNotifyListeners) {
notifyListeners();
}
}
});
IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
mContext.registerReceiver(mUserReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
// Listen to all settings that might affect the result of Vibrator.getVibrationIntensity.
registerSettingsObserver(Settings.System.getUriFor(Settings.System.VIBRATE_INPUT_DEVICES));
registerSettingsObserver(Settings.System.getUriFor(Settings.System.VIBRATE_WHEN_RINGING));
registerSettingsObserver(Settings.System.getUriFor(Settings.System.APPLY_RAMPING_RINGER));
registerSettingsObserver(Settings.System.getUriFor(
Settings.System.HAPTIC_FEEDBACK_ENABLED));
registerSettingsObserver(
Settings.System.getUriFor(Settings.System.ALARM_VIBRATION_INTENSITY));
registerSettingsObserver(
Settings.System.getUriFor(Settings.System.HAPTIC_FEEDBACK_INTENSITY));
registerSettingsObserver(
Settings.System.getUriFor(Settings.System.HARDWARE_HAPTIC_FEEDBACK_INTENSITY));
registerSettingsObserver(
Settings.System.getUriFor(Settings.System.MEDIA_VIBRATION_INTENSITY));
registerSettingsObserver(
Settings.System.getUriFor(Settings.System.NOTIFICATION_VIBRATION_INTENSITY));
registerSettingsObserver(
Settings.System.getUriFor(Settings.System.RING_VIBRATION_INTENSITY));
// Update with newly loaded services.
updateSettings();
}
/**
* Add listener to vibrator settings changes. This will trigger the listener with current state
* immediately and every time one of the settings change.
*/
public void addListener(OnVibratorSettingsChanged listener) {
synchronized (mLock) {
if (!mListeners.contains(listener)) {
mListeners.add(listener);
}
}
}
/** Remove listener to vibrator settings. */
public void removeListener(OnVibratorSettingsChanged listener) {
synchronized (mLock) {
mListeners.remove(listener);
}
}
/**
* The duration, in milliseconds, that should be applied to convert vibration effect's
* {@link android.os.vibrator.RampSegment} to a {@link android.os.vibrator.StepSegment} on
* devices without PWLE support.
*/
public int getRampStepDuration() {
return mVibrationConfig.getRampStepDurationMs();
}
/**
* The duration, in milliseconds, that should be applied to the ramp to turn off the vibrator
* when a vibration is cancelled or finished at non-zero amplitude.
*/
public int getRampDownDuration() {
return mVibrationConfig.getRampDownDurationMs();
}
/**
* Return default vibration intensity for given usage.
*
* @param usageHint one of VibrationAttributes.USAGE_*
* @return The vibration intensity, one of Vibrator.VIBRATION_INTENSITY_*
*/
public int getDefaultIntensity(@VibrationAttributes.Usage int usageHint) {
return mVibrationConfig.getDefaultVibrationIntensity(usageHint);
}
/**
* Return the current vibration intensity set for given usage at the user settings.
*
* @param usageHint one of VibrationAttributes.USAGE_*
* @return The vibration intensity, one of Vibrator.VIBRATION_INTENSITY_*
*/
public int getCurrentIntensity(@VibrationAttributes.Usage int usageHint) {
int defaultIntensity = getDefaultIntensity(usageHint);
synchronized (mLock) {
return mCurrentVibrationIntensities.get(usageHint, defaultIntensity);
}
}
/**
* Return a {@link VibrationEffect} that should be played if the device do not support given
* {@code effectId}.
*
* @param effectId one of VibrationEffect.EFFECT_*
* @return The effect to be played as a fallback
*/
public VibrationEffect getFallbackEffect(int effectId) {
return mFallbackEffects.get(effectId);
}
/** Return {@code true} if input devices should vibrate instead of this device. */
public boolean shouldVibrateInputDevices() {
return mVibrateInputDevices;
}
/**
* Check if given vibration should be ignored by the service.
*
* @return One of Vibration.Status.IGNORED_* values if the vibration should be ignored,
* null otherwise.
*/
@Nullable
public Vibration.Status shouldIgnoreVibration(int uid, VibrationAttributes attrs) {
final int usage = attrs.getUsage();
synchronized (mLock) {
if (!mUidObserver.isUidForeground(uid)
&& !BACKGROUND_PROCESS_USAGE_ALLOWLIST.contains(usage)) {
return Vibration.Status.IGNORED_BACKGROUND;
}
if (mBatterySaverMode && !BATTERY_SAVER_USAGE_ALLOWLIST.contains(usage)) {
return Vibration.Status.IGNORED_FOR_POWER;
}
int intensity = getCurrentIntensity(usage);
if ((intensity == Vibrator.VIBRATION_INTENSITY_OFF)
&& !attrs.isFlagSet(
VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF)) {
return Vibration.Status.IGNORED_FOR_SETTINGS;
}
if (!shouldVibrateForRingerModeLocked(usage)) {
return Vibration.Status.IGNORED_FOR_RINGER_MODE;
}
}
return null;
}
/**
* Return {@code true} if the device should vibrate for current ringer mode.
*
* <p>This checks the current {@link AudioManager#getRingerModeInternal()} against user settings
* for touch and ringtone usages only. All other usages are allowed by this method.
*/
@GuardedBy("mLock")
private boolean shouldVibrateForRingerModeLocked(@VibrationAttributes.Usage int usageHint) {
// If audio manager was not loaded yet then assume most restrictive mode.
int ringerMode = (mAudioManager == null)
? AudioManager.RINGER_MODE_SILENT
: mAudioManager.getRingerModeInternal();
switch (usageHint) {
case USAGE_TOUCH:
case USAGE_RINGTONE:
// Touch feedback and ringtone disabled when phone is on silent mode.
return ringerMode != AudioManager.RINGER_MODE_SILENT;
default:
// All other usages ignore ringer mode settings.
return true;
}
}
/** Updates all vibration settings and triggers registered listeners. */
@VisibleForTesting
void updateSettings() {
synchronized (mLock) {
mVibrateInputDevices = loadSystemSetting(Settings.System.VIBRATE_INPUT_DEVICES, 0) > 0;
int alarmIntensity = toIntensity(
loadSystemSetting(Settings.System.ALARM_VIBRATION_INTENSITY, -1),
getDefaultIntensity(USAGE_ALARM));
int defaultHapticFeedbackIntensity = getDefaultIntensity(USAGE_TOUCH);
int hapticFeedbackIntensity = toIntensity(
loadSystemSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, -1),
defaultHapticFeedbackIntensity);
int positiveHapticFeedbackIntensity = toPositiveIntensity(
hapticFeedbackIntensity, defaultHapticFeedbackIntensity);
int hardwareFeedbackIntensity = toIntensity(
loadSystemSetting(Settings.System.HARDWARE_HAPTIC_FEEDBACK_INTENSITY, -1),
positiveHapticFeedbackIntensity);
int mediaIntensity = toIntensity(
loadSystemSetting(Settings.System.MEDIA_VIBRATION_INTENSITY, -1),
getDefaultIntensity(USAGE_MEDIA));
int defaultNotificationIntensity = getDefaultIntensity(USAGE_NOTIFICATION);
int notificationIntensity = toIntensity(
loadSystemSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, -1),
defaultNotificationIntensity);
int positiveNotificationIntensity = toPositiveIntensity(
notificationIntensity, defaultNotificationIntensity);
int ringIntensity = toIntensity(
loadSystemSetting(Settings.System.RING_VIBRATION_INTENSITY, -1),
getDefaultIntensity(USAGE_RINGTONE));
mCurrentVibrationIntensities.clear();
mCurrentVibrationIntensities.put(USAGE_ALARM, alarmIntensity);
mCurrentVibrationIntensities.put(USAGE_NOTIFICATION, notificationIntensity);
mCurrentVibrationIntensities.put(USAGE_MEDIA, mediaIntensity);
mCurrentVibrationIntensities.put(USAGE_UNKNOWN, mediaIntensity);
// Communication request is not disabled by the notification setting.
mCurrentVibrationIntensities.put(USAGE_COMMUNICATION_REQUEST,
positiveNotificationIntensity);
if (!loadBooleanSetting(Settings.System.VIBRATE_WHEN_RINGING)
&& !loadBooleanSetting(Settings.System.APPLY_RAMPING_RINGER)) {
// Make sure deprecated boolean setting still disables ringtone vibrations.
mCurrentVibrationIntensities.put(USAGE_RINGTONE, Vibrator.VIBRATION_INTENSITY_OFF);
} else {
mCurrentVibrationIntensities.put(USAGE_RINGTONE, ringIntensity);
}
// This should adapt the behavior preceding the introduction of this new setting
// key, which is to apply HAPTIC_FEEDBACK_INTENSITY, unless it's disabled.
mCurrentVibrationIntensities.put(USAGE_HARDWARE_FEEDBACK, hardwareFeedbackIntensity);
mCurrentVibrationIntensities.put(USAGE_PHYSICAL_EMULATION, hardwareFeedbackIntensity);
if (!loadBooleanSetting(Settings.System.HAPTIC_FEEDBACK_ENABLED)) {
// Make sure deprecated boolean setting still disables touch vibrations.
mCurrentVibrationIntensities.put(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_OFF);
} else {
mCurrentVibrationIntensities.put(USAGE_TOUCH, hapticFeedbackIntensity);
}
// A11y is not disabled by any haptic feedback setting.
mCurrentVibrationIntensities.put(USAGE_ACCESSIBILITY, positiveHapticFeedbackIntensity);
}
notifyListeners();
}
@Override
public String toString() {
synchronized (mLock) {
StringBuilder vibrationIntensitiesString = new StringBuilder("{");
for (int i = 0; i < mCurrentVibrationIntensities.size(); i++) {
int usage = mCurrentVibrationIntensities.keyAt(i);
int intensity = mCurrentVibrationIntensities.valueAt(i);
vibrationIntensitiesString.append(VibrationAttributes.usageToString(usage))
.append("=(").append(intensityToString(intensity))
.append(",default:").append(intensityToString(getDefaultIntensity(usage)))
.append("), ");
}
vibrationIntensitiesString.append('}');
return "VibrationSettings{"
+ "mVibratorConfig=" + mVibrationConfig
+ ", mVibrateInputDevices=" + mVibrateInputDevices
+ ", mBatterySaverMode=" + mBatterySaverMode
+ ", mProcStatesCache=" + mUidObserver.mProcStatesCache
+ ", mVibrationIntensities=" + vibrationIntensitiesString
+ '}';
}
}
/** Write current settings into given {@link ProtoOutputStream}. */
public void dumpProto(ProtoOutputStream proto) {
synchronized (mLock) {
proto.write(VibratorManagerServiceDumpProto.ALARM_INTENSITY,
getCurrentIntensity(USAGE_ALARM));
proto.write(VibratorManagerServiceDumpProto.ALARM_DEFAULT_INTENSITY,
getDefaultIntensity(USAGE_ALARM));
proto.write(VibratorManagerServiceDumpProto.HARDWARE_FEEDBACK_INTENSITY,
getCurrentIntensity(USAGE_HARDWARE_FEEDBACK));
proto.write(VibratorManagerServiceDumpProto.HARDWARE_FEEDBACK_DEFAULT_INTENSITY,
getDefaultIntensity(USAGE_HARDWARE_FEEDBACK));
proto.write(VibratorManagerServiceDumpProto.HAPTIC_FEEDBACK_INTENSITY,
getCurrentIntensity(USAGE_TOUCH));
proto.write(VibratorManagerServiceDumpProto.HAPTIC_FEEDBACK_DEFAULT_INTENSITY,
getDefaultIntensity(USAGE_TOUCH));
proto.write(VibratorManagerServiceDumpProto.MEDIA_INTENSITY,
getCurrentIntensity(USAGE_MEDIA));
proto.write(VibratorManagerServiceDumpProto.MEDIA_DEFAULT_INTENSITY,
getDefaultIntensity(USAGE_MEDIA));
proto.write(VibratorManagerServiceDumpProto.NOTIFICATION_INTENSITY,
getCurrentIntensity(USAGE_NOTIFICATION));
proto.write(VibratorManagerServiceDumpProto.NOTIFICATION_DEFAULT_INTENSITY,
getDefaultIntensity(USAGE_NOTIFICATION));
proto.write(VibratorManagerServiceDumpProto.RING_INTENSITY,
getCurrentIntensity(USAGE_RINGTONE));
proto.write(VibratorManagerServiceDumpProto.RING_DEFAULT_INTENSITY,
getDefaultIntensity(USAGE_RINGTONE));
}
}
private void notifyListeners() {
List<OnVibratorSettingsChanged> currentListeners;
synchronized (mLock) {
currentListeners = new ArrayList<>(mListeners);
}
for (OnVibratorSettingsChanged listener : currentListeners) {
listener.onChange();
}
}
private static String intensityToString(int intensity) {
switch (intensity) {
case Vibrator.VIBRATION_INTENSITY_OFF:
return "OFF";
case Vibrator.VIBRATION_INTENSITY_LOW:
return "LOW";
case Vibrator.VIBRATION_INTENSITY_MEDIUM:
return "MEDIUM";
case Vibrator.VIBRATION_INTENSITY_HIGH:
return "HIGH";
default:
return "UNKNOWN INTENSITY " + intensity;
}
}
@VibrationIntensity
private int toPositiveIntensity(int value, @VibrationIntensity int defaultValue) {
if (value == Vibrator.VIBRATION_INTENSITY_OFF) {
return defaultValue;
}
return toIntensity(value, defaultValue);
}
@VibrationIntensity
private int toIntensity(int value, @VibrationIntensity int defaultValue) {
if ((value < Vibrator.VIBRATION_INTENSITY_OFF)
|| (value > Vibrator.VIBRATION_INTENSITY_HIGH)) {
return defaultValue;
}
return value;
}
private boolean loadBooleanSetting(String settingKey) {
return Settings.System.getIntForUser(mContext.getContentResolver(),
settingKey, 0, UserHandle.USER_CURRENT) != 0;
}
private int loadSystemSetting(String settingName, int defaultValue) {
return Settings.System.getIntForUser(mContext.getContentResolver(),
settingName, defaultValue, UserHandle.USER_CURRENT);
}
private void registerSettingsObserver(Uri settingUri) {
mContext.getContentResolver().registerContentObserver(
settingUri, /* notifyForDescendants= */ true, mSettingObserver,
UserHandle.USER_ALL);
}
@Nullable
private VibrationEffect createEffectFromResource(int resId) {
long[] timings = getLongIntArray(mContext.getResources(), resId);
return createEffectFromTimings(timings);
}
@Nullable
private static VibrationEffect createEffectFromTimings(@Nullable long[] timings) {
if (timings == null || timings.length == 0) {
return null;
} else if (timings.length == 1) {
return VibrationEffect.createOneShot(timings[0], VibrationEffect.DEFAULT_AMPLITUDE);
} else {
return VibrationEffect.createWaveform(timings, -1);
}
}
private static long[] getLongIntArray(Resources r, int resid) {
int[] ar = r.getIntArray(resid);
if (ar == null) {
return null;
}
long[] out = new long[ar.length];
for (int i = 0; i < ar.length; i++) {
out[i] = ar[i];
}
return out;
}
/** Implementation of {@link ContentObserver} to be registered to a setting {@link Uri}. */
private final class SettingsObserver extends ContentObserver {
SettingsObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
updateSettings();
}
}
/** Implementation of {@link BroadcastReceiver} to update settings on current user change. */
@VisibleForTesting
final class UserObserver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) {
updateSettings();
}
}
}
/** Implementation of {@link ContentObserver} to be registered to a setting {@link Uri}. */
@VisibleForTesting
final class UidObserver extends IUidObserver.Stub {
private final SparseArray<Integer> mProcStatesCache = new SparseArray<>();
public boolean isUidForeground(int uid) {
return mProcStatesCache.get(uid, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND)
<= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND;
}
@Override
public void onUidGone(int uid, boolean disabled) {
mProcStatesCache.delete(uid);
}
@Override
public void onUidActive(int uid) {
}
@Override
public void onUidIdle(int uid, boolean disabled) {
}
@Override
public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
mProcStatesCache.put(uid, procState);
}
@Override
public void onUidCachedChanged(int uid, boolean cached) {
}
}
}