Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8d2117d1 authored by Lais Andrade's avatar Lais Andrade
Browse files

Enable system vibrations on screen off for other feedbacks

Enable system and SysUI vibrations with usage HARDWARE_FEEDBACK and
PHYSICAL_EMULATION to continue when the screen goes off, same behavior
current enabled for TOUCH vibrations.

Fix: 220270417
Test: VibrationSettingsTest
Change-Id: I0474b87c36ad856aed8c8f6639111b9efa51c15f
parent 5960382a
Loading
Loading
Loading
Loading
+39 −0
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManagerInternal;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.media.AudioManager;
@@ -42,6 +43,7 @@ import android.os.Handler;
import android.os.PowerManager;
import android.os.PowerManagerInternal;
import android.os.PowerSaveState;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.VibrationAttributes;
@@ -106,6 +108,19 @@ final class VibrationSettings {
     */
    private static final int VIBRATE_ON_DISABLED_USAGE_ALLOWED = USAGE_ACCESSIBILITY;

    /**
     * Set of usages allowed for vibrations from system packages when the screen goes off.
     *
     * <p>Some examples are touch and hardware feedback, and physical emulation. When the system is
     * playing one of these usages during the screen off event then the vibration will not be
     * cancelled by the service.
     */
    private static final Set<Integer> SYSTEM_VIBRATION_SCREEN_OFF_USAGE_ALLOWLIST = new HashSet<>(
            Arrays.asList(
                    USAGE_TOUCH,
                    USAGE_PHYSICAL_EMULATION,
                    USAGE_HARDWARE_FEEDBACK));

    /** Listener for changes on vibration settings. */
    interface OnVibratorSettingsChanged {
        /** Callback triggered when any of the vibrator settings change. */
@@ -114,6 +129,7 @@ final class VibrationSettings {

    private final Object mLock = new Object();
    private final Context mContext;
    private final String mSystemUiPackage;
    private final SettingsObserver mSettingObserver;
    @VisibleForTesting
    final UidObserver mUidObserver;
@@ -151,6 +167,9 @@ final class VibrationSettings {
        mUidObserver = new UidObserver();
        mUserReceiver = new UserObserver();

        mSystemUiPackage = LocalServices.getService(PackageManagerInternal.class)
                .getSystemUiServiceComponent().getPackageName();

        VibrationEffect clickEffect = createEffectFromResource(
                com.android.internal.R.array.config_virtualKeyVibePattern);
        VibrationEffect doubleClickEffect = createEffectFromResource(
@@ -343,6 +362,26 @@ final class VibrationSettings {
        return null;
    }

    /**
     * Check if given vibration should be cancelled by the service when the screen goes off.
     *
     * <p>When the system is entering a non-interactive state, we want to cancel vibrations in case
     * a misbehaving app has abandoned them. However, it may happen that the system is currently
     * playing haptic feedback as part of the transition. So we don't cancel system vibrations of
     * usages like touch and hardware feedback, and physical emulation.
     *
     * @return true if the vibration should be cancelled when the screen goes off, false otherwise.
     */
    public boolean shouldCancelVibrationOnScreenOff(int uid, String opPkg,
            @VibrationAttributes.Usage int usage) {
        if (!SYSTEM_VIBRATION_SCREEN_OFF_USAGE_ALLOWLIST.contains(usage)) {
            // Usages not allowed even for system vibrations should always be cancelled.
            return true;
        }
        // Only allow vibrations from System packages to continue vibrating when the screen goes off
        return uid != Process.SYSTEM_UID && uid != 0 && !mSystemUiPackage.equals(opPkg);
    }

    /**
     * Return {@code true} if the device should vibrate for current ringer mode.
     *
+10 −18
Original line number Diff line number Diff line
@@ -28,7 +28,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.hardware.vibrator.IVibrator;
import android.os.BatteryStats;
import android.os.Binder;
@@ -62,7 +61,6 @@ 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.LocalServices;
import com.android.server.SystemService;

import libcore.util.NativeAllocationRegistry;
@@ -120,7 +118,6 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {

    private final Object mLock = new Object();
    private final Context mContext;
    private final String mSystemUiPackage;
    private final PowerManager.WakeLock mWakeLock;
    private final IBatteryStats mBatteryStatsService;
    private final Handler mHandler;
@@ -153,17 +150,12 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
        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.  However it may happen that the system is currently playing
                    // haptic feedback as part of the transition.  So we don't cancel
                    // system vibrations.
                    if (mNextVibration != null
                            && !isSystemHapticFeedback(mNextVibration.getVibration())) {
                    // 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(Vibration.Status.CANCELLED);
                    }
                    if (mCurrentVibration != null
                            && !isSystemHapticFeedback(mCurrentVibration.getVibration())) {
                    if (shouldCancelOnScreenOffLocked(mCurrentVibration)) {
                        mCurrentVibration.cancel();
                    }
                }
@@ -203,9 +195,6 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
                com.android.internal.R.integer.config_previousVibrationsDumpLimit);
        mVibratorManagerRecords = new VibratorManagerRecords(dumpLimit);

        mSystemUiPackage = LocalServices.getService(PackageManagerInternal.class)
                .getSystemUiServiceComponent().getPackageName();

        mBatteryStatsService = injector.getBatteryStatsService();

        mAppOps = mContext.getSystemService(AppOpsManager.class);
@@ -1074,11 +1063,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
                == PackageManager.PERMISSION_GRANTED;
    }

    private boolean isSystemHapticFeedback(Vibration vib) {
        if (vib.attrs.getUsage() != VibrationAttributes.USAGE_TOUCH) {
    @GuardedBy("mLock")
    private boolean shouldCancelOnScreenOffLocked(@Nullable VibrationThread vibrationThread) {
        if (vibrationThread == null) {
            return false;
        }
        return vib.uid == Process.SYSTEM_UID || vib.uid == 0 || mSystemUiPackage.equals(vib.opPkg);
        Vibration vib = vibrationThread.getVibration();
        return mVibrationSettings.shouldCancelVibrationOnScreenOff(
                vib.uid, vib.opPkg, vib.attrs.getUsage());
    }

    @GuardedBy("mLock")
+18 −0
Original line number Diff line number Diff line
@@ -18,7 +18,10 @@ package com.android.server.vibrator;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;

import android.content.ComponentName;
import android.content.pm.PackageManagerInternal;
import android.hardware.vibrator.IVibrator;
import android.os.Handler;
import android.os.VibrationEffect;
@@ -33,8 +36,14 @@ import android.platform.test.annotations.Presubmit;

import androidx.test.InstrumentationRegistry;

import com.android.server.LocalServices;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import java.util.Arrays;
import java.util.stream.IntStream;
@@ -59,10 +68,19 @@ public class DeviceVibrationEffectAdapterTest {
            new VibratorInfo.FrequencyProfile(TEST_RESONANT_FREQUENCY, TEST_MIN_FREQUENCY,
                    TEST_FREQUENCY_RESOLUTION, TEST_AMPLITUDE_MAP);

    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();

    @Mock private PackageManagerInternal mPackageManagerInternalMock;

    private DeviceVibrationEffectAdapter mAdapter;

    @Before
    public void setUp() throws Exception {
        when(mPackageManagerInternalMock.getSystemUiServiceComponent())
                .thenReturn(new ComponentName("", ""));
        LocalServices.removeServiceForTest(PackageManagerInternal.class);
        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock);

        VibrationSettings vibrationSettings = new VibrationSettings(
                InstrumentationRegistry.getContext(), new Handler(new TestLooper().getLooper()));
        mAdapter = new DeviceVibrationEffectAdapter(vibrationSettings);
+7 −0
Original line number Diff line number Diff line
@@ -31,8 +31,10 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContextWrapper;
import android.content.pm.PackageManagerInternal;
import android.os.Handler;
import android.os.IExternalVibratorService;
import android.os.PowerManagerInternal;
@@ -76,6 +78,7 @@ public class VibrationScalerTest {
    @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();

    @Mock private PowerManagerInternal mPowerManagerInternalMock;
    @Mock private PackageManagerInternal mPackageManagerInternalMock;
    @Mock private VibrationConfig mVibrationConfigMock;

    private TestLooper mTestLooper;
@@ -90,7 +93,11 @@ public class VibrationScalerTest {

        ContentResolver contentResolver = mSettingsProviderRule.mockContentResolver(mContextSpy);
        when(mContextSpy.getContentResolver()).thenReturn(contentResolver);
        when(mPackageManagerInternalMock.getSystemUiServiceComponent())
                .thenReturn(new ComponentName("", ""));

        LocalServices.removeServiceForTest(PackageManagerInternal.class);
        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock);
        LocalServices.removeServiceForTest(PowerManagerInternal.class);
        LocalServices.addService(PowerManagerInternal.class, mPowerManagerInternalMock);

+63 −11
Original line number Diff line number Diff line
@@ -47,13 +47,16 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.pm.PackageManagerInternal;
import android.media.AudioManager;
import android.os.Handler;
import android.os.PowerManagerInternal;
import android.os.PowerSaveState;
import android.os.Process;
import android.os.UserHandle;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
@@ -87,6 +90,7 @@ import org.mockito.junit.MockitoRule;
public class VibrationSettingsTest {

    private static final int UID = 1;
    private static final String SYSUI_PACKAGE_NAME = "sysui";
    private static final PowerSaveState NORMAL_POWER_STATE = new PowerSaveState.Builder().build();
    private static final PowerSaveState LOW_POWER_STATE = new PowerSaveState.Builder()
            .setBatterySaverEnabled(true).build();
@@ -104,17 +108,13 @@ public class VibrationSettingsTest {
            USAGE_TOUCH,
    };

    @Rule
    public MockitoRule mMockitoRule = MockitoJUnit.rule();
    @Rule
    public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
    @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();

    @Mock
    private VibrationSettings.OnVibratorSettingsChanged mListenerMock;
    @Mock
    private PowerManagerInternal mPowerManagerInternalMock;
    @Mock
    private VibrationConfig mVibrationConfigMock;
    @Mock private VibrationSettings.OnVibratorSettingsChanged mListenerMock;
    @Mock private PowerManagerInternal mPowerManagerInternalMock;
    @Mock private PackageManagerInternal mPackageManagerInternalMock;
    @Mock private VibrationConfig mVibrationConfigMock;

    private TestLooper mTestLooper;
    private ContextWrapper mContextSpy;
@@ -129,14 +129,17 @@ public class VibrationSettingsTest {

        ContentResolver contentResolver = mSettingsProviderRule.mockContentResolver(mContextSpy);
        when(mContextSpy.getContentResolver()).thenReturn(contentResolver);

        doAnswer(invocation -> {
            mRegisteredPowerModeListener = invocation.getArgument(0);
            return null;
        }).when(mPowerManagerInternalMock).registerLowPowerModeObserver(any());
        when(mPackageManagerInternalMock.getSystemUiServiceComponent())
                .thenReturn(new ComponentName(SYSUI_PACKAGE_NAME, ""));

        LocalServices.removeServiceForTest(PowerManagerInternal.class);
        LocalServices.addService(PowerManagerInternal.class, mPowerManagerInternalMock);
        LocalServices.removeServiceForTest(PackageManagerInternal.class);
        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock);

        setDefaultIntensity(VIBRATION_INTENSITY_MEDIUM);
        mAudioManager = mContextSpy.getSystemService(AudioManager.class);
@@ -471,6 +474,55 @@ public class VibrationSettingsTest {
        assertFalse(mVibrationSettings.shouldVibrateInputDevices());
    }

    @Test
    public void shouldCancelVibrationOnScreenOff_withNonSystemPackageAndUid_returnsAlwaysTrue() {
        for (int usage : ALL_USAGES) {
            assertTrue(mVibrationSettings.shouldCancelVibrationOnScreenOff(UID, "some.app", usage));
        }
    }

    @Test
    public void shouldCancelVibrationOnScreenOff_withUidZero_returnsFalseForTouchAndHardware() {
        for (int usage : ALL_USAGES) {
            if (usage == USAGE_TOUCH || usage == USAGE_HARDWARE_FEEDBACK
                    || usage == USAGE_PHYSICAL_EMULATION) {
                assertFalse(mVibrationSettings.shouldCancelVibrationOnScreenOff(
                        /* uid= */ 0, "", usage));
            } else {
                assertTrue(mVibrationSettings.shouldCancelVibrationOnScreenOff(
                        /* uid= */ 0, "", usage));
            }
        }
    }

    @Test
    public void shouldCancelVibrationOnScreenOff_withSystemUid_returnsFalseForTouchAndHardware() {
        for (int usage : ALL_USAGES) {
            if (usage == USAGE_TOUCH || usage == USAGE_HARDWARE_FEEDBACK
                    || usage == USAGE_PHYSICAL_EMULATION) {
                assertFalse(mVibrationSettings.shouldCancelVibrationOnScreenOff(
                        Process.SYSTEM_UID, "", usage));
            } else {
                assertTrue(mVibrationSettings.shouldCancelVibrationOnScreenOff(
                        Process.SYSTEM_UID, "", usage));
            }
        }
    }

    @Test
    public void shouldCancelVibrationOnScreenOff_withSysUi_returnsFalseForTouchAndHardware() {
        for (int usage : ALL_USAGES) {
            if (usage == USAGE_TOUCH || usage == USAGE_HARDWARE_FEEDBACK
                    || usage == USAGE_PHYSICAL_EMULATION) {
                assertFalse(mVibrationSettings.shouldCancelVibrationOnScreenOff(
                        UID, SYSUI_PACKAGE_NAME, usage));
            } else {
                assertTrue(mVibrationSettings.shouldCancelVibrationOnScreenOff(
                        UID, SYSUI_PACKAGE_NAME, usage));
            }
        }
    }

    @Test
    public void getDefaultIntensity_returnsIntensityFromVibratorConfig() {
        setDefaultIntensity(VIBRATION_INTENSITY_HIGH);
Loading