From 9b209d5690e5d0cd45232f76d59747a5015bcd35 Mon Sep 17 00:00:00 2001 From: Lais Andrade Date: Fri, 7 May 2021 16:52:17 +0100 Subject: [PATCH] Add new default vibration for notifications Use the new WaveformBuilder to create a new default vibration waveform that also changes the vibration frequency on devices that support it. Create VibratorHelper in notification package to extract interactions with the Vibrator service, the creation of VibrationEffect instances and loading the default and fallback vibrations, using the new pattern when possible. Also update NotificationRecord to store a built VibrationEffect instead of a amplitude-only waveform pattern. Fix: 186740660 Test: BuzzBeepBlinkTest NotificationRecordTest VibratorHelperTest Change-Id: I6c93eb729972abb7a0cfc2b6fd567606224f2f2b --- .../NotificationManagerService.java | 72 ++------- .../notification/NotificationRecord.java | 24 +-- .../server/notification/VibratorHelper.java | 153 ++++++++++++++++++ .../server/vibrator/VibratorController.java | 2 +- .../vibrator/VibratorControllerTest.java | 3 +- .../notification/BuzzBeepBlinkTest.java | 53 +++--- .../NotificationComparatorTest.java | 3 + .../notification/NotificationRecordTest.java | 43 ++++- .../notification/RankingHelperTest.java | 3 + .../notification/VibratorHelperTest.java | 111 +++++++++++++ 10 files changed, 367 insertions(+), 100 deletions(-) create mode 100644 services/core/java/com/android/server/notification/VibratorHelper.java create mode 100644 services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 0bc28405eab3d..d849abb3f0bea 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -211,9 +211,7 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; -import android.os.VibrationAttributes; import android.os.VibrationEffect; -import android.os.Vibrator; import android.provider.DeviceConfig; import android.provider.Settings; import android.service.notification.Adjustment; @@ -369,12 +367,8 @@ public class NotificationManagerService extends SystemService { // 1 second past the ANR timeout. static final int FINISH_TOKEN_TIMEOUT = 11 * 1000; - static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250}; - static final long SNOOZE_UNTIL_UNSPECIFIED = -1; - static final int VIBRATE_PATTERN_MAXLEN = 8 * 2 + 1; // up to eight bumps - static final int INVALID_UID = -1; static final String ROOT_PKG = "root"; @@ -482,7 +476,6 @@ public class NotificationManagerService extends SystemService { AudioManagerInternal mAudioManagerInternal; // Can be null for wear @Nullable StatusBarManagerInternal mStatusBar; - Vibrator mVibrator; private WindowManagerInternal mWindowManagerInternal; private AlarmManager mAlarmManager; private ICompanionDeviceManager mCompanionManager; @@ -504,7 +497,6 @@ public class NotificationManagerService extends SystemService { private LogicalLight mNotificationLight; LogicalLight mAttentionLight; - private long[] mFallbackVibrationPattern; private boolean mUseAttentionLight; boolean mHasLight = true; boolean mLightEnabled; @@ -583,6 +575,7 @@ public class NotificationManagerService extends SystemService { RankingHelper mRankingHelper; @VisibleForTesting PreferencesHelper mPreferencesHelper; + private VibratorHelper mVibratorHelper; private final UserProfiles mUserProfiles = new UserProfiles(); private NotificationListeners mListeners; @@ -1597,10 +1590,7 @@ public class NotificationManagerService extends SystemService { mVibrateNotificationKey = null; final long identity = Binder.clearCallingIdentity(); try { - // Stop all vibrations with usage of class alarm (ringtone, alarm, notification usages). - int usageFilter = - VibrationAttributes.USAGE_CLASS_ALARM | ~VibrationAttributes.USAGE_CLASS_MASK; - mVibrator.cancel(usageFilter); + mVibratorHelper.cancelVibration(); } finally { Binder.restoreCallingIdentity(identity); } @@ -1994,19 +1984,6 @@ public class NotificationManagerService extends SystemService { private SettingsObserver mSettingsObserver; protected ZenModeHelper mZenModeHelper; - static long[] getLongArray(Resources r, int resid, int maxlen, long[] def) { - int[] ar = r.getIntArray(resid); - if (ar == null) { - return def; - } - final int len = ar.length > maxlen ? maxlen : ar.length; - long[] out = new long[len]; - for (int i=0; i { // delay the vibration by the same amount as the notification sound @@ -7680,8 +7643,7 @@ public class NotificationManagerService extends SystemService { // to the reason so we can still debug from bugreports String reason = "Notification (" + record.getSbn().getOpPkg() + " " + record.getSbn().getUid() + ") " + (delayed ? "(Delayed)" : ""); - mVibrator.vibrate(Process.SYSTEM_UID, PackageManagerService.PLATFORM_PACKAGE_NAME, - effect, reason, record.getAudioAttributes()); + mVibratorHelper.vibrate(effect, record.getAudioAttributes(), reason); } private boolean isNotificationForCurrentUser(NotificationRecord record) { diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index e875065441889..74d731b95d40a 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -47,6 +47,7 @@ import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.UserHandle; +import android.os.VibrationEffect; import android.provider.Settings; import android.service.notification.Adjustment; import android.service.notification.NotificationListenerService; @@ -158,7 +159,7 @@ public final class NotificationRecord { private String mUserExplanation; private boolean mPreChannelsNotification = true; private Uri mSound; - private long[] mVibration; + private VibrationEffect mVibration; private AudioAttributes mAttributes; private NotificationChannel mChannel; private ArrayList mPeopleOverride; @@ -287,29 +288,28 @@ public final class NotificationRecord { return light; } - private long[] calculateVibration() { - long[] vibration; - final long[] defaultVibration = NotificationManagerService.getLongArray( - mContext.getResources(), - com.android.internal.R.array.config_defaultNotificationVibePattern, - NotificationManagerService.VIBRATE_PATTERN_MAXLEN, - NotificationManagerService.DEFAULT_VIBRATE_PATTERN); + private VibrationEffect calculateVibration() { + VibratorHelper helper = new VibratorHelper(mContext); + final Notification notification = getSbn().getNotification(); + final boolean insistent = (notification.flags & Notification.FLAG_INSISTENT) != 0; + VibrationEffect defaultVibration = helper.createDefaultVibration(insistent); + VibrationEffect vibration; if (getChannel().shouldVibrate()) { vibration = getChannel().getVibrationPattern() == null - ? defaultVibration : getChannel().getVibrationPattern(); + ? defaultVibration + : helper.createWaveformVibration(getChannel().getVibrationPattern(), insistent); } else { vibration = null; } if (mPreChannelsNotification && (getChannel().getUserLockedFields() & NotificationChannel.USER_LOCKED_VIBRATION) == 0) { - final Notification notification = getSbn().getNotification(); final boolean useDefaultVibrate = (notification.defaults & Notification.DEFAULT_VIBRATE) != 0; if (useDefaultVibrate) { vibration = defaultVibration; } else { - vibration = notification.vibrate; + vibration = helper.createWaveformVibration(notification.vibrate, insistent); } } return vibration; @@ -1067,7 +1067,7 @@ public final class NotificationRecord { return mSound; } - public long[] getVibration() { + public VibrationEffect getVibration() { return mVibration; } diff --git a/services/core/java/com/android/server/notification/VibratorHelper.java b/services/core/java/com/android/server/notification/VibratorHelper.java new file mode 100644 index 0000000000000..f25b0470cc8ae --- /dev/null +++ b/services/core/java/com/android/server/notification/VibratorHelper.java @@ -0,0 +1,153 @@ +/* + * 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.notification; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.media.AudioAttributes; +import android.os.Process; +import android.os.VibrationAttributes; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.util.Slog; + +import com.android.internal.R; +import com.android.server.pm.PackageManagerService; + +import java.util.Arrays; + +/** + * NotificationManagerService helper for functionality related to the vibrator. + */ +public final class VibratorHelper { + private static final String TAG = "NotificationVibratorHelper"; + + private static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250}; + private static final int VIBRATE_PATTERN_MAXLEN = 8 * 2 + 1; // up to eight bumps + + private final Vibrator mVibrator; + private final long[] mDefaultPattern; + private final long[] mFallbackPattern; + + public VibratorHelper(Context context) { + mVibrator = context.getSystemService(Vibrator.class); + mDefaultPattern = getLongArray( + context.getResources(), + com.android.internal.R.array.config_defaultNotificationVibePattern, + VIBRATE_PATTERN_MAXLEN, + DEFAULT_VIBRATE_PATTERN); + mFallbackPattern = getLongArray(context.getResources(), + R.array.config_notificationFallbackVibePattern, + VIBRATE_PATTERN_MAXLEN, + DEFAULT_VIBRATE_PATTERN); + } + + /** + * Safely create a {@link VibrationEffect} from given vibration {@code pattern}. + * + *

This method returns {@code null} if the pattern is also {@code null} or invalid. + * + * @param pattern The off/on vibration pattern, where each item is a duration in milliseconds. + * @param insistent {@code true} if the vibration should loop until it is cancelled. + */ + @Nullable + public static VibrationEffect createWaveformVibration(@Nullable long[] pattern, + boolean insistent) { + try { + if (pattern != null) { + return VibrationEffect.createWaveform(pattern, /* repeat= */ insistent ? 0 : -1); + } + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Error creating vibration waveform with pattern: " + + Arrays.toString(pattern)); + } + return null; + } + + /** + * Vibrate the device with given {@code effect}. + * + *

We need to vibrate as "android" so we can breakthrough DND. + */ + public void vibrate(VibrationEffect effect, AudioAttributes attrs, String reason) { + mVibrator.vibrate(Process.SYSTEM_UID, PackageManagerService.PLATFORM_PACKAGE_NAME, + effect, reason, attrs); + } + + /** Stop all notification vibrations (ringtone, alarm, notification usages). */ + public void cancelVibration() { + int usageFilter = + VibrationAttributes.USAGE_CLASS_ALARM | ~VibrationAttributes.USAGE_CLASS_MASK; + mVibrator.cancel(usageFilter); + } + + /** + * Creates a vibration to be used as fallback when the device is in vibrate mode. + * + * @param insistent {@code true} if the vibration should loop until it is cancelled. + */ + public VibrationEffect createFallbackVibration(boolean insistent) { + if (mVibrator.hasFrequencyControl()) { + return createChirpVibration(insistent); + } + return createWaveformVibration(mFallbackPattern, insistent); + } + + /** + * Creates a vibration to be used by notifications without a custom pattern. + * + * @param insistent {@code true} if the vibration should loop until it is cancelled. + */ + public VibrationEffect createDefaultVibration(boolean insistent) { + if (mVibrator.hasFrequencyControl()) { + return createChirpVibration(insistent); + } + return createWaveformVibration(mDefaultPattern, insistent); + } + + private static VibrationEffect createChirpVibration(boolean insistent) { + VibrationEffect.WaveformBuilder waveformBuilder = VibrationEffect.startWaveform() + .addStep(/* amplitude= */ 0, /* frequency= */ -0.85f, /* duration= */ 0) + .addRamp(/* amplitude= */ 1, /* frequency= */ -0.25f, /* duration= */ 100) + .addStep(/* amplitude= */ 1, /* duration= */ 150) + .addRamp(/* amplitude= */ 0, /* frequency= */ -0.85f, /* duration= */ 250); + + if (insistent) { + return waveformBuilder.build(/* repeat= */ 0); + } + + VibrationEffect singleBeat = waveformBuilder.build(); + return VibrationEffect.startComposition() + .addEffect(singleBeat) + .addEffect(singleBeat) + .compose(); + } + + private static long[] getLongArray(Resources resources, int resId, int maxLength, long[] def) { + int[] ar = resources.getIntArray(resId); + if (ar == null) { + return def; + } + final int len = ar.length > maxLength ? maxLength : ar.length; + long[] out = new long[len]; + for (int i = 0; i < len; i++) { + out[i] = ar[i]; + } + return out; + } +} diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java index a09bfc57ba233..5dceac2d066cb 100644 --- a/services/core/java/com/android/server/vibrator/VibratorController.java +++ b/services/core/java/com/android/server/vibrator/VibratorController.java @@ -67,7 +67,7 @@ final class VibratorController { mNativeWrapper = nativeWrapper; mNativeWrapper.init(vibratorId, listener); // TODO(b/167947076): load suggested range from config - mVibratorInfo = mNativeWrapper.getInfo(/* suggestedFrequencyRange= */ 100); + mVibratorInfo = mNativeWrapper.getInfo(/* suggestedFrequencyRange= */ 200); Preconditions.checkNotNull(mVibratorInfo, "Failed to retrieve data for vibrator %d", vibratorId); } diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java index 59370e2106528..9e98e7d0410c6 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; @@ -297,7 +298,7 @@ public class VibratorControllerTest { private void mockVibratorCapabilities(int capabilities) { VibratorInfo.FrequencyMapping frequencyMapping = new VibratorInfo.FrequencyMapping( Float.NaN, Float.NaN, Float.NaN, Float.NaN, null); - when(mNativeWrapperMock.getInfo(/* suggestedFrequencyRange= */ 100)).thenReturn( + when(mNativeWrapperMock.getInfo(anyFloat())).thenReturn( new VibratorInfo.Builder(VIBRATOR_ID) .setCapabilities(capabilities) .setFrequencyMapping(frequencyMapping) diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java index 00eb0f2847196..3862d754b8f5a 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java @@ -93,6 +93,9 @@ import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import org.mockito.verification.VerificationMode; + +import java.util.Objects; @SmallTest @RunWith(AndroidJUnit4.class) @@ -137,20 +140,19 @@ public class BuzzBeepBlinkTest extends UiServiceTestCase { private static final int CUSTOM_LIGHT_COLOR = Color.BLACK; private static final int CUSTOM_LIGHT_ON = 10000; private static final int CUSTOM_LIGHT_OFF = 10000; - private static final long[] FALLBACK_VIBRATION_PATTERN = new long[] {100, 100, 100}; - private static final VibrationEffect FALLBACK_VIBRATION = - VibrationEffect.createWaveform(FALLBACK_VIBRATION_PATTERN, -1); private static final int MAX_VIBRATION_DELAY = 1000; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + getContext().addMockSystemService(Vibrator.class, mVibrator); when(mAudioManager.isAudioFocusExclusive()).thenReturn(false); when(mAudioManager.getRingtonePlayer()).thenReturn(mRingtonePlayer); when(mAudioManager.getStreamVolume(anyInt())).thenReturn(10); when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL); when(mUsageStats.isAlertRateLimited(any())).thenReturn(false); + when(mVibrator.hasFrequencyControl()).thenReturn(false); long serviceReturnValue = IntPair.of( AccessibilityManager.STATE_FLAG_ACCESSIBILITY_ENABLED, @@ -164,13 +166,12 @@ public class BuzzBeepBlinkTest extends UiServiceTestCase { mService = spy(new NotificationManagerService(getContext(), mNotificationRecordLogger, mNotificationInstanceIdSequence)); + mService.setVibratorHelper(new VibratorHelper(getContext())); mService.setAudioManager(mAudioManager); - mService.setVibrator(mVibrator); mService.setSystemReady(true); mService.setHandler(mHandler); mService.setLights(mLight); mService.setScreenOn(false); - mService.setFallbackVibrationPattern(FALLBACK_VIBRATION_PATTERN); mService.setUsageStats(mUsageStats); mService.setAccessibilityManager(accessibilityManager); mService.mScreenOn = false; @@ -416,32 +417,34 @@ public class BuzzBeepBlinkTest extends UiServiceTestCase { } private void verifyVibrate() { - ArgumentCaptor captor = ArgumentCaptor.forClass(AudioAttributes.class); - verify(mVibrator, times(1)).vibrate(anyInt(), anyString(), argThat(mVibrateOnceMatcher), - anyString(), captor.capture()); - assertEquals(0, (captor.getValue().getAllFlags() - & AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY)); + verifyVibrate(/* times= */ 1); } private void verifyVibrate(int times) { - verify(mVibrator, times(times)).vibrate(eq(Process.SYSTEM_UID), - eq(PackageManagerService.PLATFORM_PACKAGE_NAME), any(), anyString(), - any(AudioAttributes.class)); + verifyVibrate(mVibrateOnceMatcher, times(times)); } private void verifyVibrateLooped() { - verify(mVibrator, times(1)).vibrate(anyInt(), anyString(), argThat(mVibrateLoopMatcher), - anyString(), any(AudioAttributes.class)); + verifyVibrate(mVibrateLoopMatcher, times(1)); } private void verifyDelayedVibrateLooped() { - verify(mVibrator, timeout(MAX_VIBRATION_DELAY).times(1)).vibrate(anyInt(), anyString(), - argThat(mVibrateLoopMatcher), anyString(), any(AudioAttributes.class)); + verifyVibrate(mVibrateLoopMatcher, timeout(MAX_VIBRATION_DELAY).times(1)); } - private void verifyDelayedVibrate() { - verify(mVibrator, timeout(MAX_VIBRATION_DELAY).times(1)).vibrate(anyInt(), anyString(), - argThat(mVibrateOnceMatcher), anyString(), any(AudioAttributes.class)); + private void verifyDelayedVibrate(VibrationEffect effect) { + verifyVibrate(argument -> Objects.equals(effect, argument), + timeout(MAX_VIBRATION_DELAY).times(1)); + } + + private void verifyVibrate(ArgumentMatcher effectMatcher, + VerificationMode verification) { + ArgumentCaptor captor = ArgumentCaptor.forClass(AudioAttributes.class); + verify(mVibrator, verification).vibrate(eq(Process.SYSTEM_UID), + eq(PackageManagerService.PLATFORM_PACKAGE_NAME), argThat(effectMatcher), + anyString(), captor.capture()); + assertEquals(0, (captor.getValue().getAllFlags() + & AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY)); } private void verifyStopVibrate() { @@ -761,11 +764,7 @@ public class BuzzBeepBlinkTest extends UiServiceTestCase { mService.buzzBeepBlinkLocked(r); - VibrationEffect effect = VibrationEffect.createWaveform(r.getVibration(), -1); - - verify(mVibrator, timeout(MAX_VIBRATION_DELAY).times(1)).vibrate(anyInt(), anyString(), - eq(effect), anyString(), - (AudioAttributes) anyObject()); + verifyDelayedVibrate(r.getVibration()); assertTrue(r.isInterruptive()); assertNotEquals(-1, r.getLastAudiblyAlertedMs()); } @@ -800,8 +799,8 @@ public class BuzzBeepBlinkTest extends UiServiceTestCase { mService.buzzBeepBlinkLocked(r); - verify(mVibrator, timeout(MAX_VIBRATION_DELAY).times(1)).vibrate(anyInt(), anyString(), - eq(FALLBACK_VIBRATION), anyString(), (AudioAttributes) anyObject()); + verifyDelayedVibrate( + mService.getVibratorHelper().createFallbackVibration(/* insistent= */ false)); verify(mRingtonePlayer, never()).playAsync (anyObject(), anyObject(), anyBoolean(), anyObject()); assertTrue(r.isInterruptive()); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java index 6a8f602a8350e..5eed30be92795 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java @@ -37,6 +37,7 @@ import android.content.pm.PackageManager; import android.graphics.Color; import android.os.Build; import android.os.UserHandle; +import android.os.Vibrator; import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.telecom.TelecomManager; @@ -63,6 +64,7 @@ public class NotificationComparatorTest extends UiServiceTestCase { @Mock TelecomManager mTm; @Mock RankingHandler handler; @Mock PackageManager mPm; + @Mock Vibrator mVibrator; private final String callPkg = "com.android.server.notification"; private final int callUid = 10; @@ -98,6 +100,7 @@ public class NotificationComparatorTest extends UiServiceTestCase { when(mContext.getContentResolver()).thenReturn(getContext().getContentResolver()); when(mContext.getPackageManager()).thenReturn(mPm); when(mContext.getSystemService(eq(Context.TELECOM_SERVICE))).thenReturn(mTm); + when(mContext.getSystemService(Vibrator.class)).thenReturn(mVibrator); when(mContext.getString(anyInt())).thenCallRealMethod(); when(mContext.getColor(anyInt())).thenCallRealMethod(); when(mTm.getDefaultDialerPackage()).thenReturn(callPkg); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java index 38c470da50294..49b63863fec75 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java @@ -62,6 +62,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.UserHandle; +import android.os.Vibrator; import android.provider.Settings; import android.service.notification.Adjustment; import android.service.notification.StatusBarNotification; @@ -82,7 +83,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; -import java.util.Arrays; @SmallTest @RunWith(AndroidJUnit4.class) @@ -91,6 +91,7 @@ public class NotificationRecordTest extends UiServiceTestCase { private final Context mMockContext = mock(Context.class); @Mock private PackageManager mPm; @Mock private ContentResolver mContentResolver; + @Mock private Vibrator mVibrator; private final String mPkg = PKG_O; private final int uid = 9583; @@ -122,6 +123,7 @@ public class NotificationRecordTest extends UiServiceTestCase { public void setUp() { MockitoAnnotations.initMocks(this); + when(mMockContext.getSystemService(eq(Vibrator.class))).thenReturn(mVibrator); when(mMockContext.getResources()).thenReturn(getContext().getResources()); when(mMockContext.getPackageManager()).thenReturn(mPm); when(mMockContext.getContentResolver()).thenReturn(mContentResolver); @@ -203,6 +205,26 @@ public class NotificationRecordTest extends UiServiceTestCase { return new StatusBarNotification(mPkg, mPkg, id1, tag1, uid, uid, n, mUser, null, uid); } + + private StatusBarNotification getInsistentNotification(boolean defaultVibration) { + final Builder builder = new Builder(mMockContext) + .setContentTitle("foo") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setPriority(Notification.PRIORITY_HIGH); + int defaults = 0; + if (defaultVibration) { + defaults |= Notification.DEFAULT_VIBRATE; + } else { + builder.setVibrate(CUSTOM_VIBRATION); + channel.setVibrationPattern(CUSTOM_CHANNEL_VIBRATION); + } + builder.setDefaults(defaults); + builder.setFlag(Notification.FLAG_INSISTENT, true); + + Notification n = builder.build(); + return new StatusBarNotification(mPkg, mPkg, id1, tag1, uid, uid, n, mUser, null, uid); + } + private StatusBarNotification getMessagingStyleNotification() { return getMessagingStyleNotification(mPkg); } @@ -309,7 +331,8 @@ public class NotificationRecordTest extends UiServiceTestCase { false /* lights */, false /* defaultLights */, null /* group */); NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel); - assertEquals(CUSTOM_VIBRATION, record.getVibration()); + assertEquals(VibratorHelper.createWaveformVibration( + CUSTOM_VIBRATION, /* insistent= */ false), record.getVibration()); } @Test @@ -322,7 +345,8 @@ public class NotificationRecordTest extends UiServiceTestCase { false /* lights */, false /* defaultLights */, null /* group */); NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel); - assertTrue(!Arrays.equals(CUSTOM_VIBRATION, record.getVibration())); + assertNotEquals(VibratorHelper.createWaveformVibration( + CUSTOM_VIBRATION, /* insistent= */ false), record.getVibration()); } @Test @@ -334,7 +358,18 @@ public class NotificationRecordTest extends UiServiceTestCase { false /* lights */, false /* defaultLights */, null /* group */); NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel); - assertEquals(CUSTOM_CHANNEL_VIBRATION, record.getVibration()); + assertEquals(VibratorHelper.createWaveformVibration( + CUSTOM_CHANNEL_VIBRATION, /* insistent= */ false), record.getVibration()); + } + + @Test + public void testVibration_insistent_createsInsistentVibrationEffect() { + channel.enableVibration(true); + StatusBarNotification sbn = getInsistentNotification(false /* defaultBuzz */); + + NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel); + assertEquals(VibratorHelper.createWaveformVibration( + CUSTOM_CHANNEL_VIBRATION, /* insistent= */ true), record.getVibration()); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java index b83f9f266ad72..4217881495b16 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java @@ -41,6 +41,7 @@ import android.media.AudioAttributes; import android.net.Uri; import android.os.Build; import android.os.UserHandle; +import android.os.Vibrator; import android.service.notification.StatusBarNotification; import android.test.suitebuilder.annotation.SmallTest; import android.testing.TestableContentResolver; @@ -84,6 +85,7 @@ public class RankingHelperTest extends UiServiceTestCase { @Mock Context mContext; @Mock ZenModeHelper mMockZenModeHelper; @Mock RankingConfig mConfig; + @Mock Vibrator mVibrator; private NotificationManager.Policy mTestNotificationPolicy; private Notification mNotiGroupGSortA; @@ -125,6 +127,7 @@ public class RankingHelperTest extends UiServiceTestCase { InstrumentationRegistry.getContext().getContentResolver()); when(mContext.getPackageManager()).thenReturn(mPm); when(mContext.getApplicationInfo()).thenReturn(legacy); + when(mContext.getSystemService(Vibrator.class)).thenReturn(mVibrator); TestableContentResolver contentResolver = getContext().getContentResolver(); contentResolver.setFallbackToExisting(false); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java new file mode 100644 index 0000000000000..c77a474e032c6 --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java @@ -0,0 +1,111 @@ +/* + * 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.notification; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.Mockito.when; + +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.test.suitebuilder.annotation.SmallTest; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.UiServiceTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class VibratorHelperTest extends UiServiceTestCase { + + private static final long[] CUSTOM_PATTERN = new long[] { 100, 200, 300, 400 }; + + @Mock private Vibrator mVibrator; + + VibratorHelper mVibratorHelper; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + getContext().addMockSystemService(Vibrator.class, mVibrator); + + mVibratorHelper = new VibratorHelper(getContext()); + } + + @Test + public void createWaveformVibration_insistent_createsRepeatingVibration() { + assertRepeatingVibration( + VibratorHelper.createWaveformVibration(CUSTOM_PATTERN, /* insistent= */ true)); + } + + @Test + public void createWaveformVibration_nonInsistent_createsSingleShotVibration() { + assertSingleVibration( + VibratorHelper.createWaveformVibration(CUSTOM_PATTERN, /* insistent= */ false)); + } + + @Test + public void createWaveformVibration_invalidPattern_returnsNullAndDoesNotCrash() { + assertNull(VibratorHelper.createWaveformVibration(null, false)); + assertNull(VibratorHelper.createWaveformVibration(new long[0], false)); + assertNull(VibratorHelper.createWaveformVibration(new long[] { 0, 0 }, false)); + } + + @Test + public void createVibration_insistent_createsRepeatingVibration() { + when(mVibrator.hasFrequencyControl()).thenReturn(false); + assertRepeatingVibration(mVibratorHelper.createDefaultVibration(/* insistent= */ true)); + assertRepeatingVibration(mVibratorHelper.createFallbackVibration(/* insistent= */ true)); + + when(mVibrator.hasFrequencyControl()).thenReturn(true); + assertRepeatingVibration(mVibratorHelper.createDefaultVibration(/* insistent= */ true)); + assertRepeatingVibration(mVibratorHelper.createFallbackVibration(/* insistent= */ true)); + } + + @Test + public void createVibration_nonInsistent_createsSingleShotVibration() { + when(mVibrator.hasFrequencyControl()).thenReturn(false); + assertSingleVibration(mVibratorHelper.createDefaultVibration(/* insistent= */ false)); + assertSingleVibration(mVibratorHelper.createFallbackVibration(/* insistent= */ false)); + + when(mVibrator.hasFrequencyControl()).thenReturn(true); + assertSingleVibration(mVibratorHelper.createDefaultVibration(/* insistent= */ false)); + assertSingleVibration(mVibratorHelper.createFallbackVibration(/* insistent= */ false)); + } + + private void assertRepeatingVibration(VibrationEffect effect) { + assertTrue(getRepeatIndex(effect) >= 0); + } + + private void assertSingleVibration(VibrationEffect effect) { + assertEquals(-1, getRepeatIndex(effect)); + } + + private static int getRepeatIndex(VibrationEffect effect) { + assertTrue("Unknown vibration effect " + effect, + effect instanceof VibrationEffect.Composed); + return ((VibrationEffect.Composed) effect).getRepeatIndex(); + } +}